Improve handling of checkout conflicts

This converts a checkout conflict exception into a RebaseResult /
MergeResult containing the conflicting paths, which enables EGit (or
others) to handle the situation in a user-friendly way

Change-Id: I48d9bdcc1e98095576513a54a225a42409f301f3
This commit is contained in:
Markus Duft 2012-07-13 08:25:25 +02:00 committed by Gerrit Code Review @ Eclipse.org
parent 912ef3da19
commit baf7ca9cc0
4 changed files with 106 additions and 50 deletions

View File

@ -1059,7 +1059,6 @@ public void testRebaseWithUntrackedFile() throws Exception {
} }
@Test @Test
@SuppressWarnings("null")
public void testRebaseWithUnstagedTopicChange() throws Exception { public void testRebaseWithUnstagedTopicChange() throws Exception {
// create file1, add and commit // create file1, add and commit
writeTrashFile(FILE1, "file1"); writeTrashFile(FILE1, "file1");
@ -1084,19 +1083,14 @@ public void testRebaseWithUnstagedTopicChange() throws Exception {
writeTrashFile("file2", "unstaged file2"); writeTrashFile("file2", "unstaged file2");
// rebase // rebase
JGitInternalException exception = null; RebaseResult result = git.rebase().setUpstream("refs/heads/master")
try { .call();
git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.CONFLICTS, result.getStatus());
} catch (JGitInternalException e) { assertEquals(1, result.getConflicts().size());
exception = e; assertEquals("file2", result.getConflicts().get(0));
}
assertNotNull(exception);
assertEquals("Checkout conflict with files: \nfile2",
exception.getMessage());
} }
@Test @Test
@SuppressWarnings("null")
public void testRebaseWithUncommittedTopicChange() throws Exception { public void testRebaseWithUncommittedTopicChange() throws Exception {
// create file1, add and commit // create file1, add and commit
writeTrashFile(FILE1, "file1"); writeTrashFile(FILE1, "file1");
@ -1122,23 +1116,17 @@ public void testRebaseWithUncommittedTopicChange() throws Exception {
git.add().addFilepattern("file2").call(); git.add().addFilepattern("file2").call();
// do not commit // do not commit
// rebase RebaseResult result = git.rebase().setUpstream("refs/heads/master")
JGitInternalException exception = null; .call();
try { assertEquals(Status.CONFLICTS, result.getStatus());
git.rebase().setUpstream("refs/heads/master").call(); assertEquals(1, result.getConflicts().size());
} catch (JGitInternalException e) { assertEquals("file2", result.getConflicts().get(0));
exception = e;
}
assertNotNull(exception);
assertEquals("Checkout conflict with files: \nfile2",
exception.getMessage());
checkFile(uncommittedFile, "uncommitted file2"); checkFile(uncommittedFile, "uncommitted file2");
assertEquals(RepositoryState.SAFE, git.getRepository().getRepositoryState()); assertEquals(RepositoryState.SAFE, git.getRepository().getRepositoryState());
} }
@Test @Test
@SuppressWarnings("null")
public void testRebaseWithUnstagedMasterChange() throws Exception { public void testRebaseWithUnstagedMasterChange() throws Exception {
// create file1, add and commit // create file1, add and commit
writeTrashFile(FILE1, "file1"); writeTrashFile(FILE1, "file1");
@ -1163,19 +1151,14 @@ public void testRebaseWithUnstagedMasterChange() throws Exception {
writeTrashFile(FILE1, "unstaged modified file1"); writeTrashFile(FILE1, "unstaged modified file1");
// rebase // rebase
JGitInternalException exception = null; RebaseResult result = git.rebase().setUpstream("refs/heads/master")
try { .call();
git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.CONFLICTS, result.getStatus());
} catch (JGitInternalException e) { assertEquals(1, result.getConflicts().size());
exception = e; assertEquals(FILE1, result.getConflicts().get(0));
}
assertNotNull(exception);
assertEquals("Checkout conflict with files: \nfile1",
exception.getMessage());
} }
@Test @Test
@SuppressWarnings("null")
public void testRebaseWithUncommittedMasterChange() throws Exception { public void testRebaseWithUncommittedMasterChange() throws Exception {
// create file1, add and commit // create file1, add and commit
writeTrashFile(FILE1, "file1"); writeTrashFile(FILE1, "file1");
@ -1202,15 +1185,11 @@ public void testRebaseWithUncommittedMasterChange() throws Exception {
// do not commit // do not commit
// rebase // rebase
JGitInternalException exception = null; RebaseResult result = git.rebase().setUpstream("refs/heads/master")
try { .call();
git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.CONFLICTS, result.getStatus());
} catch (JGitInternalException e) { assertEquals(1, result.getConflicts().size());
exception = e; assertEquals(FILE1, result.getConflicts().get(0));
}
assertNotNull(exception);
assertEquals("Checkout conflict with files: \nfile1",
exception.getMessage());
} }
@Test @Test
@ -1496,14 +1475,13 @@ public void testRebaseShouldLeaveWorkspaceUntouchedWithUnstagedChangesConflict()
File theFile = writeTrashFile(FILE1, "dirty the file"); File theFile = writeTrashFile(FILE1, "dirty the file");
// and attempt to rebase // and attempt to rebase
try { RebaseResult rebaseResult = git.rebase()
RebaseResult rebaseResult = git.rebase()
.setUpstream("refs/heads/master").call(); .setUpstream("refs/heads/master").call();
fail("Checkout with conflict should have occured, not " assertEquals(Status.CONFLICTS, rebaseResult.getStatus());
+ rebaseResult.getStatus()); assertEquals(1, rebaseResult.getConflicts().size());
} catch (JGitInternalException e) { assertEquals(FILE1, rebaseResult.getConflicts().get(0));
checkFile(theFile, "dirty the file");
} checkFile(theFile, "dirty the file");
assertEquals(RepositoryState.SAFE, git.getRepository() assertEquals(RepositoryState.SAFE, git.getRepository()
.getRepositoryState()); .getRepositoryState());

View File

@ -45,6 +45,7 @@
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.JGitText;
@ -173,6 +174,21 @@ public String toString() {
return "Not-yet-supported"; return "Not-yet-supported";
} }
@Override
public boolean isSuccessful() {
return false;
}
},
/**
* Status representing a checkout conflict, meaning that nothing could
* be merged, as the pre-scan for the trees already failed for certain
* files (i.e. local modifications prevent checkout of files).
*/
CHECKOUT_CONFLICT {
public String toString() {
return "Checkout Conflict";
}
@Override @Override
public boolean isSuccessful() { public boolean isSuccessful() {
return false; return false;
@ -201,6 +217,8 @@ public boolean isSuccessful() {
private Map<String, MergeFailureReason> failingPaths; private Map<String, MergeFailureReason> failingPaths;
private List<String> checkoutConflicts;
/** /**
* @param newHead * @param newHead
* the object the head points at after the merge * the object the head points at after the merge
@ -294,6 +312,18 @@ public MergeResult(ObjectId newHead, ObjectId base,
addConflict(result.getKey(), result.getValue()); addConflict(result.getKey(), result.getValue());
} }
/**
* Creates a new result that represents a checkout conflict before the
* operation even started for real.
*
* @param checkoutConflicts
* the conflicting files
*/
public MergeResult(List<String> checkoutConflicts) {
this.checkoutConflicts = checkoutConflicts;
this.mergeStatus = MergeStatus.CHECKOUT_CONFLICT;
}
/** /**
* @return the object the head points at after the merge * @return the object the head points at after the merge
*/ */
@ -450,4 +480,14 @@ public Map<String, int[][]> getConflicts() {
public Map<String, MergeFailureReason> getFailingPaths() { public Map<String, MergeFailureReason> getFailingPaths() {
return failingPaths; return failingPaths;
} }
/**
* Returns a list of paths that cause a checkout conflict. These paths
* prevent the operation from even starting.
*
* @return the list of files that caused the checkout conflict.
*/
public List<String> getCheckoutConflicts() {
return checkoutConflicts;
}
} }

View File

@ -355,6 +355,8 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
return RebaseResult.OK_RESULT; return RebaseResult.OK_RESULT;
} }
return RebaseResult.FAST_FORWARD_RESULT; return RebaseResult.FAST_FORWARD_RESULT;
} catch (CheckoutConflictException cce) {
return new RebaseResult(cce.getConflictingPaths());
} catch (IOException ioe) { } catch (IOException ioe) {
throw new JGitInternalException(ioe.getMessage(), ioe); throw new JGitInternalException(ioe.getMessage(), ioe);
} }
@ -880,13 +882,18 @@ private String readFile(File directory, String fileName) throws IOException {
return RawParseUtils.decode(content, 0, end); return RawParseUtils.decode(content, 0, end);
} }
private boolean checkoutCommit(RevCommit commit) throws IOException { private boolean checkoutCommit(RevCommit commit) throws IOException,
CheckoutConflictException {
try { try {
RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD)); RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD));
DirCacheCheckout dco = new DirCacheCheckout(repo, head.getTree(), DirCacheCheckout dco = new DirCacheCheckout(repo, head.getTree(),
repo.lockDirCache(), commit.getTree()); repo.lockDirCache(), commit.getTree());
dco.setFailOnConflict(true); dco.setFailOnConflict(true);
dco.checkout(); try {
dco.checkout();
} catch (org.eclipse.jgit.errors.CheckoutConflictException cce) {
throw new CheckoutConflictException(dco.getConflicts(), cce);
}
// update the HEAD // update the HEAD
RefUpdate refUpdate = repo.updateRef(Constants.HEAD, true); RefUpdate refUpdate = repo.updateRef(Constants.HEAD, true);
refUpdate.setExpectedOldObjectId(head); refUpdate.setExpectedOldObjectId(head);

