]> source.dussan.org Git - jgit.git/commitdiff
Implement rebase.autostash 09/19209/4
authorStefan Lay <stefan.lay@sap.com>
Mon, 2 Dec 2013 16:24:09 +0000 (17:24 +0100)
committerMatthias Sohn <matthias.sohn@sap.com>
Wed, 4 Dec 2013 21:02:22 +0000 (22:02 +0100)
This feature was introduced in native git with version 1.8.4.

Bug: 422951
Change-Id: I42f194174d64d7ada6631e2156c2a7bf93b5e91c
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java
org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java

index b33ad6ba5b85794c99f9c8593172db6426999007..a61b44eda8d4d0df12b649a965c75de46861e1f9 100644 (file)
@@ -68,9 +68,14 @@ import org.eclipse.jgit.api.errors.JGitInternalException;
 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.lib.RepositoryState;
 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 class RebaseCommandTest extends RepositoryTestCase {
                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<DiffEntry> diffs = getStashedDiff();
+               assertEquals(1, diffs.size());
+               assertEquals(DiffEntry.ChangeType.MODIFY, diffs.get(0).getChangeType());
+               assertEquals("file1", diffs.get(0).getOldPath());
+       }
+
+       private List<DiffEntry> getStashedDiff() throws AmbiguousObjectException,
+                       IncorrectObjectTypeException, IOException, MissingObjectException {
+               ObjectId stashId = db.resolve("stash@{0}");
+               RevWalk revWalk = new RevWalk(db);
+               RevCommit stashCommit = revWalk.parseCommit(stashId);
+               List<DiffEntry> 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<DiffEntry> 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();
index 55cf001c6c483f89f132f2c897eafb27edae80c2..10b273a7447602ebcc728b52f5264c8616970070 100644 (file)
@@ -70,6 +70,7 @@ import org.eclipse.jgit.api.errors.NoHeadException;
 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.dircache.DirCacheIterator;
 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<RebaseResult> {
 
        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 class RebaseCommand extends GitCommand<RebaseResult> {
                                                .resolve(upstreamCommitId));
                                break;
                        case BEGIN:
+                               autoStash();
                                if (stopAfterInitialization
                                                || !walk.isMergedInto(
                                                                walk.parseCommit(repo.resolve(Constants.HEAD)),
@@ -272,8 +279,10 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
                                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 class RebaseCommand extends GitCommand<RebaseResult> {
                }
        }
 
+       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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
        }
 
        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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
                // 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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
 
                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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
                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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
         */
        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 class RebaseCommand extends GitCommand<RebaseResult> {
                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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
                        }
        }
 
-       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 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
                                                        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 {
index 26d040342e82d2bb55df51663f893fdd9df07696..92c1347ab254b68767769e10d56611d0358aa9de 100644 (file)
@@ -165,6 +165,18 @@ public class RebaseResult {
                        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 class RebaseResult {
        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;
index 73d64529b30409307b931b780e4178ae13851deb..8440d8b9505318ee3fb373615bfbc19ec5713db2 100644 (file)
@@ -90,6 +90,8 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
 
        private boolean applyIndex = true;
 
+       private boolean ignoreRepositoryState;
+
        /**
         * Create command to apply the changes of a stashed commit
         *
@@ -113,6 +115,16 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
                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 class StashApplyCommand extends GitCommand<ObjectId> {
                        StashApplyFailureException {
                checkCallable();
 
-               if (repo.getRepositoryState() != RepositoryState.SAFE)
+               if (!ignoreRepositoryState
+                               && repo.getRepositoryState() != RepositoryState.SAFE)
                        throw new WrongRepositoryStateException(MessageFormat.format(
                                        JGitText.get().stashApplyOnUnsafeRepository,
                                        repo.getRepositoryState()));
index fc21b919b69d4aab5af61b4e366e3b3ed495f40d..cf0b6d1d9c4e4e0ec66d14e0835f7c24febdd4a5 100644 (file)
@@ -154,6 +154,7 @@ public class StashCreateCommand extends GitCommand<RevCommit> {
 
        /**
         * Set the reference to update with the stashed commit id
+        * If null, no reference is updated
         * <p>
         * This value defaults to {@link Constants#R_STASH}
         *
@@ -185,6 +186,8 @@ public class StashCreateCommand extends GitCommand<RevCommit> {
 
        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);
index 3ff4eefb1c8cf73e785ab4c051d184972ed19a3d..fd22764b6a8c6a8107af443f30b50ad14b5e903f 100644 (file)
@@ -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";