import java.io.IOException;
import java.util.Iterator;
+import org.eclipse.jgit.api.MergeResult.MergeStatus;
+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.dircache.DirCache;
+import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.lib.RepositoryTestCase;
+import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.storage.file.ReflogReader;
import org.junit.Test;
.startsWith("revert: Revert \""));
}
+
+ @Test
+ public void testRevertDirtyIndex() throws Exception {
+ Git git = new Git(db);
+ RevCommit sideCommit = prepareRevert(git);
+
+ // modify and add file a
+ writeTrashFile("a", "a(modified)");
+ git.add().addFilepattern("a").call();
+ // do not commit
+
+ doRevertAndCheckResult(git, sideCommit,
+ MergeFailureReason.DIRTY_INDEX);
+ }
+
+ @Test
+ public void testRevertDirtyWorktree() throws Exception {
+ Git git = new Git(db);
+ RevCommit sideCommit = prepareRevert(git);
+
+ // modify file a
+ writeTrashFile("a", "a(modified)");
+ // do not add and commit
+
+ doRevertAndCheckResult(git, sideCommit,
+ MergeFailureReason.DIRTY_WORKTREE);
+ }
+
+ @Test
+ public void testRevertConflictResolution() throws Exception {
+ Git git = new Git(db);
+ RevCommit sideCommit = prepareRevert(git);
+
+ RevertCommand revert = git.revert();
+ RevCommit newHead = revert.include(sideCommit.getId()).call();
+ assertNull(newHead);
+ MergeResult result = revert.getFailingResult();
+ assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+ assertTrue(new File(db.getDirectory(), Constants.MERGE_MSG).exists());
+ assertEquals("Revert \"" + sideCommit.getShortMessage()
+ + "\"\n\nThis reverts commit " + sideCommit.getId().getName()
+ + ".\n\nConflicts:\n\ta\n",
+ db.readMergeCommitMsg());
+ assertTrue(new File(db.getDirectory(), Constants.REVERT_HEAD)
+ .exists());
+ assertEquals(sideCommit.getId(), db.readRevertHead());
+ assertEquals(RepositoryState.REVERTING, db.getRepositoryState());
+
+ // Resolve
+ writeTrashFile("a", "a");
+ git.add().addFilepattern("a").call();
+
+ assertEquals(RepositoryState.REVERTING_RESOLVED,
+ db.getRepositoryState());
+
+ git.commit().setOnly("a").setMessage("resolve").call();
+
+ assertEquals(RepositoryState.SAFE, db.getRepositoryState());
+ }
+
+ @Test
+ public void testRevertkConflictReset() throws Exception {
+ Git git = new Git(db);
+
+ RevCommit sideCommit = prepareRevert(git);
+
+ RevertCommand revert = git.revert();
+ RevCommit newHead = revert.include(sideCommit.getId()).call();
+ assertNull(newHead);
+ MergeResult result = revert.getFailingResult();
+
+ assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+ assertEquals(RepositoryState.REVERTING, db.getRepositoryState());
+ assertTrue(new File(db.getDirectory(), Constants.REVERT_HEAD)
+ .exists());
+
+ git.reset().setMode(ResetType.MIXED).setRef("HEAD").call();
+
+ assertEquals(RepositoryState.SAFE, db.getRepositoryState());
+ assertFalse(new File(db.getDirectory(), Constants.REVERT_HEAD)
+ .exists());
+ }
+
+ @Test
+ public void testRevertOverExecutableChangeOnNonExectuableFileSystem()
+ throws Exception {
+ Git git = new Git(db);
+ File file = writeTrashFile("test.txt", "a");
+ assertNotNull(git.add().addFilepattern("test.txt").call());
+ assertNotNull(git.commit().setMessage("commit1").call());
+
+ assertNotNull(git.checkout().setCreateBranch(true).setName("a").call());
+
+ writeTrashFile("test.txt", "b");
+ assertNotNull(git.add().addFilepattern("test.txt").call());
+ RevCommit commit2 = git.commit().setMessage("commit2").call();
+ assertNotNull(commit2);
+
+ assertNotNull(git.checkout().setName(Constants.MASTER).call());
+
+ DirCache cache = db.lockDirCache();
+ cache.getEntry("test.txt").setFileMode(FileMode.EXECUTABLE_FILE);
+ cache.write();
+ assertTrue(cache.commit());
+ cache.unlock();
+
+ assertNotNull(git.commit().setMessage("commit3").call());
+
+ db.getFS().setExecute(file, false);
+ git.getRepository()
+ .getConfig()
+ .setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_FILEMODE, false);
+
+ RevertCommand revert = git.revert();
+ RevCommit newHead = revert.include(commit2).call();
+ assertNotNull(newHead);
+ }
+
+ @Test
+ public void testRevertConflictMarkers() throws Exception {
+ Git git = new Git(db);
+ RevCommit sideCommit = prepareRevert(git);
+
+ RevertCommand revert = git.revert();
+ RevCommit newHead = revert.include(sideCommit.getId())
+ .call();
+ assertNull(newHead);
+ MergeResult result = revert.getFailingResult();
+ assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+
+ String expected = "<<<<<<< master\na(latest)\n=======\na\n>>>>>>> ca96c31 second master\n";
+ checkFile(new File(db.getWorkTree(), "a"), expected);
+ }
+
+ @Test
+ public void testRevertOurCommitName() throws Exception {
+ Git git = new Git(db);
+ RevCommit sideCommit = prepareRevert(git);
+
+ RevertCommand revert = git.revert();
+ RevCommit newHead = revert.include(sideCommit.getId())
+ .setOurCommitName("custom name").call();
+ assertNull(newHead);
+ MergeResult result = revert.getFailingResult();
+ assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+
+ String expected = "<<<<<<< custom name\na(latest)\n=======\na\n>>>>>>> ca96c31 second master\n";
+ checkFile(new File(db.getWorkTree(), "a"), expected);
+ }
+
+ private RevCommit prepareRevert(final Git git) throws Exception {
+ // create, add and commit file a
+ writeTrashFile("a", "a");
+ git.add().addFilepattern("a").call();
+ git.commit().setMessage("first master").call();
+
+ // First commit
+ checkoutBranch("refs/heads/master");
+ // modify, add and commit file a
+ writeTrashFile("a", "a(previous)");
+ git.add().addFilepattern("a").call();
+ RevCommit oldCommit = git.commit().setMessage("second master").call();
+
+ // modify, add and commit file a
+ writeTrashFile("a", "a(latest)");
+ git.add().addFilepattern("a").call();
+ git.commit().setMessage("side").call();
+
+ return oldCommit;
+ }
+
+ private void doRevertAndCheckResult(final Git git,
+ final RevCommit sideCommit, final MergeFailureReason reason)
+ throws Exception {
+ // get current index state
+ String indexState = indexState(CONTENT);
+
+ // revert
+ RevertCommand revert = git.revert();
+ RevCommit resultCommit = revert.include(sideCommit.getId()).call();
+ assertNull(resultCommit);
+ MergeResult result = revert.getFailingResult();
+ assertEquals(MergeStatus.FAILED, result.getMergeStatus());
+ // staged file a causes DIRTY_INDEX
+ assertEquals(1, result.getFailingPaths().size());
+ assertEquals(reason, result.getFailingPaths().get("a"));
+ assertEquals("a(modified)", read(new File(db.getWorkTree(), "a")));
+ // index shall be unchanged
+ assertEquals(indexState, indexState(CONTENT));
+ assertEquals(RepositoryState.SAFE, db.getRepositoryState());
+
+ if (reason == null) {
+ ReflogReader reader = db.getReflogReader(Constants.HEAD);
+ assertTrue(reader.getLastEntry().getComment()
+ .startsWith("revert: "));
+ reader = db.getReflogReader(db.getBranch());
+ assertTrue(reader.getLastEntry().getComment()
+ .startsWith("revert: "));
+ }
+ }
}
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.merge.ResolveMerger.MergeFailureReason;
public class RevertCommand extends GitCommand<RevCommit> {
private List<Ref> commits = new LinkedList<Ref>();
+ private String ourCommitName = null;
+
private List<Ref> revertedRefs = new LinkedList<Ref>();
private MergeResult failingResult;
RevCommit srcCommit = revWalk.parseCommit(srcObjectId);
// get the parent of the commit to revert
- if (srcCommit.getParentCount() != 1) {
+ if (srcCommit.getParentCount() != 1)
throw new MultipleParentsNotAllowedException(
- JGitText.get().canOnlyRevertCommitsWithOneParent);
- }
+ MessageFormat.format(
+ JGitText.get().canOnlyRevertCommitsWithOneParent,
+ srcCommit.name(),
+ Integer.valueOf(srcCommit.getParentCount())));
+
RevCommit srcParent = srcCommit.getParent(0);
revWalk.parseHeaders(srcParent);
+ String ourName = calculateOurName(headRef);
+ String revertName = srcCommit.getId().abbreviate(7).name()
+ + " " + srcCommit.getShortMessage(); //$NON-NLS-1$
+
ResolveMerger merger = (ResolveMerger) MergeStrategy.RESOLVE
.newMerger(repo);
merger.setWorkingTreeIterator(new FileTreeIterator(repo));
merger.setBase(srcCommit.getTree());
+ merger.setCommitNames(new String[] { "BASE", ourName, revertName }); //$NON-NLS-1$ //$NON-NLS-2$
+ String shortMessage = "Revert \"" + srcCommit.getShortMessage() //$NON-NLS-1$
+ + "\""; //$NON-NLS-1$
+ String newMessage = shortMessage + "\n\n"
+ + "This reverts commit " + srcCommit.getId().getName() //$NON-NLS-1$
+ + ".\n"; //$NON-NLS-1$
if (merger.merge(headCommit, srcParent)) {
if (AnyObjectId.equals(headCommit.getTree().getId(), merger
.getResultTreeId()))
merger.getResultTreeId());
dco.setFailOnConflict(true);
dco.checkout();
- String shortMessage = "Revert \"" + srcCommit.getShortMessage() + "\""; //$NON-NLS-2$
- String newMessage = shortMessage + "\n\n" //$NON-NLS-1$
- + "This reverts commit " //$NON-NLS-1$
- + srcCommit.getId().getName() + ".\n"; //$NON-NLS-1$
newHead = new Git(getRepository()).commit()
.setMessage(newMessage)
.setReflogComment("revert: " + shortMessage).call(); //$NON-NLS-1$
srcParent.getId() },
MergeStatus.FAILED, MergeStrategy.RESOLVE,
merger.getMergeResults(), failingPaths, null);
+ else
+ failingResult = new MergeResult(null,
+ merger.getBaseCommit(0, 1),
+ new ObjectId[] { headCommit.getId(),
+ srcParent.getId() },
+ MergeStatus.CONFLICTING, MergeStrategy.RESOLVE,
+ merger.getMergeResults(), failingPaths, null);
+ if (!merger.failed() && !unmergedPaths.isEmpty()) {
+ String message = new MergeMessageFormatter()
+ .formatWithConflicts(newMessage,
+ merger.getUnmergedPaths());
+ repo.writeRevertHead(srcCommit.getId());
+ repo.writeMergeCommitMsg(message);
+ }
return null;
}
}
commit.copy()));
}
+ /**
+ * @param ourCommitName
+ * the name that should be used in the "OURS" place for conflict
+ * markers
+ * @return {@code this}
+ */
+ public RevertCommand setOurCommitName(String ourCommitName) {
+ this.ourCommitName = ourCommitName;
+ return this;
+ }
+
+ private String calculateOurName(Ref headRef) {
+ if (ourCommitName != null)
+ return ourCommitName;
+
+ String targetRefName = headRef.getTarget().getName();
+ String headName = Repository.shortenRefName(targetRefName);
+ return headName;
+ }
+
/**
* @return the list of successfully reverted {@link Ref}'s. Never
* <code>null</code> but maybe an empty list if no commit was
return RepositoryState.CHERRY_PICKING;
}
+ if (new File(getDirectory(), Constants.REVERT_HEAD).exists()) {
+ try {
+ if (!readDirCache().hasUnmergedPaths()) {
+ // no unmerged paths
+ return RepositoryState.REVERTING_RESOLVED;
+ }
+ } catch (IOException e) {
+ // fall through to REVERTING
+ }
+
+ return RepositoryState.REVERTING;
+ }
+
return RepositoryState.SAFE;
}
return ObjectId.fromString(raw, 0);
}
+ /**
+ * Return the information stored in the file $GIT_DIR/REVERT_HEAD.
+ *
+ * @return object id from REVERT_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 readRevertHead() throws IOException, NoWorkTreeException {
+ if (isBare() || getDirectory() == null)
+ throw new NoWorkTreeException();
+
+ byte[] raw = readGitDirectoryFile(Constants.REVERT_HEAD);
+ if (raw == null)
+ 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.
writeHeadsFile(heads, Constants.CHERRY_PICK_HEAD);
}
+ /**
+ * Write revert commit into $GIT_DIR/REVERT_HEAD. This is used in case of
+ * conflicts to store the revert which was tried to be picked.
+ *
+ * @param head
+ * an object id of the revert commit or <code>null</code> to
+ * delete the file
+ * @throws IOException
+ */
+ public void writeRevertHead(ObjectId head) throws IOException {
+ List<ObjectId> heads = (head != null) ? Collections.singletonList(head)
+ : null;
+ writeHeadsFile(heads, Constants.REVERT_HEAD);
+ }
+
/**
* Write original HEAD commit into $GIT_DIR/ORIG_HEAD.
*
FileUtils.delete(headsFile, FileUtils.SKIP_MISSING);
}
}
+
}