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 { private static final String AUTOSTASH_MSG = "On {0}: autostash"; //$NON-NLS-1$ + /** + * The folder containing the hashes of (potentially) rewritten commits when + * --preserve-merges is used. + */ + private static final String REWRITTEN = "rewritten"; //$NON-NLS-1$ + + /** + * File containing the current commit(s) to cherry pick when --preserve-merges + * is used. + */ + private static final String CURRENT_COMMIT = "current-commit"; //$NON-NLS-1$ + + private static final String REFLOG_PREFIX = "rebase:"; //$NON-NLS-1$ + /** * The available operations */ @@ -216,6 +233,8 @@ public enum Operation { private MergeStrategy strategy = MergeStrategy.RECURSIVE; + private boolean preserveMerges = false; + /** * @param repo */ @@ -266,6 +285,7 @@ public RebaseResult call() throws GitAPIException, NoHeadException, } this.upstreamCommit = walk.parseCommit(repo .resolve(upstreamCommitId)); + preserveMerges = rebaseState.getRewrittenDir().exists(); break; case BEGIN: autoStash(); @@ -412,6 +432,12 @@ private RebaseResult processStep(RebaseTodoLine step, boolean shouldPick) throws IOException, GitAPIException { if (Action.COMMENT.equals(step.getAction())) return null; + if (preserveMerges + && shouldPick + && (Action.EDIT.equals(step.getAction()) || Action.PICK + .equals(step.getAction()))) { + writeRewrittenHashes(); + } ObjectReader or = repo.newObjectReader(); Collection ids = or.resolve(step.getCommit()); @@ -468,19 +494,87 @@ private RebaseResult cherryPickCommit(RevCommit commitToPick) monitor.beginTask(MessageFormat.format( JGitText.get().applyingCommit, commitToPick.getShortMessage()), ProgressMonitor.UNKNOWN); - // if the first parent of commitToPick is the current HEAD, - // we do a fast-forward instead of cherry-pick to avoid - // unnecessary object rewriting - newHead = tryFastForward(commitToPick); - lastStepWasForward = newHead != null; - if (!lastStepWasForward) { - // TODO if the content of this commit is already merged - // here we should skip this step in order to avoid - // confusing pseudo-changed + if (preserveMerges) + return cherryPickCommitPreservingMerges(commitToPick); + else + return cherryPickCommitFlattening(commitToPick); + } finally { + monitor.endTask(); + } + } + + private RebaseResult cherryPickCommitFlattening(RevCommit commitToPick) + throws IOException, GitAPIException, NoMessageException, + UnmergedPathsException, ConcurrentRefUpdateException, + WrongRepositoryStateException, NoHeadException { + // If the first parent of commitToPick is the current HEAD, + // we do a fast-forward instead of cherry-pick to avoid + // unnecessary object rewriting + newHead = tryFastForward(commitToPick); + lastStepWasForward = newHead != null; + if (!lastStepWasForward) { + // TODO if the content of this commit is already merged + // here we should skip this step in order to avoid + // confusing pseudo-changed + String ourCommitName = getOurCommitName(); + CherryPickResult cherryPickResult = new Git(repo).cherryPick() + .include(commitToPick).setOurCommitName(ourCommitName) + .setReflogPrefix(REFLOG_PREFIX).setStrategy(strategy) + .call(); + switch (cherryPickResult.getStatus()) { + case FAILED: + if (operation == Operation.BEGIN) + return abort(RebaseResult.failed(cherryPickResult + .getFailingPaths())); + else + return stop(commitToPick, Status.STOPPED); + case CONFLICTING: + return stop(commitToPick, Status.STOPPED); + case OK: + newHead = cherryPickResult.getNewHead(); + } + } + return null; + } + + private RebaseResult cherryPickCommitPreservingMerges(RevCommit commitToPick) + throws IOException, GitAPIException, NoMessageException, + UnmergedPathsException, ConcurrentRefUpdateException, + WrongRepositoryStateException, NoHeadException { + + writeCurrentCommit(commitToPick); + + List newParents = getNewParents(commitToPick); + boolean otherParentsUnchanged = true; + for (int i = 1; i < commitToPick.getParentCount(); i++) + otherParentsUnchanged &= newParents.get(i).equals( + commitToPick.getParent(i)); + // If the first parent of commitToPick is the current HEAD, + // we do a fast-forward instead of cherry-pick to avoid + // unnecessary object rewriting + newHead = otherParentsUnchanged ? tryFastForward(commitToPick) : null; + lastStepWasForward = newHead != null; + if (!lastStepWasForward) { + ObjectId headId = getHead().getObjectId(); + if (!AnyObjectId.equals(headId, newParents.get(0))) + checkoutCommit(headId.getName(), newParents.get(0)); + + // Use the cherry-pick strategy if all non-first parents did not + // change. This is different from C Git, which always uses the merge + // strategy (see below). + if (otherParentsUnchanged) { + boolean isMerge = commitToPick.getParentCount() > 1; String ourCommitName = getOurCommitName(); - CherryPickResult cherryPickResult = new Git(repo).cherryPick() + CherryPickCommand pickCommand = new Git(repo).cherryPick() .include(commitToPick).setOurCommitName(ourCommitName) - .setReflogPrefix("rebase:").setStrategy(strategy).call(); //$NON-NLS-1$ + .setReflogPrefix(REFLOG_PREFIX).setStrategy(strategy); + if (isMerge) { + pickCommand.setMainlineParentNumber(1); + // We write a MERGE_HEAD and later commit explicitly + pickCommand.setNoCommit(true); + writeMergeInfo(commitToPick, newParents); + } + CherryPickResult cherryPickResult = pickCommand.call(); switch (cherryPickResult.getStatus()) { case FAILED: if (operation == Operation.BEGIN) @@ -491,13 +585,91 @@ private RebaseResult cherryPickCommit(RevCommit commitToPick) case CONFLICTING: return stop(commitToPick, Status.STOPPED); case OK: - newHead = cherryPickResult.getNewHead(); + if (isMerge) { + // Commit the merge (setup above using writeMergeInfo()) + CommitCommand commit = new Git(repo).commit(); + commit.setAuthor(commitToPick.getAuthorIdent()); + commit.setReflogComment(REFLOG_PREFIX + " " //$NON-NLS-1$ + + commitToPick.getShortMessage()); + newHead = commit.call(); + } else + newHead = cherryPickResult.getNewHead(); + break; + } + } else { + // Use the merge strategy to redo merges, which had some of + // their non-first parents rewritten + MergeCommand merge = new Git(repo).merge() + .setFastForward(MergeCommand.FastForwardMode.NO_FF) + .setCommit(false); + for (int i = 1; i < commitToPick.getParentCount(); i++) + merge.include(newParents.get(i)); + MergeResult mergeResult = merge.call(); + if (mergeResult.getMergeStatus().isSuccessful()) { + CommitCommand commit = new Git(repo).commit(); + commit.setAuthor(commitToPick.getAuthorIdent()); + commit.setMessage(commitToPick.getFullMessage()); + commit.setReflogComment(REFLOG_PREFIX + " " //$NON-NLS-1$ + + commitToPick.getShortMessage()); + newHead = commit.call(); + } else { + if (operation == Operation.BEGIN + && mergeResult.getMergeStatus() == MergeResult.MergeStatus.FAILED) + return abort(RebaseResult.failed(mergeResult + .getFailingPaths())); + return stop(commitToPick, Status.STOPPED); } } - return null; - } finally { - monitor.endTask(); } + return null; + } + + // Prepare MERGE_HEAD and message for the next commit + private void writeMergeInfo(RevCommit commitToPick, + List newParents) throws IOException { + repo.writeMergeHeads(newParents.subList(1, newParents.size())); + repo.writeMergeCommitMsg(commitToPick.getFullMessage()); + } + + // Get the rewritten equivalents for the parents of the given commit + private List getNewParents(RevCommit commitToPick) + throws IOException { + List newParents = new ArrayList(); + for (int p = 0; p < commitToPick.getParentCount(); p++) { + String parentHash = commitToPick.getParent(p).getName(); + if (!new File(rebaseState.getRewrittenDir(), parentHash).exists()) + newParents.add(commitToPick.getParent(p)); + else { + String newParent = RebaseState.readFile( + rebaseState.getRewrittenDir(), parentHash); + if (newParent.length() == 0) + newParents.add(walk.parseCommit(repo + .resolve(Constants.HEAD))); + else + newParents.add(walk.parseCommit(ObjectId + .fromString(newParent))); + } + } + return newParents; + } + + private void writeCurrentCommit(RevCommit commit) throws IOException { + RebaseState.appendToFile(rebaseState.getFile(CURRENT_COMMIT), + commit.name()); + } + + private void writeRewrittenHashes() throws RevisionSyntaxException, + IOException { + File currentCommitFile = rebaseState.getFile(CURRENT_COMMIT); + if (!currentCommitFile.exists()) + return; + + String head = repo.resolve(Constants.HEAD).getName(); + String currentCommits = rebaseState.readFile(CURRENT_COMMIT); + for (String current : currentCommits.split("\n")) //$NON-NLS-1$ + RebaseState + .createFile(rebaseState.getRewrittenDir(), current, head); + FileUtils.delete(currentCommitFile); } private RebaseResult finishRebase(RevCommit newHead, @@ -908,19 +1080,6 @@ else if (!isInteractive() && walk.isMergedInto(headCommit, upstream)) { monitor.beginTask(JGitText.get().obtainingCommitsForCherryPick, ProgressMonitor.UNKNOWN); - // determine the commits to be applied - LogCommand cmd = new Git(repo).log().addRange(upstreamCommit, - headCommit); - Iterable commitsToUse = cmd.call(); - - List cherryPickList = new ArrayList(); - for (RevCommit commit : commitsToUse) { - if (commit.getParentCount() != 1) - continue; - cherryPickList.add(commit); - } - - Collections.reverse(cherryPickList); // create the folder for the meta information FileUtils.mkdir(rebaseState.getDir(), true); @@ -935,6 +1094,8 @@ else if (!isInteractive() && walk.isMergedInto(headCommit, upstream)) { ArrayList toDoSteps = new ArrayList(); toDoSteps.add(new RebaseTodoLine("# Created by EGit: rebasing " + headId.name() //$NON-NLS-1$ + " onto " + upstreamCommit.name())); //$NON-NLS-1$ + // determine the commits to be applied + List cherryPickList = calculatePickList(headCommit); ObjectReader reader = walk.getObjectReader(); for (RevCommit commit : cherryPickList) toDoSteps.add(new RebaseTodoLine(Action.PICK, reader @@ -959,6 +1120,50 @@ else if (!isInteractive() && walk.isMergedInto(headCommit, upstream)) { return null; } + private List calculatePickList(RevCommit headCommit) + throws GitAPIException, NoHeadException, IOException { + LogCommand cmd = new Git(repo).log().addRange(upstreamCommit, + headCommit); + Iterable commitsToUse = cmd.call(); + List cherryPickList = new ArrayList(); + for (RevCommit commit : commitsToUse) { + if (preserveMerges || commit.getParentCount() == 1) + cherryPickList.add(commit); + } + Collections.reverse(cherryPickList); + + if (preserveMerges) { + // When preserving merges we only rewrite commits which have at + // least one parent that is itself rewritten (or a merge base) + File rewrittenDir = rebaseState.getRewrittenDir(); + FileUtils.mkdir(rewrittenDir, false); + walk.reset(); + walk.setRevFilter(RevFilter.MERGE_BASE); + walk.markStart(upstreamCommit); + walk.markStart(headCommit); + RevCommit base; + while ((base = walk.next()) != null) + RebaseState.createFile(rewrittenDir, base.getName(), + upstreamCommit.getName()); + + Iterator iterator = cherryPickList.iterator(); + pickLoop: while(iterator.hasNext()){ + RevCommit commit = iterator.next(); + for (int i = 0; i < commit.getParentCount(); i++) { + boolean parentRewritten = new File(rewrittenDir, commit + .getParent(i).getName()).exists(); + if (parentRewritten) { + new File(rewrittenDir, commit.getName()).createNewFile(); + continue pickLoop; + } + } + // commit is only merged in, needs not be rewritten + iterator.remove(); + } + } + return cherryPickList; + } + private static String getHeadName(Ref head) { String headName; if (head.isSymbolic()) @@ -1139,6 +1344,7 @@ private RebaseResult abort(RebaseResult result) throws IOException, // cleanup the files FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE); repo.writeCherryPickHead(null); + repo.writeMergeHeads(null); if (stashConflicts) return RebaseResult.STASH_APPLY_CONFLICTS_RESULT; return result; @@ -1320,6 +1526,18 @@ public RebaseCommand setStrategy(MergeStrategy strategy) { return this; } + /** + * @param preserve + * True to re-create merges during rebase. Defaults to false, a + * flattening rebase. + * @return {@code this} + * @since 3.5 + */ + public RebaseCommand setPreserveMerges(boolean preserve) { + this.preserveMerges = preserve; + return this; + } + /** * Allows configure rebase interactive process and modify commit message */ @@ -1408,6 +1626,14 @@ public File getDir() { return dir; } + /** + * @return Directory with rewritten commit hashes, usually exists if + * {@link RebaseCommand#preserveMerges} is true + **/ + public File getRewrittenDir() { + return new File(getDir(), REWRITTEN); + } + public String readFile(String name) throws IOException { return readFile(getDir(), name); } @@ -1444,5 +1670,16 @@ private static void createFile(File parentDir, String name, fos.close(); } } + + private static void appendToFile(File file, String content) + throws IOException { + FileOutputStream fos = new FileOutputStream(file, true); + try { + fos.write(content.getBytes(Constants.CHARACTER_ENCODING)); + fos.write('\n'); + } finally { + fos.close(); + } + } } }