* changes: Add additional RebaseResult for editing commits Add Squash/Fixup support for rebase interactive in RebaseCommandtags/v3.2.0.201311130903-m3
@@ -42,9 +42,9 @@ | |||
*/ | |||
package org.eclipse.jgit.api; | |||
import static org.hamcrest.MatcherAssert.assertThat; | |||
import static org.hamcrest.CoreMatchers.equalTo; | |||
import static org.hamcrest.CoreMatchers.not; | |||
import static org.hamcrest.MatcherAssert.assertThat; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertFalse; | |||
import static org.junit.Assert.assertNotNull; | |||
@@ -63,6 +63,7 @@ import org.eclipse.jgit.api.MergeResult.MergeStatus; | |||
import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler; | |||
import org.eclipse.jgit.api.RebaseCommand.Operation; | |||
import org.eclipse.jgit.api.RebaseResult.Status; | |||
import org.eclipse.jgit.api.errors.InvalidRebaseStepException; | |||
import org.eclipse.jgit.api.errors.JGitInternalException; | |||
import org.eclipse.jgit.api.errors.RefNotFoundException; | |||
import org.eclipse.jgit.api.errors.UnmergedPathsException; | |||
@@ -83,6 +84,8 @@ import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.revwalk.RevWalk; | |||
import org.eclipse.jgit.util.FileUtils; | |||
import org.eclipse.jgit.util.IO; | |||
import org.eclipse.jgit.util.RawParseUtils; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
@@ -1938,7 +1941,7 @@ public class RebaseCommandTest extends RepositoryTestCase { | |||
return ""; // not used | |||
} | |||
}).call(); | |||
assertEquals(Status.STOPPED, res.getStatus()); | |||
assertEquals(Status.EDIT, res.getStatus()); | |||
RevCommit toBeEditted = git.log().call().iterator().next(); | |||
assertEquals("updated file1 on master", toBeEditted.getFullMessage()); | |||
@@ -1957,6 +1960,340 @@ public class RebaseCommandTest extends RepositoryTestCase { | |||
assertEquals("edited commit message", actualCommitMag); | |||
} | |||
@Test | |||
public void testParseSquashFixupSequenceCount() { | |||
int count = RebaseCommand | |||
.parseSquashFixupSequenceCount("# This is a combination of 3 commits.\n# newline"); | |||
assertEquals(3, count); | |||
} | |||
@Test | |||
public void testRebaseInteractiveSingleSquashAndModifyMessage() throws Exception { | |||
// create file1 on master | |||
writeTrashFile(FILE1, FILE1); | |||
git.add().addFilepattern(FILE1).call(); | |||
git.commit().setMessage("Add file1\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), FILE1).exists()); | |||
// create file2 on master | |||
writeTrashFile("file2", "file2"); | |||
git.add().addFilepattern("file2").call(); | |||
git.commit().setMessage("Add file2\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), "file2").exists()); | |||
// update FILE1 on master | |||
writeTrashFile(FILE1, "blah"); | |||
git.add().addFilepattern(FILE1).call(); | |||
git.commit().setMessage("updated file1 on master\nnew line").call(); | |||
writeTrashFile("file2", "more change"); | |||
git.add().addFilepattern("file2").call(); | |||
git.commit().setMessage("update file2 on master\nnew line").call(); | |||
git.rebase().setUpstream("HEAD~3") | |||
.runInteractively(new InteractiveHandler() { | |||
public void prepareSteps(List<RebaseTodoLine> steps) { | |||
steps.get(1).setAction(Action.SQUASH); | |||
} | |||
public String modifyCommitMessage(String commit) { | |||
final File messageSquashFile = new File(db | |||
.getDirectory(), "rebase-merge/message-squash"); | |||
final File messageFixupFile = new File(db | |||
.getDirectory(), "rebase-merge/message-fixup"); | |||
assertFalse(messageFixupFile.exists()); | |||
assertTrue(messageSquashFile.exists()); | |||
assertEquals( | |||
"# This is a combination of 2 commits.\n# This is the 2nd commit message:\nupdated file1 on master\nnew line\n# The first commit's message is:\nAdd file2\nnew line", | |||
commit); | |||
try { | |||
byte[] messageSquashBytes = IO | |||
.readFully(messageSquashFile); | |||
int end = RawParseUtils.prevLF(messageSquashBytes, | |||
messageSquashBytes.length); | |||
String messageSquashContent = RawParseUtils.decode( | |||
messageSquashBytes, 0, end + 1); | |||
assertEquals(messageSquashContent, commit); | |||
} catch (Throwable t) { | |||
fail(t.getMessage()); | |||
} | |||
return "changed"; | |||
} | |||
}).call(); | |||
RevWalk walk = new RevWalk(db); | |||
ObjectId headId = db.resolve(Constants.HEAD); | |||
RevCommit headCommit = walk.parseCommit(headId); | |||
assertEquals(headCommit.getFullMessage(), | |||
"update file2 on master\nnew line"); | |||
ObjectId head2Id = db.resolve(Constants.HEAD + "^1"); | |||
RevCommit head1Commit = walk.parseCommit(head2Id); | |||
assertEquals("changed", head1Commit.getFullMessage()); | |||
} | |||
@Test | |||
public void testRebaseInteractiveMultipleSquash() throws Exception { | |||
// create file0 on master | |||
writeTrashFile("file0", "file0"); | |||
git.add().addFilepattern("file0").call(); | |||
git.commit().setMessage("Add file0\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), "file0").exists()); | |||
// create file1 on master | |||
writeTrashFile(FILE1, FILE1); | |||
git.add().addFilepattern(FILE1).call(); | |||
git.commit().setMessage("Add file1\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), FILE1).exists()); | |||
// create file2 on master | |||
writeTrashFile("file2", "file2"); | |||
git.add().addFilepattern("file2").call(); | |||
git.commit().setMessage("Add file2\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), "file2").exists()); | |||
// update FILE1 on master | |||
writeTrashFile(FILE1, "blah"); | |||
git.add().addFilepattern(FILE1).call(); | |||
git.commit().setMessage("updated file1 on master\nnew line").call(); | |||
writeTrashFile("file2", "more change"); | |||
git.add().addFilepattern("file2").call(); | |||
git.commit().setMessage("update file2 on master\nnew line").call(); | |||
git.rebase().setUpstream("HEAD~4") | |||
.runInteractively(new InteractiveHandler() { | |||
public void prepareSteps(List<RebaseTodoLine> steps) { | |||
steps.get(1).setAction(Action.SQUASH); | |||
steps.get(2).setAction(Action.SQUASH); | |||
} | |||
public String modifyCommitMessage(String commit) { | |||
final File messageSquashFile = new File(db.getDirectory(), | |||
"rebase-merge/message-squash"); | |||
final File messageFixupFile = new File(db.getDirectory(), | |||
"rebase-merge/message-fixup"); | |||
assertFalse(messageFixupFile.exists()); | |||
assertTrue(messageSquashFile.exists()); | |||
assertEquals( | |||
"# This is a combination of 3 commits.\n# This is the 3rd commit message:\nupdated file1 on master\nnew line\n# This is the 2nd commit message:\nAdd file2\nnew line\n# The first commit's message is:\nAdd file1\nnew line", | |||
commit); | |||
try { | |||
byte[] messageSquashBytes = IO | |||
.readFully(messageSquashFile); | |||
int end = RawParseUtils.prevLF(messageSquashBytes, | |||
messageSquashBytes.length); | |||
String messageSquashContend = RawParseUtils.decode( | |||
messageSquashBytes, 0, end + 1); | |||
assertEquals(messageSquashContend, commit); | |||
} catch (Throwable t) { | |||
fail(t.getMessage()); | |||
} | |||
return "# This is a combination of 3 commits.\n# This is the 3rd commit message:\nupdated file1 on master\nnew line\n# This is the 2nd commit message:\nAdd file2\nnew line\n# The first commit's message is:\nAdd file1\nnew line"; | |||
} | |||
}).call(); | |||
RevWalk walk = new RevWalk(db); | |||
ObjectId headId = db.resolve(Constants.HEAD); | |||
RevCommit headCommit = walk.parseCommit(headId); | |||
assertEquals(headCommit.getFullMessage(), | |||
"update file2 on master\nnew line"); | |||
ObjectId head2Id = db.resolve(Constants.HEAD + "^1"); | |||
RevCommit head1Commit = walk.parseCommit(head2Id); | |||
assertEquals( | |||
"updated file1 on master\nnew line\nAdd file2\nnew line\nAdd file1\nnew line", | |||
head1Commit.getFullMessage()); | |||
} | |||
@Test | |||
public void testRebaseInteractiveMixedSquashAndFixup() throws Exception { | |||
// create file0 on master | |||
writeTrashFile("file0", "file0"); | |||
git.add().addFilepattern("file0").call(); | |||
git.commit().setMessage("Add file0\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), "file0").exists()); | |||
// create file1 on master | |||
writeTrashFile(FILE1, FILE1); | |||
git.add().addFilepattern(FILE1).call(); | |||
git.commit().setMessage("Add file1\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), FILE1).exists()); | |||
// create file2 on master | |||
writeTrashFile("file2", "file2"); | |||
git.add().addFilepattern("file2").call(); | |||
git.commit().setMessage("Add file2\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), "file2").exists()); | |||
// update FILE1 on master | |||
writeTrashFile(FILE1, "blah"); | |||
git.add().addFilepattern(FILE1).call(); | |||
git.commit().setMessage("updated file1 on master\nnew line").call(); | |||
writeTrashFile("file2", "more change"); | |||
git.add().addFilepattern("file2").call(); | |||
git.commit().setMessage("update file2 on master\nnew line").call(); | |||
git.rebase().setUpstream("HEAD~4") | |||
.runInteractively(new InteractiveHandler() { | |||
public void prepareSteps(List<RebaseTodoLine> steps) { | |||
steps.get(1).setAction(Action.FIXUP); | |||
steps.get(2).setAction(Action.SQUASH); | |||
} | |||
public String modifyCommitMessage(String commit) { | |||
final File messageSquashFile = new File(db | |||
.getDirectory(), "rebase-merge/message-squash"); | |||
final File messageFixupFile = new File(db | |||
.getDirectory(), "rebase-merge/message-fixup"); | |||
assertFalse(messageFixupFile.exists()); | |||
assertTrue(messageSquashFile.exists()); | |||
assertEquals( | |||
"# This is a combination of 3 commits.\n# This is the 3rd commit message:\nupdated file1 on master\nnew line\n# The 2nd commit message will be skipped:\n# Add file2\n# new line\n# The first commit's message is:\nAdd file1\nnew line", | |||
commit); | |||
try { | |||
byte[] messageSquashBytes = IO | |||
.readFully(messageSquashFile); | |||
int end = RawParseUtils.prevLF(messageSquashBytes, | |||
messageSquashBytes.length); | |||
String messageSquashContend = RawParseUtils.decode( | |||
messageSquashBytes, 0, end + 1); | |||
assertEquals(messageSquashContend, commit); | |||
} catch (Throwable t) { | |||
fail(t.getMessage()); | |||
} | |||
return "changed"; | |||
} | |||
}).call(); | |||
RevWalk walk = new RevWalk(db); | |||
ObjectId headId = db.resolve(Constants.HEAD); | |||
RevCommit headCommit = walk.parseCommit(headId); | |||
assertEquals(headCommit.getFullMessage(), | |||
"update file2 on master\nnew line"); | |||
ObjectId head2Id = db.resolve(Constants.HEAD + "^1"); | |||
RevCommit head1Commit = walk.parseCommit(head2Id); | |||
assertEquals("changed", head1Commit.getFullMessage()); | |||
} | |||
@Test | |||
public void testRebaseInteractiveSingleFixup() throws Exception { | |||
// create file1 on master | |||
writeTrashFile(FILE1, FILE1); | |||
git.add().addFilepattern(FILE1).call(); | |||
git.commit().setMessage("Add file1\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), FILE1).exists()); | |||
// create file2 on master | |||
writeTrashFile("file2", "file2"); | |||
git.add().addFilepattern("file2").call(); | |||
git.commit().setMessage("Add file2\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), "file2").exists()); | |||
// update FILE1 on master | |||
writeTrashFile(FILE1, "blah"); | |||
git.add().addFilepattern(FILE1).call(); | |||
git.commit().setMessage("updated file1 on master\nnew line").call(); | |||
writeTrashFile("file2", "more change"); | |||
git.add().addFilepattern("file2").call(); | |||
git.commit().setMessage("update file2 on master\nnew line").call(); | |||
git.rebase().setUpstream("HEAD~3") | |||
.runInteractively(new InteractiveHandler() { | |||
public void prepareSteps(List<RebaseTodoLine> steps) { | |||
steps.get(1).setAction(Action.FIXUP); | |||
} | |||
public String modifyCommitMessage(String commit) { | |||
fail("No callback to modify commit message expected for single fixup"); | |||
return commit; | |||
} | |||
}).call(); | |||
RevWalk walk = new RevWalk(db); | |||
ObjectId headId = db.resolve(Constants.HEAD); | |||
RevCommit headCommit = walk.parseCommit(headId); | |||
assertEquals("update file2 on master\nnew line", | |||
headCommit.getFullMessage()); | |||
ObjectId head1Id = db.resolve(Constants.HEAD + "^1"); | |||
RevCommit head1Commit = walk.parseCommit(head1Id); | |||
assertEquals("Add file2\nnew line", | |||
head1Commit.getFullMessage()); | |||
} | |||
@Test(expected = InvalidRebaseStepException.class) | |||
public void testRebaseInteractiveFixupFirstCommitShouldFail() | |||
throws Exception { | |||
// create file1 on master | |||
writeTrashFile(FILE1, FILE1); | |||
git.add().addFilepattern(FILE1).call(); | |||
git.commit().setMessage("Add file1\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), FILE1).exists()); | |||
// create file2 on master | |||
writeTrashFile("file2", "file2"); | |||
git.add().addFilepattern("file2").call(); | |||
git.commit().setMessage("Add file2\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), "file2").exists()); | |||
git.rebase().setUpstream("HEAD~1") | |||
.runInteractively(new InteractiveHandler() { | |||
public void prepareSteps(List<RebaseTodoLine> steps) { | |||
steps.get(0).setAction(Action.FIXUP); | |||
} | |||
public String modifyCommitMessage(String commit) { | |||
return commit; | |||
} | |||
}).call(); | |||
} | |||
@Test(expected = InvalidRebaseStepException.class) | |||
public void testRebaseInteractiveSquashFirstCommitShouldFail() | |||
throws Exception { | |||
// create file1 on master | |||
writeTrashFile(FILE1, FILE1); | |||
git.add().addFilepattern(FILE1).call(); | |||
git.commit().setMessage("Add file1\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), FILE1).exists()); | |||
// create file2 on master | |||
writeTrashFile("file2", "file2"); | |||
git.add().addFilepattern("file2").call(); | |||
git.commit().setMessage("Add file2\nnew line").call(); | |||
assertTrue(new File(db.getWorkTree(), "file2").exists()); | |||
git.rebase().setUpstream("HEAD~1") | |||
.runInteractively(new InteractiveHandler() { | |||
public void prepareSteps(List<RebaseTodoLine> steps) { | |||
steps.get(0).setAction(Action.SQUASH); | |||
} | |||
public String modifyCommitMessage(String commit) { | |||
return commit; | |||
} | |||
}).call(); | |||
} | |||
private File getTodoFile() { | |||
File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO); | |||
return todoFile; |
@@ -78,6 +78,7 @@ cannotReadObject=Cannot read object | |||
cannotReadTree=Cannot read tree {0} | |||
cannotRebaseWithoutCurrentHead=Can not rebase without a current HEAD | |||
cannotResolveLocalTrackingRefForUpdating=Cannot resolve local tracking ref {0} for updating. | |||
cannotSquashFixupWithoutPreviousCommit=Cannot {0} without previous commit. | |||
cannotStoreObjects=cannot store objects | |||
cannotUnloadAModifiedTree=Cannot unload a modified tree. | |||
cannotWorkWithOtherStagesThanZeroRightNow=Cannot work with other stages than zero right now. Won't write corrupt index. |
@@ -55,10 +55,14 @@ import java.util.HashMap; | |||
import java.util.LinkedList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.regex.Matcher; | |||
import java.util.regex.Pattern; | |||
import org.eclipse.jgit.api.RebaseResult.Status; | |||
import org.eclipse.jgit.api.ResetCommand.ResetType; | |||
import org.eclipse.jgit.api.errors.CheckoutConflictException; | |||
import org.eclipse.jgit.api.errors.GitAPIException; | |||
import org.eclipse.jgit.api.errors.InvalidRebaseStepException; | |||
import org.eclipse.jgit.api.errors.InvalidRefNameException; | |||
import org.eclipse.jgit.api.errors.JGitInternalException; | |||
import org.eclipse.jgit.api.errors.NoHeadException; | |||
@@ -147,6 +151,10 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
private static final String AMEND = "amend"; //$NON-NLS-1$ | |||
private static final String MESSAGE_FIXUP = "message-fixup"; //$NON-NLS-1$ | |||
private static final String MESSAGE_SQUASH = "message-squash"; //$NON-NLS-1$ | |||
/** | |||
* The available operations | |||
*/ | |||
@@ -281,7 +289,9 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO), | |||
steps, false); | |||
} | |||
for (RebaseTodoLine step : steps) { | |||
checkSteps(steps); | |||
for (int i = 0; i < steps.size(); i++) { | |||
RebaseTodoLine step = steps.get(i); | |||
popSteps(1); | |||
if (Action.COMMENT.equals(step.getAction())) | |||
continue; | |||
@@ -292,7 +302,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
RevCommit commitToPick = walk | |||
.parseCommit(ids.iterator().next()); | |||
if (monitor.isCancelled()) | |||
return new RebaseResult(commitToPick); | |||
return new RebaseResult(commitToPick, Status.STOPPED); | |||
try { | |||
monitor.beginTask(MessageFormat.format( | |||
JGitText.get().applyingCommit, | |||
@@ -318,13 +328,14 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
return abort(new RebaseResult( | |||
cherryPickResult.getFailingPaths())); | |||
else | |||
return stop(commitToPick); | |||
return stop(commitToPick, Status.STOPPED); | |||
case CONFLICTING: | |||
return stop(commitToPick); | |||
return stop(commitToPick, Status.STOPPED); | |||
case OK: | |||
newHead = cherryPickResult.getNewHead(); | |||
} | |||
} | |||
boolean isSquash = false; | |||
switch (step.getAction()) { | |||
case PICK: | |||
continue; // continue rebase process on pick command | |||
@@ -337,9 +348,23 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
continue; | |||
case EDIT: | |||
rebaseState.createFile(AMEND, commitToPick.name()); | |||
return stop(commitToPick); | |||
return stop(commitToPick, Status.EDIT); | |||
case COMMENT: | |||
break; | |||
case SQUASH: | |||
isSquash = true; | |||
//$FALL-THROUGH$ | |||
case FIXUP: | |||
resetSoftToParent(); | |||
RebaseTodoLine nextStep = (i >= steps.size() - 1 ? null | |||
: steps.get(i + 1)); | |||
File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP); | |||
File messageSquashFile = rebaseState | |||
.getFile(MESSAGE_SQUASH); | |||
if (isSquash && messageFixupFile.exists()) | |||
messageFixupFile.delete(); | |||
newHead = doSquashFixup(isSquash, commitToPick, | |||
nextStep, messageFixupFile, messageSquashFile); | |||
} | |||
} finally { | |||
monitor.endTask(); | |||
@@ -361,6 +386,175 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
} | |||
} | |||
private void checkSteps(List<RebaseTodoLine> steps) | |||
throws InvalidRebaseStepException, IOException { | |||
if (steps.isEmpty()) | |||
return; | |||
if (RebaseTodoLine.Action.SQUASH.equals(steps.get(0).getAction()) | |||
|| RebaseTodoLine.Action.FIXUP.equals(steps.get(0).getAction())) { | |||
if (!rebaseState.getFile(DONE).exists() | |||
|| rebaseState.readFile(DONE).trim().length() == 0) { | |||
throw new InvalidRebaseStepException(MessageFormat.format( | |||
JGitText.get().cannotSquashFixupWithoutPreviousCommit, | |||
steps.get(0).getAction().name())); | |||
} | |||
} | |||
} | |||
private RevCommit doSquashFixup(boolean isSquash, RevCommit commitToPick, | |||
RebaseTodoLine nextStep, File messageFixup, File messageSquash) | |||
throws IOException, GitAPIException { | |||
if (!messageSquash.exists()) { | |||
// init squash/fixup sequence | |||
ObjectId headId = repo.resolve(Constants.HEAD); | |||
RevCommit previousCommit = walk.parseCommit(headId); | |||
initializeSquashFixupFile(MESSAGE_SQUASH, | |||
previousCommit.getFullMessage()); | |||
if (!isSquash) | |||
initializeSquashFixupFile(MESSAGE_FIXUP, | |||
previousCommit.getFullMessage()); | |||
} | |||
String currSquashMessage = rebaseState | |||
.readFile(MESSAGE_SQUASH); | |||
int count = parseSquashFixupSequenceCount(currSquashMessage) + 1; | |||
String content = composeSquashMessage(isSquash, | |||
commitToPick, currSquashMessage, count); | |||
rebaseState.createFile(MESSAGE_SQUASH, content); | |||
if (messageFixup.exists()) | |||
rebaseState.createFile(MESSAGE_FIXUP, content); | |||
return squashIntoPrevious( | |||
!messageFixup.exists(), | |||
nextStep); | |||
} | |||
private void resetSoftToParent() throws IOException, | |||
GitAPIException, CheckoutConflictException { | |||
Ref orig_head = repo.getRef(Constants.ORIG_HEAD); | |||
ObjectId orig_headId = orig_head.getObjectId(); | |||
try { | |||
// we have already commited the cherry-picked commit. | |||
// what we need is to have changes introduced by this | |||
// commit to be on the index | |||
// resetting is a workaround | |||
Git.wrap(repo).reset().setMode(ResetType.SOFT) | |||
.setRef("HEAD~1").call(); //$NON-NLS-1$ | |||
} finally { | |||
// set ORIG_HEAD back to where we started because soft | |||
// reset moved it | |||
repo.writeOrigHead(orig_headId); | |||
} | |||
} | |||
private RevCommit squashIntoPrevious(boolean sequenceContainsSquash, | |||
RebaseTodoLine nextStep) | |||
throws IOException, GitAPIException { | |||
RevCommit newHead; | |||
String commitMessage = rebaseState | |||
.readFile(MESSAGE_SQUASH); | |||
if (nextStep == null | |||
|| ((nextStep.getAction() != Action.FIXUP) && (nextStep | |||
.getAction() != Action.SQUASH))) { | |||
// this is the last step in this sequence | |||
if (sequenceContainsSquash) { | |||
commitMessage = interactiveHandler | |||
.modifyCommitMessage(commitMessage); | |||
} | |||
newHead = new Git(repo).commit() | |||
.setMessage(stripCommentLines(commitMessage)) | |||
.setAmend(true).call(); | |||
rebaseState.getFile(MESSAGE_SQUASH).delete(); | |||
rebaseState.getFile(MESSAGE_FIXUP).delete(); | |||
} else { | |||
// Next step is either Squash or Fixup | |||
newHead = new Git(repo).commit() | |||
.setMessage(commitMessage).setAmend(true) | |||
.call(); | |||
} | |||
return newHead; | |||
} | |||
private static String stripCommentLines(String commitMessage) { | |||
StringBuilder result = new StringBuilder(); | |||
for (String line : commitMessage.split("\n")) { //$NON-NLS-1$ | |||
if (!line.trim().startsWith("#")) //$NON-NLS-1$ | |||
result.append(line).append("\n"); //$NON-NLS-1$ | |||
} | |||
if (!commitMessage.endsWith("\n")) //$NON-NLS-1$ | |||
result.deleteCharAt(result.length() - 1); | |||
return result.toString(); | |||
} | |||
@SuppressWarnings("nls") | |||
private static String composeSquashMessage(boolean isSquash, | |||
RevCommit commitToPick, String currSquashMessage, int count) { | |||
StringBuilder sb = new StringBuilder(); | |||
String ordinal = getOrdinal(count); | |||
sb.setLength(0); | |||
sb.append("# This is a combination of ").append(count) | |||
.append(" commits.\n"); | |||
if (isSquash) { | |||
sb.append("# This is the ").append(count).append(ordinal) | |||
.append(" commit message:\n"); | |||
sb.append(commitToPick.getFullMessage()); | |||
} else { | |||
sb.append("# The ").append(count).append(ordinal) | |||
.append(" commit message will be skipped:\n# "); | |||
sb.append(commitToPick.getFullMessage().replaceAll("([\n\r]+)", | |||
"$1# ")); | |||
} | |||
// Add the previous message without header (i.e first line) | |||
sb.append("\n"); | |||
sb.append(currSquashMessage.substring(currSquashMessage.indexOf("\n") + 1)); | |||
return sb.toString(); | |||
} | |||
private static String getOrdinal(int count) { | |||
switch (count % 10) { | |||
case 1: | |||
return "st"; //$NON-NLS-1$ | |||
case 2: | |||
return "nd"; //$NON-NLS-1$ | |||
case 3: | |||
return "rd"; //$NON-NLS-1$ | |||
default: | |||
return "th"; //$NON-NLS-1$ | |||
} | |||
} | |||
/** | |||
* Parse the count from squashed commit messages | |||
* | |||
* @param currSquashMessage | |||
* the squashed commit message to be parsed | |||
* @return the count of squashed messages in the given string | |||
*/ | |||
static int parseSquashFixupSequenceCount(String currSquashMessage) { | |||
String regex = "This is a combination of (.*) commits"; //$NON-NLS-1$ | |||
String firstLine = currSquashMessage.substring(0, | |||
currSquashMessage.indexOf("\n")); //$NON-NLS-1$ | |||
Pattern pattern = Pattern.compile(regex); | |||
Matcher matcher = pattern.matcher(firstLine); | |||
if (!matcher.find()) | |||
throw new IllegalArgumentException(); | |||
return Integer.parseInt(matcher.group(1)); | |||
} | |||
private void initializeSquashFixupFile(String messageFile, | |||
String fullMessage) throws IOException { | |||
rebaseState | |||
.createFile( | |||
messageFile, | |||
"# This is a combination of 1 commits.\n# The first commit's message is:\n" + fullMessage); //$NON-NLS-1$); | |||
} | |||
private String getOurCommitName() { | |||
// If onto is different from upstream, this should say "onto", but | |||
// RebaseCommand doesn't support a different "onto" at the moment. | |||
@@ -479,7 +673,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
return parseAuthor(raw); | |||
} | |||
private RebaseResult stop(RevCommit commitToPick) throws IOException { | |||
private RebaseResult stop(RevCommit commitToPick, RebaseResult.Status status) | |||
throws IOException { | |||
PersonIdent author = commitToPick.getAuthorIdent(); | |||
String authorScript = toAuthorScript(author); | |||
rebaseState.createFile(AUTHOR_SCRIPT, authorScript); | |||
@@ -497,7 +692,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { | |||
// Remove cherry pick state file created by CherryPickCommand, it's not | |||
// needed for rebase | |||
repo.writeCherryPickHead(null); | |||
return new RebaseResult(commitToPick); | |||
return new RebaseResult(commitToPick, status); | |||
} | |||
String toAuthorScript(PersonIdent author) { |
@@ -84,6 +84,15 @@ public class RebaseResult { | |||
return false; | |||
} | |||
}, | |||
/** | |||
* Stopped for editing in the context of an interactive rebase | |||
*/ | |||
EDIT { | |||
@Override | |||
public boolean isSuccessful() { | |||
return false; | |||
} | |||
}, | |||
/** | |||
* Failed; the original HEAD was restored | |||
*/ | |||
@@ -183,9 +192,10 @@ public class RebaseResult { | |||
* | |||
* @param commit | |||
* current commit | |||
* @param status | |||
*/ | |||
RebaseResult(RevCommit commit) { | |||
status = Status.STOPPED; | |||
RebaseResult(RevCommit commit, RebaseResult.Status status) { | |||
this.status = status; | |||
currentCommit = commit; | |||
} | |||
@@ -0,0 +1,60 @@ | |||
/* | |||
* Copyright (C) 2013, Stefan Lay <stefan.lay@sap.com> and | |||
* other copyright owners as documented in the project's IP log. | |||
* | |||
* This program and the accompanying materials are made available under the | |||
* terms of the Eclipse Distribution License v1.0 which accompanies this | |||
* distribution, is reproduced below, and is available at | |||
* http://www.eclipse.org/org/documents/edl-v10.php | |||
* | |||
* All rights reserved. | |||
* | |||
* Redistribution and use in source and binary forms, with or without | |||
* modification, are permitted provided that the following conditions are met: | |||
* | |||
* - Redistributions of source code must retain the above copyright notice, this | |||
* list of conditions and the following disclaimer. | |||
* | |||
* - Redistributions in binary form must reproduce the above copyright notice, | |||
* this list of conditions and the following disclaimer in the documentation | |||
* and/or other materials provided with the distribution. | |||
* | |||
* - Neither the name of the Eclipse Foundation, Inc. nor the names of its | |||
* contributors may be used to endorse or promote products derived from this | |||
* software without specific prior written permission. | |||
* | |||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | |||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |||
* POSSIBILITY OF SUCH DAMAGE. | |||
*/ | |||
package org.eclipse.jgit.api.errors; | |||
/** | |||
* Exception thrown if a rebase step is invalid. E.g., a rebase must not start | |||
* with squash or fixup. | |||
*/ | |||
public class InvalidRebaseStepException extends GitAPIException { | |||
private static final long serialVersionUID = 1L; | |||
/** | |||
* @param msg | |||
*/ | |||
public InvalidRebaseStepException(String msg) { | |||
super(msg); | |||
} | |||
/** | |||
* @param msg | |||
* @param cause | |||
*/ | |||
public InvalidRebaseStepException(String msg, Throwable cause) { | |||
super(msg, cause); | |||
} | |||
} |
@@ -140,6 +140,7 @@ public class JGitText extends TranslationBundle { | |||
/***/ public String cannotReadTree; | |||
/***/ public String cannotRebaseWithoutCurrentHead; | |||
/***/ public String cannotResolveLocalTrackingRefForUpdating; | |||
/***/ public String cannotSquashFixupWithoutPreviousCommit; | |||
/***/ public String cannotStoreObjects; | |||
/***/ public String cannotUnloadAModifiedTree; | |||
/***/ public String cannotWorkWithOtherStagesThanZeroRightNow; |
@@ -66,7 +66,11 @@ public class RebaseTodoLine { | |||
/** Use commit, but stop for amending */ | |||
EDIT("edit", "e"), | |||
// TODO: add SQUASH, FIXUP, etc. | |||
/** Use commit, but meld into previous commit */ | |||
SQUASH("squash", "s"), | |||
/** like "squash", but discard this commit's log message */ | |||
FIXUP("fixup", "f"), | |||
/** | |||
* A comment in the file. Also blank lines (or lines containing only |