Add CHERRY_PICK_HEAD for cherry-pick conflicts

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>
This commit is contained in:
Robin Stocker 2011-04-03 19:37:55 +02:00 committed by Chris Aniszczyk
parent fbf35fea4e
commit 6e10aa42e9
10 changed files with 203 additions and 10 deletions

View File

@ -44,14 +44,17 @@
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 void testCherryPickDirtyWorktree() throws Exception {
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");

View File

@ -60,6 +60,7 @@
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 CherryPickResult call() throws GitAPIException {
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;
}
}

View File

@ -233,6 +233,9 @@ public RevCommit call() throws NoHeadException, NoMessageException,
// 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;
}

View File

@ -399,6 +399,9 @@ private RebaseResult stop(RevCommit commitToPick) throws IOException {
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 @@ private RebaseResult abort(RebaseResult result) throws IOException {
}
// cleanup the files
FileUtils.delete(rebaseDir, FileUtils.RECURSIVE);
repo.writeCherryPickHead(null);
return result;
} finally {

View File

@ -129,11 +129,12 @@ public Ref call() throws IOException {
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 Ref call() throws IOException {
}
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 @@ private void resetMerge() throws IOException {
repo.writeMergeCommitMsg(null);
}
private void resetCherryPick() throws IOException {
repo.writeCherryPickHead(null);
repo.writeMergeCommitMsg(null);
}
}

View File

@ -536,6 +536,9 @@ public static byte[] encode(final String str) {
/** 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

View File

@ -922,7 +922,7 @@ public RepositoryState getRepositoryState() {
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 RepositoryState getRepositoryState() {
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 void writeMergeHeads(List<ObjectId> heads) throws IOException {
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);
}
}
}

View 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
*/

View File

@ -123,6 +123,27 @@ else if (ref.getName().equals(ref.getObjectId().getName()))
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)

View 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;