Browse Source

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>
tags/v0.12.1
Robin Stocker 13 years ago
parent
commit
6e10aa42e9

+ 52
- 0
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java View 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");

+ 10
- 1
org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java View 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;
}
}

+ 3
- 0
org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java View 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;
}

+ 4
- 0
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java View 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 {

+ 17
- 7
org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java View 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);
}

}

+ 3
- 0
org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java View 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

+ 71
- 1
org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java View 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);
}
}
}

+ 20
- 0
org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java 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
*/

+ 21
- 0
org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java View 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)

+ 2
- 1
org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java 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;


Loading…
Cancel
Save