diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java index c5829ec96..8e64776f7 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java @@ -56,6 +56,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -78,6 +79,7 @@ import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RebaseTodoLine; import org.eclipse.jgit.lib.RebaseTodoLine.Action; @@ -86,6 +88,7 @@ import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; @@ -321,6 +324,281 @@ static void assertDerivedFrom(RevCommit derived, RevCommit original) { assertEquals(original.getFullMessage(), derived.getFullMessage()); } + @Test + public void testRebasePreservingMerges1() throws Exception { + doTestRebasePreservingMerges(true); + } + + @Test + public void testRebasePreservingMerges2() throws Exception { + doTestRebasePreservingMerges(false); + } + + /** + * Transforms the same before-state as in + * {@link #testRebaseShouldIgnoreMergeCommits()} to the following. + *
+ * This test should always rewrite E. + * + *
+ * A - B (master) - - - C' - D' - F' (topic') + * \ \ / + * C - D - F (topic) - E' + * \ / + * - E (side) + *+ * + * @param testConflict + * @throws Exception + */ + private void doTestRebasePreservingMerges(boolean testConflict) + throws Exception { + RevWalk rw = new RevWalk(db); + + // create file1 on master + writeTrashFile(FILE1, FILE1); + git.add().addFilepattern(FILE1).call(); + RevCommit a = git.commit().setMessage("commit a").call(); + + // create a topic branch + createBranch(a, "refs/heads/topic"); + + // update FILE1 on master + writeTrashFile(FILE1, "blah"); + writeTrashFile("conflict", "b"); + git.add().addFilepattern(".").call(); + RevCommit b = git.commit().setMessage("commit b").call(); + + checkoutBranch("refs/heads/topic"); + writeTrashFile("file3", "more changess"); + git.add().addFilepattern("file3").call(); + RevCommit c = git.commit().setMessage("commit c").call(); + + // create a branch from the topic commit + createBranch(c, "refs/heads/side"); + + // second commit on topic + writeTrashFile("file2", "file2"); + if (testConflict) + writeTrashFile("conflict", "d"); + git.add().addFilepattern(".").call(); + RevCommit d = git.commit().setMessage("commit d").call(); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + + // switch to side branch and update file2 + checkoutBranch("refs/heads/side"); + writeTrashFile("file3", "more change"); + if (testConflict) + writeTrashFile("conflict", "e"); + git.add().addFilepattern(".").call(); + RevCommit e = git.commit().setMessage("commit e").call(); + + // switch back to topic and merge in side, creating f + checkoutBranch("refs/heads/topic"); + MergeResult result = git.merge().include(e.getId()) + .setStrategy(MergeStrategy.RESOLVE).call(); + final RevCommit f; + if (testConflict) { + assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); + assertEquals(Collections.singleton("conflict"), git.status().call() + .getConflicting()); + // resolve + writeTrashFile("conflict", "f resolved"); + git.add().addFilepattern("conflict").call(); + f = git.commit().setMessage("commit f").call(); + } else { + assertEquals(MergeStatus.MERGED, result.getMergeStatus()); + f = rw.parseCommit(result.getNewHead()); + } + + RebaseResult res = git.rebase().setUpstream("refs/heads/master") + .setPreserveMerges(true).call(); + if (testConflict) { + // first there is a conflict whhen applying d + assertEquals(Status.STOPPED, res.getStatus()); + assertEquals(Collections.singleton("conflict"), git.status().call() + .getConflicting()); + assertTrue(read("conflict").contains("\nb\n=======\nd\n")); + // resolve + writeTrashFile("conflict", "d new"); + git.add().addFilepattern("conflict").call(); + res = git.rebase().setOperation(Operation.CONTINUE).call(); + + // then there is a conflict when applying e + assertEquals(Status.STOPPED, res.getStatus()); + assertEquals(Collections.singleton("conflict"), git.status().call() + .getConflicting()); + assertTrue(read("conflict").contains("\nb\n=======\ne\n")); + // resolve + writeTrashFile("conflict", "e new"); + git.add().addFilepattern("conflict").call(); + res = git.rebase().setOperation(Operation.CONTINUE).call(); + + // finally there is a conflict merging e' + assertEquals(Status.STOPPED, res.getStatus()); + assertEquals(Collections.singleton("conflict"), git.status().call() + .getConflicting()); + assertTrue(read("conflict").contains("\nd new\n=======\ne new\n")); + // resolve + writeTrashFile("conflict", "f new resolved"); + git.add().addFilepattern("conflict").call(); + res = git.rebase().setOperation(Operation.CONTINUE).call(); + } + assertEquals(Status.OK, res.getStatus()); + + if (testConflict) + assertEquals("f new resolved", read("conflict")); + assertEquals("blah", read(FILE1)); + assertEquals("file2", read("file2")); + assertEquals("more change", read("file3")); + + rw.markStart(rw.parseCommit(db.resolve("refs/heads/topic"))); + RevCommit newF = rw.next(); + assertDerivedFrom(newF, f); + assertEquals(2, newF.getParentCount()); + RevCommit newD = rw.next(); + assertDerivedFrom(newD, d); + if (testConflict) + assertEquals("d new", readFile("conflict", newD)); + RevCommit newE = rw.next(); + assertDerivedFrom(newE, e); + if (testConflict) + assertEquals("e new", readFile("conflict", newE)); + assertEquals(newD, newF.getParent(0)); + assertEquals(newE, newF.getParent(1)); + assertDerivedFrom(rw.next(), c); + assertEquals(b, rw.next()); + assertEquals(a, rw.next()); + } + + private String readFile(String path, RevCommit commit) throws IOException { + TreeWalk walk = TreeWalk.forPath(db, path, commit.getTree()); + ObjectLoader loader = db.open(walk.getObjectId(0), Constants.OBJ_BLOB); + String result = RawParseUtils.decode(loader.getCachedBytes()); + walk.release(); + return result; + } + + @Test + public void testRebasePreservingMergesWithUnrelatedSide1() throws Exception { + doTestRebasePreservingMergesWithUnrelatedSide(true); + } + + @Test + public void testRebasePreservingMergesWithUnrelatedSide2() throws Exception { + doTestRebasePreservingMergesWithUnrelatedSide(false); + } + + /** + * Rebase topic onto master, not rewriting E. The merge resulting in D is + * confliicting to show that the manual merge resolution survives the + * rebase. + * + *
+ * A - B - G (master) + * \ \ + * \ C - D - F (topic) + * \ / + * E (side) + *+ * + *
+ * A - B - G (master) + * \ \ + * \ C' - D' - F' (topic') + * \ / + * E (side) + *+ * + * @param testConflict + * @throws Exception + */ + private void doTestRebasePreservingMergesWithUnrelatedSide( + boolean testConflict) throws Exception { + RevWalk rw = new RevWalk(db); + rw.sort(RevSort.TOPO); + + writeTrashFile(FILE1, FILE1); + git.add().addFilepattern(FILE1).call(); + RevCommit a = git.commit().setMessage("commit a").call(); + + writeTrashFile("file2", "blah"); + git.add().addFilepattern("file2").call(); + RevCommit b = git.commit().setMessage("commit b").call(); + + // create a topic branch + createBranch(b, "refs/heads/topic"); + checkoutBranch("refs/heads/topic"); + + writeTrashFile("file3", "more changess"); + writeTrashFile(FILE1, "preparing conflict"); + git.add().addFilepattern("file3").addFilepattern(FILE1).call(); + RevCommit c = git.commit().setMessage("commit c").call(); + + createBranch(a, "refs/heads/side"); + checkoutBranch("refs/heads/side"); + writeTrashFile("conflict", "e"); + writeTrashFile(FILE1, FILE1 + "\n" + "line 2"); + git.add().addFilepattern(".").call(); + RevCommit e = git.commit().setMessage("commit e").call(); + + // switch back to topic and merge in side, creating d + checkoutBranch("refs/heads/topic"); + MergeResult result = git.merge().include(e) + .setStrategy(MergeStrategy.RESOLVE).call(); + + assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); + assertEquals(result.getConflicts().keySet(), + Collections.singleton(FILE1)); + writeTrashFile(FILE1, "merge resolution"); + git.add().addFilepattern(FILE1).call(); + RevCommit d = git.commit().setMessage("commit d").call(); + + RevCommit f = commitFile("file2", "new content two", "topic"); + + checkoutBranch("refs/heads/master"); + writeTrashFile("fileg", "fileg"); + if (testConflict) + writeTrashFile("conflict", "g"); + git.add().addFilepattern(".").call(); + RevCommit g = git.commit().setMessage("commit g").call(); + + checkoutBranch("refs/heads/topic"); + RebaseResult res = git.rebase().setUpstream("refs/heads/master") + .setPreserveMerges(true).call(); + if (testConflict) { + assertEquals(Status.STOPPED, res.getStatus()); + assertEquals(Collections.singleton("conflict"), git.status().call() + .getConflicting()); + // resolve + writeTrashFile("conflict", "e"); + git.add().addFilepattern("conflict").call(); + res = git.rebase().setOperation(Operation.CONTINUE).call(); + } + assertEquals(Status.OK, res.getStatus()); + + assertEquals("merge resolution", read(FILE1)); + assertEquals("new content two", read("file2")); + assertEquals("more changess", read("file3")); + assertEquals("fileg", read("fileg")); + + rw.markStart(rw.parseCommit(db.resolve("refs/heads/topic"))); + RevCommit newF = rw.next(); + assertDerivedFrom(newF, f); + RevCommit newD = rw.next(); + assertDerivedFrom(newD, d); + assertEquals(2, newD.getParentCount()); + RevCommit newC = rw.next(); + assertDerivedFrom(newC, c); + RevCommit newE = rw.next(); + assertEquals(e, newE); + assertEquals(newC, newD.getParent(0)); + assertEquals(e, newD.getParent(1)); + assertEquals(g, rw.next()); + assertEquals(b, rw.next()); + assertEquals(a, rw.next()); + } + @Test public void testRebaseParentOntoHeadShouldBeUptoDate() throws Exception { writeTrashFile(FILE1, FILE1); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index 21d7138c9..f8406e023 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -245,7 +245,8 @@ public RevCommit call() throws GitAPIException, NoHeadException, case FORCED: case FAST_FORWARD: { setCallable(false); - if (state == RepositoryState.MERGING_RESOLVED) { + if (state == RepositoryState.MERGING_RESOLVED + || isMergeDuringRebase(state)) { // Commit was successful. Now delete the files // used for merge commits repo.writeMergeCommitMsg(null); @@ -489,7 +490,8 @@ private void processOptions(RepositoryState state, RevWalk rw) author = committer; // when doing a merge commit parse MERGE_HEAD and MERGE_MSG files - if (state == RepositoryState.MERGING_RESOLVED) { + if (state == RepositoryState.MERGING_RESOLVED + || isMergeDuringRebase(state)) { try { parents = repo.readMergeHeads(); if (parents != null) @@ -530,6 +532,19 @@ private void processOptions(RepositoryState state, RevWalk rw) throw new NoMessageException(JGitText.get().commitMessageNotSpecified); } + private boolean isMergeDuringRebase(RepositoryState state) { + if (state != RepositoryState.REBASING_INTERACTIVE + && state != RepositoryState.REBASING_MERGE) + return false; + try { + return repo.readMergeHeads() != null; + } catch (IOException e) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR, + Constants.MERGE_HEAD, e), e); + } + } + /** * @param message * the commit message used for the {@code commit} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java index 8277ead84..9ba741029 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -52,6 +52,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -77,6 +78,7 @@ import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; @@ -96,6 +98,7 @@ import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.revwalk.filter.RevFilter; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FileUtils; @@ -167,6 +170,20 @@ public class RebaseCommand extends GitCommand