diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java index 1646c7b48..94af81e81 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java @@ -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"); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java index 1ececd23f..c8b7e89f1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java @@ -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 { 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; } } 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 963e3ed84..2412e2c6c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -233,6 +233,9 @@ public class CommitCommand extends GitCommand { // 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; } 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 cf2111ea7..7e2e67799 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -399,6 +399,9 @@ public class RebaseCommand extends GitCommand { 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 { } // cleanup the files FileUtils.delete(rebaseDir, FileUtils.RECURSIVE); + repo.writeCherryPickHead(null); return result; } finally { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java index d55767d3a..24aae22ff 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java @@ -129,11 +129,12 @@ public class ResetCommand extends GitCommand { 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 { } - 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 { repo.writeMergeCommitMsg(null); } + private void resetCherryPick() throws IOException { + repo.writeCherryPickHead(null); + repo.writeMergeCommitMsg(null); + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java index 0290689c8..1d77cc174 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -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 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index 759ab3e3a..4847a5dbf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -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 null 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); + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java index e0b5389ac..10170624b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java @@ -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 */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java index cdd7a2f37..96395d0bf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java @@ -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 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 names, String singular, String plural) { if (names.size() == 1) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java index 5a36a71ee..bba634a6b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java @@ -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;