]> source.dussan.org Git - jgit.git/commitdiff
Add CHERRY_PICK_HEAD for cherry-pick conflicts 96/2996/3
authorRobin Stocker <robin@nibor.org>
Sun, 3 Apr 2011 17:37:55 +0000 (19:37 +0200)
committerChris Aniszczyk <caniszczyk@gmail.com>
Wed, 6 Apr 2011 18:28:10 +0000 (13:28 -0500)
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>
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java
org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java
org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java

index 1646c7b4814cd783dd25af450109648fecc9083b..94af81e81244e7f54b7bf140ca660960bd67f358 100644 (file)
@@ -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");
index 1ececd23f016fb16c3020f5f95977a5ac8cdca01..c8b7e89f1b00ee3646d3cddf2ea6cd63273380be 100644 (file)
@@ -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;
                                }
                        }
index 963e3ed842a81b5bfb5c5bca370c3b1b21ff9616..2412e2c6c0b51617ad02537251eb45a9326d6a9d 100644 (file)
@@ -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;
                                                }
index cf2111ea7e0f013006f524c7d77a5d7326b9b078..7e2e67799363d6db0501395de08f691d8ccd6bb8 100644 (file)
@@ -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 {
index d55767d3a6ee10d8f75261dc2bba49cbf5782e69..24aae22ffd1a8b74f68c020b83a67fa10f96810d 100644 (file)
@@ -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);
+       }
+
 }
index 0290689c819a2110081e22309201ffe1540efb6a..1d77cc174fe78a957e256ba84e44d31693904aa2 100644 (file)
@@ -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
index 759ab3e3a2d1e696a007fe77ca8646d20f1d192b..4847a5dbf5d18f8592b35b47557d51c20b9839f5 100644 (file)
@@ -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);
+               }
+       }
 }
index e0b5389ac02d92551de2934fd078a5136dd4bcf8..10170624b130a72ca204222af680304fcce5136d 100644 (file)
@@ -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
         */
index cdd7a2f371a903b8cf3bd9c163259f8582716cdb..96395d0bfa6e41cbf564da730f7f81938eee0e3f 100644 (file)
@@ -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)
index 5a36a71ee48feb8bb4759ab38018d071bc255a7c..bba634a6b646af1a1a1c3f11b0eec9f60699f12e 100644 (file)
@@ -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;