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>tags/v3.2.0.201312181205-r
@@ -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(); |
@@ -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 { |
@@ -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; |
@@ -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())); |
@@ -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); |
@@ -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"; | |||