/* * Copyright (C) 2012, GitHub Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which * accompanies this distribution, is reproduced below, and is * available at http://www.eclipse.org/org/documents/edl-v10.php * * All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * - Neither the name of the Eclipse Foundation, Inc. nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.eclipse.jgit.api; import java.io.File; import java.io.IOException; import java.text.MessageFormat; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheEditor; import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath; import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.CheckoutConflictException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FileUtils; /** * Command class to apply a stashed commit. * * @see Git documentation about Stash * @since 2.0 */ public class StashApplyCommand extends GitCommand { private static final String DEFAULT_REF = Constants.STASH + "@{0}"; /** * Stash diff filter that looks for differences in the first three trees * which must be the stash head tree, stash index tree, and stash working * directory tree in any order. */ private static class StashDiffFilter extends TreeFilter { @Override public boolean include(final TreeWalk walker) { final int m = walker.getRawMode(0); if (walker.getRawMode(1) != m || !walker.idEqual(1, 0)) return true; if (walker.getRawMode(2) != m || !walker.idEqual(2, 0)) return true; return false; } @Override public boolean shouldBeRecursive() { return false; } @Override public TreeFilter clone() { return this; } @Override public String toString() { return "STASH_DIFF"; } } private String stashRef; /** * Create command to apply the changes of a stashed commit * * @param repo */ public StashApplyCommand(final Repository repo) { super(repo); } /** * Set the stash reference to apply *

* This will default to apply the latest stashed commit (stash@{0}) if * unspecified * * @param stashRef * @return {@code this} */ public StashApplyCommand setStashRef(final String stashRef) { this.stashRef = stashRef; return this; } private boolean isEqualEntry(AbstractTreeIterator iter1, AbstractTreeIterator iter2) { if (!iter1.getEntryFileMode().equals(iter2.getEntryFileMode())) return false; ObjectId id1 = iter1.getEntryObjectId(); ObjectId id2 = iter2.getEntryObjectId(); return id1 != null ? id1.equals(id2) : id2 == null; } /** * Would unstashing overwrite local changes? * * @param stashIndexIter * @param stashWorkingTreeIter * @param headIter * @param indexIter * @param workingTreeIter * @return true if unstash conflict, false otherwise */ private boolean isConflict(AbstractTreeIterator stashIndexIter, AbstractTreeIterator stashWorkingTreeIter, AbstractTreeIterator headIter, AbstractTreeIterator indexIter, AbstractTreeIterator workingTreeIter) { // Is the current index dirty? boolean indexDirty = indexIter != null && (headIter == null || !isEqualEntry(indexIter, headIter)); // Is the current working tree dirty? boolean workingTreeDirty = workingTreeIter != null && (headIter == null || !isEqualEntry(workingTreeIter, headIter)); // Would unstashing overwrite existing index changes? if (indexDirty && stashIndexIter != null && indexIter != null && !isEqualEntry(stashIndexIter, indexIter)) return true; // Would unstashing overwrite existing working tree changes? if (workingTreeDirty && stashWorkingTreeIter != null && workingTreeIter != null && !isEqualEntry(stashWorkingTreeIter, workingTreeIter)) return true; return false; } private ObjectId getHeadTree() throws GitAPIException { final ObjectId headTree; try { headTree = repo.resolve(Constants.HEAD + "^{tree}"); } catch (IOException e) { throw new JGitInternalException(JGitText.get().cannotReadTree, e); } if (headTree == null) throw new NoHeadException(JGitText.get().cannotReadTree); return headTree; } private ObjectId getStashId() throws GitAPIException { final String revision = stashRef != null ? stashRef : DEFAULT_REF; final ObjectId stashId; try { stashId = repo.resolve(revision); } catch (IOException e) { throw new InvalidRefNameException(MessageFormat.format( JGitText.get().stashResolveFailed, revision), e); } if (stashId == null) throw new InvalidRefNameException(MessageFormat.format( JGitText.get().stashResolveFailed, revision)); return stashId; } private void scanForConflicts(TreeWalk treeWalk) throws IOException { File workingTree = repo.getWorkTree(); while (treeWalk.next()) { // State of the stashed index and working directory AbstractTreeIterator stashIndexIter = treeWalk.getTree(1, AbstractTreeIterator.class); AbstractTreeIterator stashWorkingIter = treeWalk.getTree(2, AbstractTreeIterator.class); // State of the current HEAD, index, and working directory AbstractTreeIterator headIter = treeWalk.getTree(3, AbstractTreeIterator.class); AbstractTreeIterator indexIter = treeWalk.getTree(4, AbstractTreeIterator.class); AbstractTreeIterator workingIter = treeWalk.getTree(5, AbstractTreeIterator.class); if (isConflict(stashIndexIter, stashWorkingIter, headIter, indexIter, workingIter)) { String path = treeWalk.getPathString(); File file = new File(workingTree, path); throw new CheckoutConflictException(file.getAbsolutePath()); } } } private void applyChanges(TreeWalk treeWalk, DirCache cache, DirCacheEditor editor) throws IOException { File workingTree = repo.getWorkTree(); while (treeWalk.next()) { String path = treeWalk.getPathString(); File file = new File(workingTree, path); // State of the stashed HEAD, index, and working directory AbstractTreeIterator stashHeadIter = treeWalk.getTree(0, AbstractTreeIterator.class); AbstractTreeIterator stashIndexIter = treeWalk.getTree(1, AbstractTreeIterator.class); AbstractTreeIterator stashWorkingIter = treeWalk.getTree(2, AbstractTreeIterator.class); if (stashWorkingIter != null && stashIndexIter != null) { // Checkout index change DirCacheEntry entry = cache.getEntry(path); if (entry == null) entry = new DirCacheEntry(treeWalk.getRawPath()); entry.setFileMode(stashIndexIter.getEntryFileMode()); entry.setObjectId(stashIndexIter.getEntryObjectId()); DirCacheCheckout.checkoutEntry(repo, file, entry, treeWalk.getObjectReader()); final DirCacheEntry updatedEntry = entry; editor.add(new PathEdit(path) { public void apply(DirCacheEntry ent) { ent.copyMetaData(updatedEntry); } }); // Checkout working directory change if (!stashWorkingIter.idEqual(stashIndexIter)) { entry = new DirCacheEntry(treeWalk.getRawPath()); entry.setObjectId(stashWorkingIter.getEntryObjectId()); DirCacheCheckout.checkoutEntry(repo, file, entry, treeWalk.getObjectReader()); } } else { if (stashIndexIter == null || (stashHeadIter != null && !stashIndexIter .idEqual(stashHeadIter))) editor.add(new DeletePath(path)); FileUtils .delete(file, FileUtils.RETRY | FileUtils.SKIP_MISSING); } } } /** * Apply the changes in a stashed commit to the working directory and index * * @return id of stashed commit that was applied * @throws GitAPIException * @throws WrongRepositoryStateException */ public ObjectId call() throws GitAPIException, WrongRepositoryStateException { checkCallable(); if (repo.getRepositoryState() != RepositoryState.SAFE) throw new WrongRepositoryStateException(MessageFormat.format( JGitText.get().stashApplyOnUnsafeRepository, repo.getRepositoryState())); final ObjectId headTree = getHeadTree(); final ObjectId stashId = getStashId(); ObjectReader reader = repo.newObjectReader(); try { RevWalk revWalk = new RevWalk(reader); RevCommit stashCommit = revWalk.parseCommit(stashId); if (stashCommit.getParentCount() != 2) throw new JGitInternalException(MessageFormat.format( JGitText.get().stashCommitMissingTwoParents, stashId.name())); RevTree stashWorkingTree = stashCommit.getTree(); RevTree stashIndexTree = revWalk.parseCommit( stashCommit.getParent(1)).getTree(); RevTree stashHeadTree = revWalk.parseCommit( stashCommit.getParent(0)).getTree(); CanonicalTreeParser stashWorkingIter = new CanonicalTreeParser(); stashWorkingIter.reset(reader, stashWorkingTree); CanonicalTreeParser stashIndexIter = new CanonicalTreeParser(); stashIndexIter.reset(reader, stashIndexTree); CanonicalTreeParser stashHeadIter = new CanonicalTreeParser(); stashHeadIter.reset(reader, stashHeadTree); CanonicalTreeParser headIter = new CanonicalTreeParser(); headIter.reset(reader, headTree); DirCache cache = repo.lockDirCache(); DirCacheEditor editor = cache.editor(); try { DirCacheIterator indexIter = new DirCacheIterator(cache); FileTreeIterator workingIter = new FileTreeIterator(repo); TreeWalk treeWalk = new TreeWalk(reader); treeWalk.setRecursive(true); treeWalk.setFilter(new StashDiffFilter()); treeWalk.addTree(stashHeadIter); treeWalk.addTree(stashIndexIter); treeWalk.addTree(stashWorkingIter); treeWalk.addTree(headIter); treeWalk.addTree(indexIter); treeWalk.addTree(workingIter); scanForConflicts(treeWalk); // Reset trees and walk treeWalk.reset(); stashWorkingIter.reset(reader, stashWorkingTree); stashIndexIter.reset(reader, stashIndexTree); stashHeadIter.reset(reader, stashHeadTree); treeWalk.addTree(stashHeadIter); treeWalk.addTree(stashIndexIter); treeWalk.addTree(stashWorkingIter); applyChanges(treeWalk, cache, editor); } finally { editor.commit(); cache.unlock(); } } catch (JGitInternalException e) { throw e; } catch (IOException e) { throw new JGitInternalException(JGitText.get().stashApplyFailed, e); } finally { reader.release(); } return stashId; } }