Browse Source

Rebase: Add --preserve-merges support

With --preserve-merges C Git re-does merges using the rewritten merge
parents, discarding the old merge commit. For the common use-case of
pull with rebase this is unfortunate, as it loses the merge conflict
resolution (and other fixes in the merge), which may have taken quite
some time to get right in the first place.

To overcome this we use a two-fold approach:
If any of the (non-first) merge parents of a merge were rewritten, we
also redo the merge, to include the (potential) new changes in those
commits.
If only the first parent was rewritten, i.e. we are merging a branch
that is otherwise unaffected by the rebase, we instead cherry-pick the
merge commit at hand. This is done with the --mainline 1 and --no-commit
options to apply the changes introduced by the merge. Then we set up an
appropriate MERGE_HEAD and commit the result, thus effectively forging a
merge.

Apart from the approach taken to rebase merge commits, this
implementation closely follows C Git. As a result, both Git
implementations can continue rebases of each other.

Preserving merges works for both interactive and non-interactive rebase,
but as in C Git it is easy do get undesired outcomes with interactive
rebase.

CommitCommand supports committing merges during rebase now.

Bug: 439421
Change-Id: I4cf69b9d4ec6109d130ab8e3f42fcbdac25a13b2
Signed-off-by: Konrad Kügler <swamblumat-eclipsebugs@yahoo.de>
tags/v3.5.0.201409071800-rc1
Konrad Kügler 10 years ago
parent
commit
e0fbae5dc3

+ 278
- 0
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java View File

@@ -56,6 +56,7 @@ import java.io.File;
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.AbbreviatedObjectId;
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.ReflogEntry;
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 @@ public class RebaseCommandTest extends RepositoryTestCase {
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.
* <p>
* This test should always rewrite E.
*
* <pre>
* A - B (master) - - - C' - D' - F' (topic')
* \ \ /
* C - D - F (topic) - E'
* \ /
* - E (side)
* </pre>
*
* @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.
*
* <pre>
* A - B - G (master)
* \ \
* \ C - D - F (topic)
* \ /
* E (side)
* </pre>
*
* <pre>
* A - B - G (master)
* \ \
* \ C' - D' - F' (topic')
* \ /
* E (side)
* </pre>
*
* @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);

+ 17
- 2
org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java View File

@@ -245,7 +245,8 @@ public class CommitCommand extends GitCommand<RevCommit> {
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 @@ public class CommitCommand extends GitCommand<RevCommit> {
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 @@ public class CommitCommand extends GitCommand<RevCommit> {
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}

+ 265
- 28
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java View File

@@ -52,6 +52,7 @@ import java.util.ArrayList;
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.diff.DiffFormatter;
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.lib.Repository;
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<RebaseResult> {

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 class RebaseCommand extends GitCommand<RebaseResult> {

private MergeStrategy strategy = MergeStrategy.RECURSIVE;

private boolean preserveMerges = false;

/**
* @param repo
*/
@@ -266,6 +285,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
this.upstreamCommit = walk.parseCommit(repo
.resolve(upstreamCommitId));
preserveMerges = rebaseState.getRewrittenDir().exists();
break;
case BEGIN:
autoStash();
@@ -412,6 +432,12 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
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<ObjectId> ids = or.resolve(step.getCommit());
@@ -468,19 +494,87 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
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<RevCommit> 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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
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<RevCommit> 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<RevCommit> getNewParents(RevCommit commitToPick)
throws IOException {
List<RevCommit> newParents = new ArrayList<RevCommit>();
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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
monitor.beginTask(JGitText.get().obtainingCommitsForCherryPick,
ProgressMonitor.UNKNOWN);

// determine the commits to be applied
LogCommand cmd = new Git(repo).log().addRange(upstreamCommit,
headCommit);
Iterable<RevCommit> commitsToUse = cmd.call();

List<RevCommit> cherryPickList = new ArrayList<RevCommit>();
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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
ArrayList<RebaseTodoLine> toDoSteps = new ArrayList<RebaseTodoLine>();
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<RevCommit> cherryPickList = calculatePickList(headCommit);
ObjectReader reader = walk.getObjectReader();
for (RevCommit commit : cherryPickList)
toDoSteps.add(new RebaseTodoLine(Action.PICK, reader
@@ -959,6 +1120,50 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return null;
}

private List<RevCommit> calculatePickList(RevCommit headCommit)
throws GitAPIException, NoHeadException, IOException {
LogCommand cmd = new Git(repo).log().addRange(upstreamCommit,
headCommit);
Iterable<RevCommit> commitsToUse = cmd.call();
List<RevCommit> cherryPickList = new ArrayList<RevCommit>();
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<RevCommit> 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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
// 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 class RebaseCommand extends GitCommand<RebaseResult> {
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 class RebaseCommand extends GitCommand<RebaseResult> {
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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
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();
}
}
}
}

Loading…
Cancel
Save