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:
parent
fbf35fea4e
commit
6e10aa42e9
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 @@ 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)
|
||||
|
|
|
@ -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…
Reference in New Issue