diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java index 97d0efe10..bc11b7a70 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java @@ -46,10 +46,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.File; +import java.text.MessageFormat; +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.errors.CheckoutConflictException; +import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryTestCase; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.util.FileUtils; @@ -343,4 +351,106 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertEquals(1, status.getAdded().size()); assertTrue(status.getAdded().contains(addedPath)); } + + @Test + public void workingDirectoryContentConflict() throws Exception { + writeTrashFile(PATH, "content2"); + + RevCommit stashed = git.stashCreate().call(); + assertNotNull(stashed); + assertEquals("content", read(committedFile)); + assertTrue(git.status().call().isClean()); + + writeTrashFile(PATH, "content3"); + + try { + git.stashApply().call(); + fail("Exception not thrown"); + } catch (JGitInternalException e) { + assertTrue(e.getCause() instanceof CheckoutConflictException); + } + } + + @Test + public void indexContentConflict() throws Exception { + writeTrashFile(PATH, "content2"); + + RevCommit stashed = git.stashCreate().call(); + assertNotNull(stashed); + assertEquals("content", read(committedFile)); + assertTrue(git.status().call().isClean()); + + writeTrashFile(PATH, "content3"); + git.add().addFilepattern(PATH).call(); + writeTrashFile(PATH, "content2"); + + try { + git.stashApply().call(); + fail("Exception not thrown"); + } catch (JGitInternalException e) { + assertTrue(e.getCause() instanceof CheckoutConflictException); + } + } + + @Test + public void workingDirectoryEditPreCommit() throws Exception { + writeTrashFile(PATH, "content2"); + + RevCommit stashed = git.stashCreate().call(); + assertNotNull(stashed); + assertEquals("content", read(committedFile)); + assertTrue(git.status().call().isClean()); + + String path2 = "file2.txt"; + writeTrashFile(path2, "content3"); + git.add().addFilepattern(path2).call(); + assertNotNull(git.commit().setMessage("adding file").call()); + + ObjectId unstashed = git.stashApply().call(); + assertEquals(stashed, unstashed); + + Status status = git.status().call(); + assertTrue(status.getAdded().isEmpty()); + assertTrue(status.getChanged().isEmpty()); + assertTrue(status.getConflicting().isEmpty()); + assertTrue(status.getMissing().isEmpty()); + assertTrue(status.getRemoved().isEmpty()); + assertTrue(status.getUntracked().isEmpty()); + + assertEquals(1, status.getModified().size()); + assertTrue(status.getModified().contains(PATH)); + } + + @Test + public void unstashNonStashCommit() throws Exception { + try { + git.stashApply().setStashRef(head.name()).call(); + fail("Exception not thrown"); + } catch (JGitInternalException e) { + assertEquals(MessageFormat.format( + JGitText.get().stashCommitMissingTwoParents, head.name()), + e.getMessage()); + } + } + + @Test + public void unstashNoHead() throws Exception { + Repository repo = createWorkRepository(); + try { + Git.wrap(repo).stashApply().call(); + fail("Exception not thrown"); + } catch (NoHeadException e) { + assertNotNull(e.getMessage()); + } + } + + @Test + public void noStashedCommits() throws Exception { + try { + git.stashApply().call(); + fail("Exception not thrown"); + } catch (InvalidRefNameException e) { + assertNotNull(e.getMessage()); + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java index d5851a334..3876e48cd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java @@ -49,12 +49,16 @@ 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; @@ -65,6 +69,7 @@ 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; @@ -75,11 +80,45 @@ import org.eclipse.jgit.util.FileUtils; * * @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; /** @@ -105,86 +144,230 @@ public class StashApplyCommand extends GitCommand { 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; + } + /** - * Apply the changes in a stashed commit to the working directory and index + * Would unstashing overwrite local changes? * - * @return id of stashed commit that was applied + * @param stashIndexIter + * @param stashWorkingTreeIter + * @param headIter + * @param indexIter + * @param workingTreeIter + * @return true if unstash conflict, false otherwise */ - public ObjectId call() throws GitAPIException, JGitInternalException { - checkCallable(); + 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)); - if (repo.getRepositoryState() != RepositoryState.SAFE) - throw new WrongRepositoryStateException(MessageFormat.format( - JGitText.get().stashApplyOnUnsafeRepository, - repo.getRepositoryState())); + // 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 JGitInternalException, + 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 JGitInternalException, GitAPIException { final String revision = stashRef != null ? stashRef : DEFAULT_REF; final ObjectId stashId; try { stashId = repo.resolve(revision); } catch (IOException e) { - throw new JGitInternalException(JGitText.get().stashApplyFailed, 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 + */ + public ObjectId call() throws GitAPIException, JGitInternalException { + 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 wtCommit = revWalk.parseCommit(stashId); - if (wtCommit.getParentCount() != 2) + RevCommit stashCommit = revWalk.parseCommit(stashId); + if (stashCommit.getParentCount() != 2) throw new JGitInternalException(MessageFormat.format( JGitText.get().stashCommitMissingTwoParents, stashId.name())); - // Apply index changes - RevTree indexTree = revWalk.parseCommit(wtCommit.getParent(1)) - .getTree(); - DirCacheCheckout dco = new DirCacheCheckout(repo, - repo.lockDirCache(), indexTree, new FileTreeIterator(repo)); - dco.setFailOnConflict(true); - dco.checkout(); - - // Apply working directory changes - RevTree headTree = revWalk.parseCommit(wtCommit.getParent(0)) - .getTree(); + 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.addTree(headTree); - treeWalk.addTree(indexTree); - treeWalk.addTree(wtCommit.getTree()); - treeWalk.setFilter(TreeFilter.ANY_DIFF); - File workingTree = repo.getWorkTree(); - while (treeWalk.next()) { - String path = treeWalk.getPathString(); - File file = new File(workingTree, path); - AbstractTreeIterator headIter = treeWalk.getTree(0, - AbstractTreeIterator.class); - AbstractTreeIterator indexIter = treeWalk.getTree(1, - AbstractTreeIterator.class); - AbstractTreeIterator wtIter = treeWalk.getTree(2, - AbstractTreeIterator.class); - if (wtIter != null) { - DirCacheEntry entry = new DirCacheEntry( - treeWalk.getRawPath()); - entry.setObjectId(wtIter.getEntryObjectId()); - DirCacheCheckout.checkoutEntry(repo, file, entry); - } else { - if (indexIter != null && headIter != null - && !indexIter.idEqual(headIter)) - editor.add(new DeletePath(path)); - FileUtils.delete(file, FileUtils.RETRY - | FileUtils.SKIP_MISSING); - } - } + 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 {