summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Sohn <matthias.sohn@sap.com>2013-11-05 17:03:35 -0500
committerGerrit Code Review @ Eclipse.org <gerrit@eclipse.org>2013-11-05 17:03:35 -0500
commit34fbd814d40a18f8be57e3d8a766e854f2fe2d00 (patch)
treec6704e24f935fa998a29f8ee445bd26230284e1f
parentb8eac43c0f57e3f1829ab6981bfe2aeeb39e78c5 (diff)
parentcce2561e9fe2ce1cf60182f9d95c8537ce13de92 (diff)
downloadjgit-34fbd814d40a18f8be57e3d8a766e854f2fe2d00.tar.gz
jgit-34fbd814d40a18f8be57e3d8a766e854f2fe2d00.zip
Merge changes I40f2311c,I3c419094
* changes: Add additional RebaseResult for editing commits Add Squash/Fixup support for rebase interactive in RebaseCommand
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java341
-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.java209
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java14
-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
7 files changed, 620 insertions, 12 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 b9f5bc95ea..eb6c5f0a6b 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;
@@ -1938,7 +1941,7 @@ public class RebaseCommandTest extends RepositoryTestCase {
return ""; // not used
}
}).call();
- assertEquals(Status.STOPPED, res.getStatus());
+ assertEquals(Status.EDIT, res.getStatus());
RevCommit toBeEditted = git.log().call().iterator().next();
assertEquals("updated file1 on master", toBeEditted.getFullMessage());
@@ -1957,6 +1960,340 @@ public class RebaseCommandTest extends RepositoryTestCase {
assertEquals("edited commit message", actualCommitMag);
}
+ @Test
+ public void testParseSquashFixupSequenceCount() {
+ int count = RebaseCommand
+ .parseSquashFixupSequenceCount("# This is a combination of 3 commits.\n# newline");
+ assertEquals(3, count);
+ }
+
+ @Test
+ public void testRebaseInteractiveSingleSquashAndModifyMessage() throws Exception {
+ // create file1 on master
+ writeTrashFile(FILE1, FILE1);
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("Add file1\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), FILE1).exists());
+
+ // create file2 on master
+ writeTrashFile("file2", "file2");
+ git.add().addFilepattern("file2").call();
+ git.commit().setMessage("Add file2\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), "file2").exists());
+
+ // update FILE1 on master
+ writeTrashFile(FILE1, "blah");
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("updated file1 on master\nnew line").call();
+
+ writeTrashFile("file2", "more change");
+ git.add().addFilepattern("file2").call();
+ git.commit().setMessage("update file2 on master\nnew line").call();
+
+ git.rebase().setUpstream("HEAD~3")
+ .runInteractively(new InteractiveHandler() {
+
+ public void prepareSteps(List<RebaseTodoLine> steps) {
+ steps.get(1).setAction(Action.SQUASH);
+ }
+
+ public String modifyCommitMessage(String commit) {
+ final File messageSquashFile = new File(db
+ .getDirectory(), "rebase-merge/message-squash");
+ final File messageFixupFile = new File(db
+ .getDirectory(), "rebase-merge/message-fixup");
+
+ assertFalse(messageFixupFile.exists());
+ assertTrue(messageSquashFile.exists());
+ assertEquals(
+ "# This is a combination of 2 commits.\n# This is the 2nd commit message:\nupdated file1 on master\nnew line\n# The first commit's message is:\nAdd file2\nnew line",
+ commit);
+
+ try {
+ byte[] messageSquashBytes = IO
+ .readFully(messageSquashFile);
+ int end = RawParseUtils.prevLF(messageSquashBytes,
+ messageSquashBytes.length);
+ String messageSquashContent = RawParseUtils.decode(
+ messageSquashBytes, 0, end + 1);
+ assertEquals(messageSquashContent, commit);
+ } catch (Throwable t) {
+ fail(t.getMessage());
+ }
+
+ return "changed";
+ }
+ }).call();
+
+ RevWalk walk = new RevWalk(db);
+ ObjectId headId = db.resolve(Constants.HEAD);
+ RevCommit headCommit = walk.parseCommit(headId);
+ assertEquals(headCommit.getFullMessage(),
+ "update file2 on master\nnew line");
+
+ ObjectId head2Id = db.resolve(Constants.HEAD + "^1");
+ RevCommit head1Commit = walk.parseCommit(head2Id);
+ assertEquals("changed", head1Commit.getFullMessage());
+ }
+
+ @Test
+ public void testRebaseInteractiveMultipleSquash() throws Exception {
+ // create file0 on master
+ writeTrashFile("file0", "file0");
+ git.add().addFilepattern("file0").call();
+ git.commit().setMessage("Add file0\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), "file0").exists());
+
+ // create file1 on master
+ writeTrashFile(FILE1, FILE1);
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("Add file1\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), FILE1).exists());
+
+ // create file2 on master
+ writeTrashFile("file2", "file2");
+ git.add().addFilepattern("file2").call();
+ git.commit().setMessage("Add file2\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), "file2").exists());
+
+ // update FILE1 on master
+ writeTrashFile(FILE1, "blah");
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("updated file1 on master\nnew line").call();
+
+ writeTrashFile("file2", "more change");
+ git.add().addFilepattern("file2").call();
+ git.commit().setMessage("update file2 on master\nnew line").call();
+
+ git.rebase().setUpstream("HEAD~4")
+ .runInteractively(new InteractiveHandler() {
+
+ public void prepareSteps(List<RebaseTodoLine> steps) {
+ steps.get(1).setAction(Action.SQUASH);
+ steps.get(2).setAction(Action.SQUASH);
+ }
+
+ public String modifyCommitMessage(String commit) {
+ final File messageSquashFile = new File(db.getDirectory(),
+ "rebase-merge/message-squash");
+ final File messageFixupFile = new File(db.getDirectory(),
+ "rebase-merge/message-fixup");
+ assertFalse(messageFixupFile.exists());
+ assertTrue(messageSquashFile.exists());
+ assertEquals(
+ "# This is a combination of 3 commits.\n# This is the 3rd commit message:\nupdated file1 on master\nnew line\n# This is the 2nd commit message:\nAdd file2\nnew line\n# The first commit's message is:\nAdd file1\nnew line",
+ commit);
+
+ try {
+ byte[] messageSquashBytes = IO
+ .readFully(messageSquashFile);
+ int end = RawParseUtils.prevLF(messageSquashBytes,
+ messageSquashBytes.length);
+ String messageSquashContend = RawParseUtils.decode(
+ messageSquashBytes, 0, end + 1);
+ assertEquals(messageSquashContend, commit);
+ } catch (Throwable t) {
+ fail(t.getMessage());
+ }
+
+ return "# This is a combination of 3 commits.\n# This is the 3rd commit message:\nupdated file1 on master\nnew line\n# This is the 2nd commit message:\nAdd file2\nnew line\n# The first commit's message is:\nAdd file1\nnew line";
+ }
+ }).call();
+
+ RevWalk walk = new RevWalk(db);
+ ObjectId headId = db.resolve(Constants.HEAD);
+ RevCommit headCommit = walk.parseCommit(headId);
+ assertEquals(headCommit.getFullMessage(),
+ "update file2 on master\nnew line");
+
+ ObjectId head2Id = db.resolve(Constants.HEAD + "^1");
+ RevCommit head1Commit = walk.parseCommit(head2Id);
+ assertEquals(
+ "updated file1 on master\nnew line\nAdd file2\nnew line\nAdd file1\nnew line",
+ head1Commit.getFullMessage());
+ }
+
+ @Test
+ public void testRebaseInteractiveMixedSquashAndFixup() throws Exception {
+ // create file0 on master
+ writeTrashFile("file0", "file0");
+ git.add().addFilepattern("file0").call();
+ git.commit().setMessage("Add file0\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), "file0").exists());
+
+ // create file1 on master
+ writeTrashFile(FILE1, FILE1);
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("Add file1\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), FILE1).exists());
+
+ // create file2 on master
+ writeTrashFile("file2", "file2");
+ git.add().addFilepattern("file2").call();
+ git.commit().setMessage("Add file2\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), "file2").exists());
+
+ // update FILE1 on master
+ writeTrashFile(FILE1, "blah");
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("updated file1 on master\nnew line").call();
+
+ writeTrashFile("file2", "more change");
+ git.add().addFilepattern("file2").call();
+ git.commit().setMessage("update file2 on master\nnew line").call();
+
+ git.rebase().setUpstream("HEAD~4")
+ .runInteractively(new InteractiveHandler() {
+
+ public void prepareSteps(List<RebaseTodoLine> steps) {
+ steps.get(1).setAction(Action.FIXUP);
+ steps.get(2).setAction(Action.SQUASH);
+ }
+
+ public String modifyCommitMessage(String commit) {
+ final File messageSquashFile = new File(db
+ .getDirectory(), "rebase-merge/message-squash");
+ final File messageFixupFile = new File(db
+ .getDirectory(), "rebase-merge/message-fixup");
+
+ assertFalse(messageFixupFile.exists());
+ assertTrue(messageSquashFile.exists());
+ assertEquals(
+ "# This is a combination of 3 commits.\n# This is the 3rd commit message:\nupdated file1 on master\nnew line\n# The 2nd commit message will be skipped:\n# Add file2\n# new line\n# The first commit's message is:\nAdd file1\nnew line",
+ commit);
+
+ try {
+ byte[] messageSquashBytes = IO
+ .readFully(messageSquashFile);
+ int end = RawParseUtils.prevLF(messageSquashBytes,
+ messageSquashBytes.length);
+ String messageSquashContend = RawParseUtils.decode(
+ messageSquashBytes, 0, end + 1);
+ assertEquals(messageSquashContend, commit);
+ } catch (Throwable t) {
+ fail(t.getMessage());
+ }
+
+ return "changed";
+ }
+ }).call();
+
+ RevWalk walk = new RevWalk(db);
+ ObjectId headId = db.resolve(Constants.HEAD);
+ RevCommit headCommit = walk.parseCommit(headId);
+ assertEquals(headCommit.getFullMessage(),
+ "update file2 on master\nnew line");
+
+ ObjectId head2Id = db.resolve(Constants.HEAD + "^1");
+ RevCommit head1Commit = walk.parseCommit(head2Id);
+ assertEquals("changed", head1Commit.getFullMessage());
+ }
+
+ @Test
+ public void testRebaseInteractiveSingleFixup() throws Exception {
+ // create file1 on master
+ writeTrashFile(FILE1, FILE1);
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("Add file1\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), FILE1).exists());
+
+ // create file2 on master
+ writeTrashFile("file2", "file2");
+ git.add().addFilepattern("file2").call();
+ git.commit().setMessage("Add file2\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), "file2").exists());
+
+ // update FILE1 on master
+ writeTrashFile(FILE1, "blah");
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("updated file1 on master\nnew line").call();
+
+ writeTrashFile("file2", "more change");
+ git.add().addFilepattern("file2").call();
+ git.commit().setMessage("update file2 on master\nnew line").call();
+
+ git.rebase().setUpstream("HEAD~3")
+ .runInteractively(new InteractiveHandler() {
+
+ public void prepareSteps(List<RebaseTodoLine> steps) {
+ steps.get(1).setAction(Action.FIXUP);
+ }
+
+ public String modifyCommitMessage(String commit) {
+ fail("No callback to modify commit message expected for single fixup");
+ return commit;
+ }
+ }).call();
+
+ RevWalk walk = new RevWalk(db);
+ ObjectId headId = db.resolve(Constants.HEAD);
+ RevCommit headCommit = walk.parseCommit(headId);
+ assertEquals("update file2 on master\nnew line",
+ headCommit.getFullMessage());
+
+ ObjectId head1Id = db.resolve(Constants.HEAD + "^1");
+ RevCommit head1Commit = walk.parseCommit(head1Id);
+ assertEquals("Add file2\nnew line",
+ head1Commit.getFullMessage());
+ }
+
+
+ @Test(expected = InvalidRebaseStepException.class)
+ public void testRebaseInteractiveFixupFirstCommitShouldFail()
+ throws Exception {
+ // create file1 on master
+ writeTrashFile(FILE1, FILE1);
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("Add file1\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), FILE1).exists());
+
+ // create file2 on master
+ writeTrashFile("file2", "file2");
+ git.add().addFilepattern("file2").call();
+ git.commit().setMessage("Add file2\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), "file2").exists());
+
+ git.rebase().setUpstream("HEAD~1")
+ .runInteractively(new InteractiveHandler() {
+
+ public void prepareSteps(List<RebaseTodoLine> steps) {
+ steps.get(0).setAction(Action.FIXUP);
+ }
+
+ public String modifyCommitMessage(String commit) {
+ return commit;
+ }
+ }).call();
+ }
+
+ @Test(expected = InvalidRebaseStepException.class)
+ public void testRebaseInteractiveSquashFirstCommitShouldFail()
+ throws Exception {
+ // create file1 on master
+ writeTrashFile(FILE1, FILE1);
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("Add file1\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), FILE1).exists());
+
+ // create file2 on master
+ writeTrashFile("file2", "file2");
+ git.add().addFilepattern("file2").call();
+ git.commit().setMessage("Add file2\nnew line").call();
+ assertTrue(new File(db.getWorkTree(), "file2").exists());
+
+ git.rebase().setUpstream("HEAD~1")
+ .runInteractively(new InteractiveHandler() {
+
+ public void prepareSteps(List<RebaseTodoLine> steps) {
+ steps.get(0).setAction(Action.SQUASH);
+ }
+
+ public String modifyCommitMessage(String commit) {
+ return commit;
+ }
+ }).call();
+ }
+
private File getTodoFile() {
File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO);
return todoFile;
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 5cf7af81d2..474b8f349c 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 e7c31da417..ef739bb050 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;
@@ -292,7 +302,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
RevCommit commitToPick = walk
.parseCommit(ids.iterator().next());
if (monitor.isCancelled())
- return new RebaseResult(commitToPick);
+ return new RebaseResult(commitToPick, Status.STOPPED);
try {
monitor.beginTask(MessageFormat.format(
JGitText.get().applyingCommit,
@@ -318,13 +328,14 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return abort(new RebaseResult(
cherryPickResult.getFailingPaths()));
else
- return stop(commitToPick);
+ return stop(commitToPick, Status.STOPPED);
case CONFLICTING:
- return stop(commitToPick);
+ return stop(commitToPick, Status.STOPPED);
case OK:
newHead = cherryPickResult.getNewHead();
}
}
+ boolean isSquash = false;
switch (step.getAction()) {
case PICK:
continue; // continue rebase process on pick command
@@ -337,9 +348,23 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
continue;
case EDIT:
rebaseState.createFile(AMEND, commitToPick.name());
- return stop(commitToPick);
+ return stop(commitToPick, Status.EDIT);
case COMMENT:
break;
+ case SQUASH:
+ isSquash = true;
+ //$FALL-THROUGH$
+ case FIXUP:
+ resetSoftToParent();
+ RebaseTodoLine nextStep = (i >= steps.size() - 1 ? null
+ : steps.get(i + 1));
+ File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP);
+ File messageSquashFile = rebaseState
+ .getFile(MESSAGE_SQUASH);
+ if (isSquash && messageFixupFile.exists())
+ messageFixupFile.delete();
+ newHead = doSquashFixup(isSquash, commitToPick,
+ nextStep, messageFixupFile, messageSquashFile);
}
} finally {
monitor.endTask();
@@ -361,6 +386,175 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
}
+ private void checkSteps(List<RebaseTodoLine> steps)
+ throws InvalidRebaseStepException, IOException {
+ if (steps.isEmpty())
+ return;
+ if (RebaseTodoLine.Action.SQUASH.equals(steps.get(0).getAction())
+ || RebaseTodoLine.Action.FIXUP.equals(steps.get(0).getAction())) {
+ if (!rebaseState.getFile(DONE).exists()
+ || rebaseState.readFile(DONE).trim().length() == 0) {
+ throw new InvalidRebaseStepException(MessageFormat.format(
+ JGitText.get().cannotSquashFixupWithoutPreviousCommit,
+ steps.get(0).getAction().name()));
+ }
+ }
+
+ }
+
+ private RevCommit doSquashFixup(boolean isSquash, RevCommit commitToPick,
+ RebaseTodoLine nextStep, File messageFixup, File messageSquash)
+ throws IOException, GitAPIException {
+
+ if (!messageSquash.exists()) {
+ // init squash/fixup sequence
+ ObjectId headId = repo.resolve(Constants.HEAD);
+ RevCommit previousCommit = walk.parseCommit(headId);
+
+ initializeSquashFixupFile(MESSAGE_SQUASH,
+ previousCommit.getFullMessage());
+ if (!isSquash)
+ initializeSquashFixupFile(MESSAGE_FIXUP,
+ previousCommit.getFullMessage());
+ }
+ String currSquashMessage = rebaseState
+ .readFile(MESSAGE_SQUASH);
+
+ int count = parseSquashFixupSequenceCount(currSquashMessage) + 1;
+
+ String content = composeSquashMessage(isSquash,
+ commitToPick, currSquashMessage, count);
+ rebaseState.createFile(MESSAGE_SQUASH, content);
+ if (messageFixup.exists())
+ rebaseState.createFile(MESSAGE_FIXUP, content);
+
+ return squashIntoPrevious(
+ !messageFixup.exists(),
+ nextStep);
+ }
+
+ private void resetSoftToParent() throws IOException,
+ GitAPIException, CheckoutConflictException {
+ Ref orig_head = repo.getRef(Constants.ORIG_HEAD);
+ ObjectId orig_headId = orig_head.getObjectId();
+ try {
+ // we have already commited the cherry-picked commit.
+ // what we need is to have changes introduced by this
+ // commit to be on the index
+ // resetting is a workaround
+ Git.wrap(repo).reset().setMode(ResetType.SOFT)
+ .setRef("HEAD~1").call(); //$NON-NLS-1$
+ } finally {
+ // set ORIG_HEAD back to where we started because soft
+ // reset moved it
+ repo.writeOrigHead(orig_headId);
+ }
+ }
+
+ private RevCommit squashIntoPrevious(boolean sequenceContainsSquash,
+ RebaseTodoLine nextStep)
+ throws IOException, GitAPIException {
+ RevCommit newHead;
+ String commitMessage = rebaseState
+ .readFile(MESSAGE_SQUASH);
+
+ if (nextStep == null
+ || ((nextStep.getAction() != Action.FIXUP) && (nextStep
+ .getAction() != Action.SQUASH))) {
+ // this is the last step in this sequence
+ if (sequenceContainsSquash) {
+ commitMessage = interactiveHandler
+ .modifyCommitMessage(commitMessage);
+ }
+ newHead = new Git(repo).commit()
+ .setMessage(stripCommentLines(commitMessage))
+ .setAmend(true).call();
+ rebaseState.getFile(MESSAGE_SQUASH).delete();
+ rebaseState.getFile(MESSAGE_FIXUP).delete();
+
+ } else {
+ // Next step is either Squash or Fixup
+ newHead = new Git(repo).commit()
+ .setMessage(commitMessage).setAmend(true)
+ .call();
+ }
+ return newHead;
+ }
+
+ private static String stripCommentLines(String commitMessage) {
+ StringBuilder result = new StringBuilder();
+ for (String line : commitMessage.split("\n")) { //$NON-NLS-1$
+ if (!line.trim().startsWith("#")) //$NON-NLS-1$
+ result.append(line).append("\n"); //$NON-NLS-1$
+ }
+ if (!commitMessage.endsWith("\n")) //$NON-NLS-1$
+ result.deleteCharAt(result.length() - 1);
+ return result.toString();
+ }
+
+ @SuppressWarnings("nls")
+ private static String composeSquashMessage(boolean isSquash,
+ RevCommit commitToPick, String currSquashMessage, int count) {
+ StringBuilder sb = new StringBuilder();
+ String ordinal = getOrdinal(count);
+ sb.setLength(0);
+ sb.append("# This is a combination of ").append(count)
+ .append(" commits.\n");
+ if (isSquash) {
+ sb.append("# This is the ").append(count).append(ordinal)
+ .append(" commit message:\n");
+ sb.append(commitToPick.getFullMessage());
+ } else {
+ sb.append("# The ").append(count).append(ordinal)
+ .append(" commit message will be skipped:\n# ");
+ sb.append(commitToPick.getFullMessage().replaceAll("([\n\r]+)",
+ "$1# "));
+ }
+ // Add the previous message without header (i.e first line)
+ sb.append("\n");
+ sb.append(currSquashMessage.substring(currSquashMessage.indexOf("\n") + 1));
+ return sb.toString();
+ }
+
+ private static String getOrdinal(int count) {
+ switch (count % 10) {
+ case 1:
+ return "st"; //$NON-NLS-1$
+ case 2:
+ return "nd"; //$NON-NLS-1$
+ case 3:
+ return "rd"; //$NON-NLS-1$
+ default:
+ return "th"; //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Parse the count from squashed commit messages
+ *
+ * @param currSquashMessage
+ * the squashed commit message to be parsed
+ * @return the count of squashed messages in the given string
+ */
+ static int parseSquashFixupSequenceCount(String currSquashMessage) {
+ String regex = "This is a combination of (.*) commits"; //$NON-NLS-1$
+ String firstLine = currSquashMessage.substring(0,
+ currSquashMessage.indexOf("\n")); //$NON-NLS-1$
+ Pattern pattern = Pattern.compile(regex);
+ Matcher matcher = pattern.matcher(firstLine);
+ if (!matcher.find())
+ throw new IllegalArgumentException();
+ return Integer.parseInt(matcher.group(1));
+ }
+
+ private void initializeSquashFixupFile(String messageFile,
+ String fullMessage) throws IOException {
+ rebaseState
+ .createFile(
+ messageFile,
+ "# This is a combination of 1 commits.\n# The first commit's message is:\n" + fullMessage); //$NON-NLS-1$);
+ }
+
private String getOurCommitName() {
// If onto is different from upstream, this should say "onto", but
// RebaseCommand doesn't support a different "onto" at the moment.
@@ -479,7 +673,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return parseAuthor(raw);
}
- private RebaseResult stop(RevCommit commitToPick) throws IOException {
+ private RebaseResult stop(RevCommit commitToPick, RebaseResult.Status status)
+ throws IOException {
PersonIdent author = commitToPick.getAuthorIdent();
String authorScript = toAuthorScript(author);
rebaseState.createFile(AUTHOR_SCRIPT, authorScript);
@@ -497,7 +692,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
// Remove cherry pick state file created by CherryPickCommand, it's not
// needed for rebase
repo.writeCherryPickHead(null);
- return new RebaseResult(commitToPick);
+ return new RebaseResult(commitToPick, status);
}
String toAuthorScript(PersonIdent author) {
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 ff18adce4f..6df5ffdd1d 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 class RebaseResult {
}
},
/**
+ * 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 @@ public class RebaseResult {
*
* @param commit
* current commit
+ * @param status
*/
- RebaseResult(RevCommit commit) {
- status = Status.STOPPED;
+ RebaseResult(RevCommit commit, RebaseResult.Status status) {
+ this.status = status;
currentCommit = commit;
}
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 0000000000..764725dcbb
--- /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 b4f99409bd..b2c27a483c 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 747c4d3fc4..8eeb1ea890 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