From f86a488e32906593903acb31a93a82bed8d87915 Mon Sep 17 00:00:00 2001 From: Stefan Lay Date: Mon, 2 Dec 2013 17:24:09 +0100 Subject: [PATCH] Implement rebase.autostash This feature was introduced in native git with version 1.8.4. Bug: 422951 Change-Id: I42f194174d64d7ada6631e2156c2a7bf93b5e91c Signed-off-by: Matthias Sohn --- .../eclipse/jgit/api/RebaseCommandTest.java | 137 ++++++++++++++++++ .../org/eclipse/jgit/api/RebaseCommand.java | 114 ++++++++++++--- .../org/eclipse/jgit/api/RebaseResult.java | 15 ++ .../eclipse/jgit/api/StashApplyCommand.java | 15 +- .../eclipse/jgit/api/StashCreateCommand.java | 3 + .../org/eclipse/jgit/lib/ConfigConstants.java | 15 ++ 6 files changed, 276 insertions(+), 23 deletions(-) diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java index b33ad6ba5..a61b44eda 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java @@ -68,9 +68,14 @@ import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.api.errors.UnmergedPathsException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; @@ -82,6 +87,8 @@ import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; @@ -1567,6 +1574,136 @@ public void testRebaseWithUncommittedDelete() throws Exception { assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } + @Test + public void testRebaseWithAutoStash() + throws Exception { + // create file0, add and commit + db.getConfig().setBoolean(ConfigConstants.CONFIG_REBASE_SECTION, null, + ConfigConstants.CONFIG_KEY_AUTOSTASH, true); + writeTrashFile("file0", "file0"); + git.add().addFilepattern("file0").call(); + git.commit().setMessage("commit0").call(); + // create file1, add and commit + writeTrashFile(FILE1, "file1"); + git.add().addFilepattern(FILE1).call(); + RevCommit commit = git.commit().setMessage("commit1").call(); + + // create topic branch and checkout / create file2, add and commit + createBranch(commit, "refs/heads/topic"); + checkoutBranch("refs/heads/topic"); + writeTrashFile("file2", "file2"); + git.add().addFilepattern("file2").call(); + git.commit().setMessage("commit2").call(); + + // checkout master branch / modify file1, add and commit + checkoutBranch("refs/heads/master"); + writeTrashFile(FILE1, "modified file1"); + git.add().addFilepattern(FILE1).call(); + git.commit().setMessage("commit3").call(); + + // checkout topic branch / modify file0 + checkoutBranch("refs/heads/topic"); + writeTrashFile("file0", "unstaged modified file0"); + + // rebase + assertEquals(Status.OK, + git.rebase().setUpstream("refs/heads/master").call() + .getStatus()); + checkFile(new File(db.getWorkTree(), "file0"), + "unstaged modified file0"); + checkFile(new File(db.getWorkTree(), FILE1), "modified file1"); + checkFile(new File(db.getWorkTree(), "file2"), "file2"); + assertEquals("[file0, mode:100644, content:file0]" + + "[file1, mode:100644, content:modified file1]" + + "[file2, mode:100644, content:file2]", + indexState(CONTENT)); + assertEquals(RepositoryState.SAFE, db.getRepositoryState()); + } + + @Test + public void testRebaseWithAutoStashConflictOnApply() throws Exception { + // create file0, add and commit + db.getConfig().setBoolean(ConfigConstants.CONFIG_REBASE_SECTION, null, + ConfigConstants.CONFIG_KEY_AUTOSTASH, true); + writeTrashFile("file0", "file0"); + git.add().addFilepattern("file0").call(); + git.commit().setMessage("commit0").call(); + // create file1, add and commit + writeTrashFile(FILE1, "file1"); + git.add().addFilepattern(FILE1).call(); + RevCommit commit = git.commit().setMessage("commit1").call(); + + // create topic branch and checkout / create file2, add and commit + createBranch(commit, "refs/heads/topic"); + checkoutBranch("refs/heads/topic"); + writeTrashFile("file2", "file2"); + git.add().addFilepattern("file2").call(); + git.commit().setMessage("commit2").call(); + + // checkout master branch / modify file1, add and commit + checkoutBranch("refs/heads/master"); + writeTrashFile(FILE1, "modified file1"); + git.add().addFilepattern(FILE1).call(); + git.commit().setMessage("commit3").call(); + + // checkout topic branch / modify file0 + checkoutBranch("refs/heads/topic"); + writeTrashFile("file1", "unstaged modified file1"); + + // rebase + assertEquals(Status.STASH_APPLY_CONFLICTS, + git.rebase().setUpstream("refs/heads/master").call() + .getStatus()); + checkFile(new File(db.getWorkTree(), "file0"), "file0"); + checkFile( + new File(db.getWorkTree(), FILE1), + "<<<<<<< HEAD\nmodified file1\n=======\nunstaged modified file1\n>>>>>>> stash\n"); + checkFile(new File(db.getWorkTree(), "file2"), "file2"); + assertEquals( + "[file0, mode:100644, content:file0]" + + "[file1, mode:100644, stage:1, content:file1]" + + "[file1, mode:100644, stage:2, content:modified file1]" + + "[file1, mode:100644, stage:3, content:unstaged modified file1]" + + "[file2, mode:100644, content:file2]", + indexState(CONTENT)); + assertEquals(RepositoryState.SAFE, db.getRepositoryState()); + + List diffs = getStashedDiff(); + assertEquals(1, diffs.size()); + assertEquals(DiffEntry.ChangeType.MODIFY, diffs.get(0).getChangeType()); + assertEquals("file1", diffs.get(0).getOldPath()); + } + + private List getStashedDiff() throws AmbiguousObjectException, + IncorrectObjectTypeException, IOException, MissingObjectException { + ObjectId stashId = db.resolve("stash@{0}"); + RevWalk revWalk = new RevWalk(db); + RevCommit stashCommit = revWalk.parseCommit(stashId); + List diffs = diffWorkingAgainstHead(stashCommit, revWalk); + return diffs; + } + + private TreeWalk createTreeWalk() { + TreeWalk walk = new TreeWalk(db); + walk.setRecursive(true); + walk.setFilter(TreeFilter.ANY_DIFF); + return walk; + } + + private List diffWorkingAgainstHead(final RevCommit commit, + RevWalk revWalk) + throws IOException { + TreeWalk walk = createTreeWalk(); + RevCommit parentCommit = revWalk.parseCommit(commit.getParent(0)); + try { + walk.addTree(parentCommit.getTree()); + walk.addTree(commit.getTree()); + return DiffEntry.scan(walk); + } finally { + walk.release(); + } + } + private int countPicks() throws IOException { int count = 0; File todoFile = getTodoFile(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java index 55cf001c6..10b273a74 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -70,6 +70,7 @@ import org.eclipse.jgit.api.errors.NoMessageException; import org.eclipse.jgit.api.errors.RefAlreadyExistsException; import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.api.errors.StashApplyFailureException; import org.eclipse.jgit.api.errors.UnmergedPathsException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.diff.DiffFormatter; @@ -79,6 +80,7 @@ import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; @@ -158,6 +160,10 @@ public class RebaseCommand extends GitCommand { private static final String MESSAGE_SQUASH = "message-squash"; //$NON-NLS-1$ + private static final String AUTOSTASH = "autostash"; //$NON-NLS-1$ + + private static final String AUTOSTASH_MSG = "On {0}: autostash"; + /** * The available operations */ @@ -257,6 +263,7 @@ public RebaseResult call() throws GitAPIException, NoHeadException, .resolve(upstreamCommitId)); break; case BEGIN: + autoStash(); if (stopAfterInitialization || !walk.isMergedInto( walk.parseCommit(repo.resolve(Constants.HEAD)), @@ -272,8 +279,10 @@ public RebaseResult call() throws GitAPIException, NoHeadException, RebaseResult res = initFilesAndRewind(); if (stopAfterInitialization) return RebaseResult.INTERACTIVE_PREPARED_RESULT; - if (res != null) + if (res != null) { + autoStashApply(); return res; + } } if (monitor.isCancelled()) @@ -339,6 +348,57 @@ public RebaseResult call() throws GitAPIException, NoHeadException, } } + private void autoStash() throws GitAPIException, IOException { + if (repo.getConfig().getBoolean(ConfigConstants.CONFIG_REBASE_SECTION, + ConfigConstants.CONFIG_KEY_AUTOSTASH, false)) { + String message = MessageFormat.format( + AUTOSTASH_MSG, + Repository + .shortenRefName(getHeadName(getHead()))); + RevCommit stashCommit = Git.wrap(repo).stashCreate().setRef(null) + .setWorkingDirectoryMessage( + message) + .call(); + if (stashCommit != null) { + FileUtils.mkdir(rebaseState.getDir()); + rebaseState.createFile(AUTOSTASH, stashCommit.getName()); + } + } + } + + private boolean autoStashApply() throws IOException, GitAPIException { + boolean conflicts = false; + if (rebaseState.getFile(AUTOSTASH).exists()) { + String stash = rebaseState.readFile(AUTOSTASH); + try { + Git.wrap(repo).stashApply().setStashRef(stash) + .ignoreRepositoryState(true).call(); + } catch (StashApplyFailureException e) { + conflicts = true; + RevWalk rw = new RevWalk(repo); + ObjectId stashId = repo.resolve(stash); + RevCommit commit = rw.parseCommit(stashId); + updateStashRef(commit, commit.getAuthorIdent(), + commit.getShortMessage()); + } + } + return conflicts; + } + + private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent, + String refLogMessage) throws IOException { + Ref currentRef = repo.getRef(Constants.R_STASH); + RefUpdate refUpdate = repo.updateRef(Constants.R_STASH); + refUpdate.setNewObjectId(commitId); + refUpdate.setRefLogIdent(refLogIdent); + refUpdate.setRefLogMessage(refLogMessage, false); + if (currentRef != null) + refUpdate.setExpectedOldObjectId(currentRef.getObjectId()); + else + refUpdate.setExpectedOldObjectId(ObjectId.zeroId()); + refUpdate.forceUpdate(); + } + private RebaseResult processStep(RebaseTodoLine step, boolean shouldPick) throws IOException, GitAPIException { if (Action.COMMENT.equals(step.getAction())) @@ -432,10 +492,13 @@ private RebaseResult cherryPickCommit(RevCommit commitToPick) } private RebaseResult finishRebase(RevCommit newHead, - boolean lastStepWasForward) throws IOException { + boolean lastStepWasForward) throws IOException, GitAPIException { String headName = rebaseState.readFile(HEAD_NAME); updateHead(headName, newHead, upstreamCommit); + boolean stashConflicts = autoStashApply(); FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE); + if (stashConflicts) + return RebaseResult.STASH_APPLY_CONFLICTS_RESULT; if (lastStepWasForward || newHead == null) return RebaseResult.FAST_FORWARD_RESULT; return RebaseResult.OK_RESULT; @@ -809,16 +872,9 @@ private RebaseResult initFilesAndRewind() throws IOException, // we need to store everything into files so that we can implement // --skip, --continue, and --abort - Ref head = repo.getRef(Constants.HEAD); - if (head == null || head.getObjectId() == null) - throw new RefNotFoundException(MessageFormat.format( - JGitText.get().refNotResolved, Constants.HEAD)); + Ref head = getHead(); - String headName; - if (head.isSymbolic()) - headName = head.getTarget().getName(); - else - headName = head.getObjectId().getName(); + String headName = getHeadName(head); ObjectId headId = head.getObjectId(); if (headId == null) throw new RefNotFoundException(MessageFormat.format( @@ -857,7 +913,7 @@ else if (!isInteractive() && walk.isMergedInto(headCommit, upstream)) { Collections.reverse(cherryPickList); // create the folder for the meta information - FileUtils.mkdir(rebaseState.getDir()); + FileUtils.mkdir(rebaseState.getDir(), true); repo.writeOrigHead(headId); rebaseState.createFile(REBASE_HEAD, headId.name()); @@ -893,6 +949,23 @@ else if (!isInteractive() && walk.isMergedInto(headCommit, upstream)) { return null; } + private static String getHeadName(Ref head) { + String headName; + if (head.isSymbolic()) + headName = head.getTarget().getName(); + else + headName = head.getObjectId().getName(); + return headName; + } + + private Ref getHead() throws IOException, RefNotFoundException { + Ref head = repo.getRef(Constants.HEAD); + if (head == null || head.getObjectId() == null) + throw new RefNotFoundException(MessageFormat.format( + JGitText.get().refNotResolved, Constants.HEAD)); + return head; + } + private boolean isInteractive() { return interactiveHandler != null; } @@ -907,10 +980,7 @@ private boolean isInteractive() { */ public RevCommit tryFastForward(RevCommit newCommit) throws IOException, GitAPIException { - Ref head = repo.getRef(Constants.HEAD); - if (head == null || head.getObjectId() == null) - throw new RefNotFoundException(MessageFormat.format( - JGitText.get().refNotResolved, Constants.HEAD)); + Ref head = getHead(); ObjectId headId = head.getObjectId(); if (headId == null) @@ -920,11 +990,7 @@ public RevCommit tryFastForward(RevCommit newCommit) throws IOException, if (walk.isMergedInto(newCommit, headCommit)) return newCommit; - String headName; - if (head.isSymbolic()) - headName = head.getTarget().getName(); - else - headName = head.getObjectId().getName(); + String headName = getHeadName(head); return tryFastForward(headName, headCommit, newCommit); } @@ -1004,7 +1070,8 @@ private void checkParameters() throws WrongRepositoryStateException { } } - private RebaseResult abort(RebaseResult result) throws IOException { + private RebaseResult abort(RebaseResult result) throws IOException, + GitAPIException { try { ObjectId origHead = repo.readOrigHead(); String commitId = origHead != null ? origHead.name() : null; @@ -1053,9 +1120,12 @@ private RebaseResult abort(RebaseResult result) throws IOException { JGitText.get().abortingRebaseFailed); } } + boolean stashConflicts = autoStashApply(); // cleanup the files FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE); repo.writeCherryPickHead(null); + if (stashConflicts) + return RebaseResult.STASH_APPLY_CONFLICTS_RESULT; return result; } finally { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java index 26d040342..92c1347ab 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java @@ -165,6 +165,18 @@ public boolean isSuccessful() { public boolean isSuccessful() { return false; } + }, + + /** + * Applying stash resulted in conflicts + * + * @since 3.2 + */ + STASH_APPLY_CONFLICTS { + @Override + public boolean isSuccessful() { + return true; + } }; /** @@ -189,6 +201,9 @@ public boolean isSuccessful() { static final RebaseResult INTERACTIVE_PREPARED_RESULT = new RebaseResult( Status.INTERACTIVE_PREPARED); + static final RebaseResult STASH_APPLY_CONFLICTS_RESULT = new RebaseResult( + Status.STASH_APPLY_CONFLICTS); + private final Status status; private final RevCommit currentCommit; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java index 73d64529b..8440d8b95 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java @@ -90,6 +90,8 @@ public class StashApplyCommand extends GitCommand { private boolean applyIndex = true; + private boolean ignoreRepositoryState; + /** * Create command to apply the changes of a stashed commit * @@ -113,6 +115,16 @@ public StashApplyCommand setStashRef(final String stashRef) { return this; } + /** + * @param ignoreRepositoryState + * @return {@code this} + * @since 3.2 + */ + public StashApplyCommand ignoreRepositoryState(boolean ignoreRepositoryState) { + this.ignoreRepositoryState = ignoreRepositoryState; + return this; + } + private ObjectId getStashId() throws GitAPIException { final String revision = stashRef != null ? stashRef : DEFAULT_REF; final ObjectId stashId; @@ -143,7 +155,8 @@ public ObjectId call() throws GitAPIException, StashApplyFailureException { checkCallable(); - if (repo.getRepositoryState() != RepositoryState.SAFE) + if (!ignoreRepositoryState + && repo.getRepositoryState() != RepositoryState.SAFE) throw new WrongRepositoryStateException(MessageFormat.format( JGitText.get().stashApplyOnUnsafeRepository, repo.getRepositoryState())); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java index fc21b919b..cf0b6d1d9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java @@ -154,6 +154,7 @@ public StashCreateCommand setPerson(PersonIdent person) { /** * Set the reference to update with the stashed commit id + * If null, no reference is updated *

* This value defaults to {@link Constants#R_STASH} * @@ -185,6 +186,8 @@ private CommitBuilder createBuilder(ObjectId headId) { private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent, String refLogMessage) throws IOException { + if (ref == null) + return; Ref currentRef = repo.getRef(ref); RefUpdate refUpdate = repo.updateRef(ref); refUpdate.setNewObjectId(commitId); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 3ff4eefb1..fd22764b6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -77,6 +77,13 @@ public class ConfigConstants { /** The "submodule" section */ public static final String CONFIG_SUBMODULE_SECTION = "submodule"; + /** + * The "rebase" section + * + * @since 3.2 + */ + public static final String CONFIG_REBASE_SECTION = "rebase"; + /** The "gc" section */ public static final String CONFIG_GC_SECTION = "gc"; @@ -136,6 +143,14 @@ public class ConfigConstants { /** The "autosetuprebase" key */ public static final String CONFIG_KEY_AUTOSETUPREBASE = "autosetuprebase"; + + /** + * The "autostash" key + * + * @since 3.2 + */ + public static final String CONFIG_KEY_AUTOSTASH = "autostash"; + /** The "name" key */ public static final String CONFIG_KEY_NAME = "name";