Add handling of CHERRY_PICK_HEAD file in .git (similar to MERGE_HEAD), which is written in case of a conflicting cherry-pick merge. It is used so that Repository.getRepositoryState can return the new states CHERRY_PICKING and CHERRY_PICKING_RESOLVED. These states, as well as CHERRY_PICK_HEAD can be used in EGit to properly show the merge tool. Also, in case of a conflict, MERGE_MSG is written with the original commit message and a "Conflicts" section appended. This way, the cherry-picked message is not lost and can later be re-used in the commit dialog. Bug: 339092 Change-Id: I947967fdc2f1d55016c95106b104c2afcc9797a1 Signed-off-by: Robin Stocker <robin@nibor.org> Signed-off-by: Chris Aniszczyk <caniszczyk@gmail.com>tags/v0.12.1
@@ -44,14 +44,17 @@ package org.eclipse.jgit.api; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertFalse; | |||
import static org.junit.Assert.assertTrue; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.util.Iterator; | |||
import org.eclipse.jgit.api.CherryPickResult.CherryPickStatus; | |||
import org.eclipse.jgit.api.ResetCommand.ResetType; | |||
import org.eclipse.jgit.api.errors.GitAPIException; | |||
import org.eclipse.jgit.api.errors.JGitInternalException; | |||
import org.eclipse.jgit.lib.Constants; | |||
import org.eclipse.jgit.lib.RepositoryState; | |||
import org.eclipse.jgit.lib.RepositoryTestCase; | |||
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | |||
@@ -130,6 +133,55 @@ public class CherryPickCommandTest extends RepositoryTestCase { | |||
MergeFailureReason.DIRTY_WORKTREE); | |||
} | |||
@Test | |||
public void testCherryPickConflictResolution() throws Exception { | |||
Git git = new Git(db); | |||
RevCommit sideCommit = prepareCherryPick(git); | |||
CherryPickResult result = git.cherryPick().include(sideCommit.getId()) | |||
.call(); | |||
assertEquals(CherryPickStatus.CONFLICTING, result.getStatus()); | |||
assertTrue(new File(db.getDirectory(), Constants.MERGE_MSG).exists()); | |||
assertEquals("side\n\nConflicts:\n\ta\n", db.readMergeCommitMsg()); | |||
assertTrue(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD) | |||
.exists()); | |||
assertEquals(sideCommit.getId(), db.readCherryPickHead()); | |||
assertEquals(RepositoryState.CHERRY_PICKING, db.getRepositoryState()); | |||
// Resolve | |||
writeTrashFile("a", "a"); | |||
git.add().addFilepattern("a").call(); | |||
assertEquals(RepositoryState.CHERRY_PICKING_RESOLVED, | |||
db.getRepositoryState()); | |||
git.commit().setOnly("a").setMessage("resolve").call(); | |||
assertEquals(RepositoryState.SAFE, db.getRepositoryState()); | |||
} | |||
@Test | |||
public void testCherryPickConflictReset() throws Exception { | |||
Git git = new Git(db); | |||
RevCommit sideCommit = prepareCherryPick(git); | |||
CherryPickResult result = git.cherryPick().include(sideCommit.getId()) | |||
.call(); | |||
assertEquals(CherryPickStatus.CONFLICTING, result.getStatus()); | |||
assertEquals(RepositoryState.CHERRY_PICKING, db.getRepositoryState()); | |||
assertTrue(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD) | |||
.exists()); | |||
git.reset().setMode(ResetType.MIXED).setRef("HEAD").call(); | |||
assertEquals(RepositoryState.SAFE, db.getRepositoryState()); | |||
assertFalse(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD) | |||
.exists()); | |||
} | |||
private RevCommit prepareCherryPick(final Git git) throws Exception { | |||
// create, add and commit file a | |||
writeTrashFile("a", "a"); |
@@ -60,6 +60,7 @@ import org.eclipse.jgit.lib.ObjectIdRef; | |||
import org.eclipse.jgit.lib.Ref; | |||
import org.eclipse.jgit.lib.Ref.Storage; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.merge.MergeMessageFormatter; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.merge.ResolveMerger; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
@@ -150,7 +151,15 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { | |||
if (merger.failed()) | |||
return new CherryPickResult(merger.getFailingPaths()); | |||
// merge conflicts | |||
// there are merge conflicts | |||
String message = new MergeMessageFormatter() | |||
.formatWithConflicts(srcCommit.getFullMessage(), | |||
merger.getUnmergedPaths()); | |||
repo.writeCherryPickHead(srcCommit.getId()); | |||
repo.writeMergeCommitMsg(message); | |||
return CherryPickResult.CONFLICT; | |||
} | |||
} |
@@ -233,6 +233,9 @@ public class CommitCommand extends GitCommand<RevCommit> { | |||
// used for merge commits | |||
repo.writeMergeCommitMsg(null); | |||
repo.writeMergeHeads(null); | |||
} else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) { | |||
repo.writeMergeCommitMsg(null); | |||
repo.writeCherryPickHead(null); | |||
} | |||
return revCommit; | |||
} |
@@ -399,6 +399,9 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
Constants.CHARACTER_ENCODING)); | |||
createFile(rebaseDir, STOPPED_SHA, repo.newObjectReader().abbreviate( | |||
commitToPick).name()); | |||
// Remove cherry pick state file created by CherryPickCommand, it's not | |||
// needed for rebase | |||
repo.writeCherryPickHead(null); | |||
return new RebaseResult(commitToPick); | |||
} | |||
@@ -744,6 +747,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
} | |||
// cleanup the files | |||
FileUtils.delete(rebaseDir, FileUtils.RECURSIVE); | |||
repo.writeCherryPickHead(null); | |||
return result; | |||
} finally { |
@@ -129,11 +129,12 @@ public class ResetCommand extends GitCommand<Ref> { | |||
RevCommit commit; | |||
try { | |||
boolean merging = false; | |||
if (repo.getRepositoryState().equals(RepositoryState.MERGING) | |||
|| repo.getRepositoryState().equals( | |||
RepositoryState.MERGING_RESOLVED)) | |||
merging = true; | |||
RepositoryState state = repo.getRepositoryState(); | |||
final boolean merging = state.equals(RepositoryState.MERGING) | |||
|| state.equals(RepositoryState.MERGING_RESOLVED); | |||
final boolean cherryPicking = state | |||
.equals(RepositoryState.CHERRY_PICKING) | |||
|| state.equals(RepositoryState.CHERRY_PICKING_RESOLVED); | |||
// resolve the ref to a commit | |||
final ObjectId commitId; | |||
@@ -183,8 +184,12 @@ public class ResetCommand extends GitCommand<Ref> { | |||
} | |||
if (mode != ResetType.SOFT && merging) | |||
resetMerge(); | |||
if (mode != ResetType.SOFT) { | |||
if (merging) | |||
resetMerge(); | |||
else if (cherryPicking) | |||
resetCherryPick(); | |||
} | |||
setCallable(false); | |||
r = ru.getRef(); | |||
@@ -255,4 +260,9 @@ public class ResetCommand extends GitCommand<Ref> { | |||
repo.writeMergeCommitMsg(null); | |||
} | |||
private void resetCherryPick() throws IOException { | |||
repo.writeCherryPickHead(null); | |||
repo.writeMergeCommitMsg(null); | |||
} | |||
} |
@@ -536,6 +536,9 @@ public final class Constants { | |||
/** name of the file containing the IDs of the parents of a merge commit */ | |||
public static final String MERGE_HEAD = "MERGE_HEAD"; | |||
/** name of the file containing the ID of a cherry pick commit in case of conflicts */ | |||
public static final String CHERRY_PICK_HEAD = "CHERRY_PICK_HEAD"; | |||
/** | |||
* name of the ref ORIG_HEAD used by certain commands to store the original | |||
* value of HEAD |
@@ -922,7 +922,7 @@ public abstract class Repository { | |||
return RepositoryState.REBASING_MERGE; | |||
// Both versions | |||
if (new File(getDirectory(), "MERGE_HEAD").exists()) { | |||
if (new File(getDirectory(), Constants.MERGE_HEAD).exists()) { | |||
// we are merging - now check whether we have unmerged paths | |||
try { | |||
if (!readDirCache().hasUnmergedPaths()) { | |||
@@ -941,6 +941,20 @@ public abstract class Repository { | |||
if (new File(getDirectory(), "BISECT_LOG").exists()) | |||
return RepositoryState.BISECTING; | |||
if (new File(getDirectory(), Constants.CHERRY_PICK_HEAD).exists()) { | |||
try { | |||
if (!readDirCache().hasUnmergedPaths()) { | |||
// no unmerged paths | |||
return RepositoryState.CHERRY_PICKING_RESOLVED; | |||
} | |||
} catch (IOException e) { | |||
// fall through to CHERRY_PICKING | |||
e.printStackTrace(); | |||
} | |||
return RepositoryState.CHERRY_PICKING; | |||
} | |||
return RepositoryState.SAFE; | |||
} | |||
@@ -1192,4 +1206,60 @@ public abstract class Repository { | |||
FileUtils.delete(mergeHeadFile); | |||
} | |||
} | |||
/** | |||
* Return the information stored in the file $GIT_DIR/CHERRY_PICK_HEAD. | |||
* | |||
* @return object id from CHERRY_PICK_HEAD file or {@code null} if this file | |||
* doesn't exist. Also if the file exists but is empty {@code null} | |||
* will be returned | |||
* @throws IOException | |||
* @throws NoWorkTreeException | |||
* if this is bare, which implies it has no working directory. | |||
* See {@link #isBare()}. | |||
*/ | |||
public ObjectId readCherryPickHead() throws IOException, | |||
NoWorkTreeException { | |||
if (isBare() || getDirectory() == null) | |||
throw new NoWorkTreeException(); | |||
File mergeHeadFile = new File(getDirectory(), | |||
Constants.CHERRY_PICK_HEAD); | |||
byte[] raw; | |||
try { | |||
raw = IO.readFully(mergeHeadFile); | |||
} catch (FileNotFoundException notFound) { | |||
return null; | |||
} | |||
if (raw.length == 0) | |||
return null; | |||
return ObjectId.fromString(raw, 0); | |||
} | |||
/** | |||
* Write cherry pick commit into $GIT_DIR/CHERRY_PICK_HEAD. This is used in | |||
* case of conflicts to store the cherry which was tried to be picked. | |||
* | |||
* @param head | |||
* an object id of the cherry commit or <code>null</code> to | |||
* delete the file | |||
* @throws IOException | |||
*/ | |||
public void writeCherryPickHead(ObjectId head) throws IOException { | |||
File cherryPickHeadFile = new File(gitDir, Constants.CHERRY_PICK_HEAD); | |||
if (head != null) { | |||
BufferedOutputStream bos = new BufferedOutputStream( | |||
new FileOutputStream(cherryPickHeadFile)); | |||
try { | |||
head.copyTo(bos); | |||
bos.write('\n'); | |||
} finally { | |||
bos.close(); | |||
} | |||
} else { | |||
FileUtils.delete(cherryPickHeadFile, FileUtils.SKIP_MISSING); | |||
} | |||
} | |||
} |
@@ -93,6 +93,26 @@ public enum RepositoryState { | |||
public String getDescription() { return JGitText.get().repositoryState_merged; } | |||
}, | |||
/** An unfinished cherry-pick. Must resolve or reset before continuing normally | |||
*/ | |||
CHERRY_PICKING { | |||
public boolean canCheckout() { return false; } | |||
public boolean canResetHead() { return true; } | |||
public boolean canCommit() { return false; } | |||
public String getDescription() { return JGitText.get().repositoryState_conflicts; } | |||
}, | |||
/** | |||
* A cherry-pick where all conflicts have been resolved. The index does not | |||
* contain any unmerged paths. | |||
*/ | |||
CHERRY_PICKING_RESOLVED { | |||
public boolean canCheckout() { return true; } | |||
public boolean canResetHead() { return true; } | |||
public boolean canCommit() { return true; } | |||
public String getDescription() { return JGitText.get().repositoryState_merged; } | |||
}, | |||
/** | |||
* An unfinished rebase or am. Must resolve, skip or abort before normal work can take place | |||
*/ |
@@ -123,6 +123,27 @@ public class MergeMessageFormatter { | |||
return sb.toString(); | |||
} | |||
/** | |||
* Add section with conflicting paths to merge message. | |||
* | |||
* @param message | |||
* the original merge message | |||
* @param conflictingPaths | |||
* the paths with conflicts | |||
* @return merge message with conflicting paths added | |||
*/ | |||
public String formatWithConflicts(String message, | |||
List<String> conflictingPaths) { | |||
StringBuilder sb = new StringBuilder(message); | |||
if (!message.endsWith("\n")) | |||
sb.append("\n"); | |||
sb.append("\n"); | |||
sb.append("Conflicts:\n"); | |||
for (String conflictingPath : conflictingPaths) | |||
sb.append('\t').append(conflictingPath).append('\n'); | |||
return sb.toString(); | |||
} | |||
private static String joinNames(List<String> names, String singular, | |||
String plural) { | |||
if (names.size() == 1) |
@@ -130,7 +130,8 @@ public class RefDirectory extends RefDatabase { | |||
/** The names of the additional refs supported by this class */ | |||
private static final String[] additionalRefsNames = new String[] { | |||
Constants.MERGE_HEAD, Constants.FETCH_HEAD, Constants.ORIG_HEAD }; | |||
Constants.MERGE_HEAD, Constants.FETCH_HEAD, Constants.ORIG_HEAD, | |||
Constants.CHERRY_PICK_HEAD }; | |||
private final FileRepository parent; | |||