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>
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;
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;
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();
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;
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;
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
*/
.resolve(upstreamCommitId));
break;
case BEGIN:
+ autoStash();
if (stopAfterInitialization
|| !walk.isMergedInto(
walk.parseCommit(repo.resolve(Constants.HEAD)),
RebaseResult res = initFilesAndRewind();
if (stopAfterInitialization)
return RebaseResult.INTERACTIVE_PREPARED_RESULT;
- if (res != null)
+ if (res != null) {
+ autoStashApply();
return res;
+ }
}
if (monitor.isCancelled())
}
}
+ 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()))
}
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;
// 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(
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());
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;
}
*/
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)
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);
}
}
}
- 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;
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 {
public boolean isSuccessful() {
return false;
}
+ },
+
+ /**
+ * Applying stash resulted in conflicts
+ *
+ * @since 3.2
+ */
+ STASH_APPLY_CONFLICTS {
+ @Override
+ public boolean isSuccessful() {
+ return true;
+ }
};
/**
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;
private boolean applyIndex = true;
+ private boolean ignoreRepositoryState;
+
/**
* Create command to apply the changes of a stashed commit
*
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;
StashApplyFailureException {
checkCallable();
- if (repo.getRepositoryState() != RepositoryState.SAFE)
+ if (!ignoreRepositoryState
+ && repo.getRepositoryState() != RepositoryState.SAFE)
throw new WrongRepositoryStateException(MessageFormat.format(
JGitText.get().stashApplyOnUnsafeRepository,
repo.getRepositoryState()));
/**
* 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}
*
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);
/** 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";
/** 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";