aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTobias Pfeifer2013-08-07 08:18:23 (EDT)
committerMatthias Sohn2013-11-05 12:05:02 (EST)
commit765896febb4b63cfe2c37dc0a73c90c79dd1591b (patch)
treef6c13b3c779d5d64a99a294fd960fb460cf20a6c
parent01b5b999a567691fa9602792f365ba0dd5ae061f (diff)
downloadjgit-765896febb4b63cfe2c37dc0a73c90c79dd1591b.zip
jgit-765896febb4b63cfe2c37dc0a73c90c79dd1591b.tar.gz
jgit-765896febb4b63cfe2c37dc0a73c90c79dd1591b.tar.bz2
Add Squash/Fixup support for rebase interactive in RebaseCommandrefs/changes/93/15293/11
The rebase command now supports squash and fixup. Both actions are not allowed as the first step of the rebase. In JGit, before any rebase step is performed, the next commit is already cherry-picked. This commit keeps that behaviour. In case of squash or fixup a soft reset to the parent is perfomed afterwards. CQ: 7684 Bug: 396510 Change-Id: I3c4190940b4d7f19860e223d647fc78705e57203 Signed-off-by: Tobias Pfeifer <to.pfeifer@web.de> Signed-off-by: Stefan Lay <stefan.lay@sap.com> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java339
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java196
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRebaseStepException.java60
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java6
6 files changed, 600 insertions, 3 deletions
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 b9f5bc9..63ef21d 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
@@ -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;
@@ -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;
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index 5cf7af8..474b8f3 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -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.
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 e7c31da..1feb3f2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -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;
@@ -325,6 +335,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
newHead = cherryPickResult.getNewHead();
}
}
+ boolean isSquash = false;
switch (step.getAction()) {
case PICK:
continue; // continue rebase process on pick command
@@ -340,6 +351,20 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return stop(commitToPick);
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.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRebaseStepException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRebaseStepException.java
new file mode 100644
index 0000000..764725d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRebaseStepException.java
@@ -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);
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index b4f9940..b2c27a4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -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;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java
index 747c4d3..8eeb1ea 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java
@@ -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