]> source.dussan.org Git - jgit.git/commitdiff
Add Squash/Fixup support for rebase interactive in RebaseCommand 93/15293/11
authorTobias Pfeifer <to.pfeifer@web.de>
Wed, 7 Aug 2013 12:18:23 +0000 (14:18 +0200)
committerMatthias Sohn <matthias.sohn@sap.com>
Tue, 5 Nov 2013 17:05:02 +0000 (18:05 +0100)
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>
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRebaseStepException.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java

index b9f5bc95eae05d0dc701008b885de955a93f18a9..63ef21d8d30c59a4957e2b94e992fdab33faf01b 100644 (file)
@@ -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;
index 5cf7af81d27d5bc8f1914d3d7641534c3d34ccbd..474b8f349c705eada0cc4377c69f233fdc562582 100644 (file)
@@ -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.
index e7c31da4174b45118b8af345fd8e605792a00ba7..1feb3f2090fe72dbdf9d8514b3ca43e36cc8f9b4 100644 (file)
@@ -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 (file)
index 0000000..764725d
--- /dev/null
@@ -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);
+       }
+}
index b4f99409bd0a295b5a7d4521d9df8e03366de82d..b2c27a483c8d0b14fed144f18ceba882ed03fd99 100644 (file)
@@ -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;
index 747c4d3fc4082c2b48c84cb9cb8e397c78148b29..8eeb1ea8901fba83f880cce44f93d6d94f11fe27 100644 (file)
@@ -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