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 <matthias.sohn@sap.com>
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 b33ad6b..a61b44e 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<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();
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 55cf001..10b273a 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<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 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 26d0403..92c1347 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 73d6452..8440d8b 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<ObjectId> {
 
 	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 fc21b91..cf0b6d1 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
 	 * <p>
 	 * 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 3ff4eef..fd22764 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";