View File

@ -42,6 +42,7 @@
*/ */
package org.eclipse.jgit.api; package org.eclipse.jgit.api;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.eclipse.jgit.merge.ResolveMerger; import org.eclipse.jgit.merge.ResolveMerger;
@ -92,6 +93,15 @@ public boolean isSuccessful() {
return false; return false;
} }
}, },
/**
* Conflicts: checkout of target HEAD failed
*/
CONFLICTS {
@Override
public boolean isSuccessful() {
return false;
}
},
/** /**
* Already up-to-date * Already up-to-date
*/ */
@ -148,6 +158,8 @@ public boolean isSuccessful() {
private Map<String, MergeFailureReason> failingPaths; private Map<String, MergeFailureReason> failingPaths;
private List<String> conflicts;
private RebaseResult(Status status) { private RebaseResult(Status status) {
this.status = status; this.status = status;
currentCommit = null; currentCommit = null;
@ -176,6 +188,18 @@ private RebaseResult(Status status) {
this.failingPaths = failingPaths; this.failingPaths = failingPaths;
} }
/**
* Create <code>RebaseResult</code> with status {@link Status#CONFLICTS}
*
* @param conflicts
* the list of conflicting paths
*/
RebaseResult(List<String> conflicts) {
status = Status.CONFLICTS;
currentCommit = null;
this.conflicts = conflicts;
}
/** /**
* @return the overall status * @return the overall status
*/ */
@ -199,4 +223,11 @@ public RevCommit getCurrentCommit() {
public Map<String, MergeFailureReason> getFailingPaths() { public Map<String, MergeFailureReason> getFailingPaths() {
return failingPaths; return failingPaths;
} }
/**
* @return the list of conflicts if status is {@link Status#CONFLICTS}
*/
public List<String> getConflicts() {
return conflicts;
}
} }