Merge changes I40f2311c,I3c419094

* changes:
  Add additional RebaseResult for editing commits
  Add Squash/Fixup support for rebase interactive in RebaseCommand
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..eb6c5f0 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.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.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 String modifyCommitMessage(String commit) {
 						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 String modifyCommitMessage(String commit) {
 		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 @@
 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..ef739bb 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.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 RebaseResult call() throws GitAPIException, NoHeadException,
 				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 RebaseResult call() throws GitAPIException, NoHeadException,
 				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 RebaseResult call() throws GitAPIException, NoHeadException,
 								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 RebaseResult call() throws GitAPIException, NoHeadException,
 						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 RebaseResult call() throws GitAPIException, NoHeadException,
 		}
 	}
 
+	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 @@ private PersonIdent parseAuthor() throws IOException {
 		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 @@ private RebaseResult stop(RevCommit commitToPick) throws IOException {
 		// 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) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java
index ff18adc..6df5ffd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java
@@ -85,6 +85,15 @@ public boolean isSuccessful() {
 			}
 		},
 		/**
+		 * Stopped for editing in the context of an interactive rebase
+		 */
+		EDIT {
+			@Override
+			public boolean isSuccessful() {
+				return false;
+			}
+		},
+		/**
 		 * Failed; the original HEAD was restored
 		 */
 		FAILED {
@@ -183,9 +192,10 @@ private RebaseResult(Status status) {
 	 *
 	 * @param commit
 	 *            current commit
+	 * @param status
 	 */
-	RebaseResult(RevCommit commit) {
-		status = Status.STOPPED;
+	RebaseResult(RevCommit commit, RebaseResult.Status status) {
+		this.status = status;
 		currentCommit = commit;
 	}
 
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 static JGitText get() {
 	/***/ 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 static enum Action {
 		/** 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