Browse Source

Merge changes I40f2311c,I3c419094

* changes:
  Add additional RebaseResult for editing commits
  Add Squash/Fixup support for rebase interactive in RebaseCommand
stable-3.2
Matthias Sohn 11 years ago committed by Gerrit Code Review @ Eclipse.org
parent
commit
34fbd814d4
  1. 341
      org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
  2. 1
      org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
  3. 209
      org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
  4. 14
      org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java
  5. 60
      org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRebaseStepException.java
  6. 1
      org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
  7. 6
      org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java

341
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java

@ -42,9 +42,9 @@
*/ */
package org.eclipse.jgit.api; package org.eclipse.jgit.api;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; 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.InteractiveHandler;
import org.eclipse.jgit.api.RebaseCommand.Operation; import org.eclipse.jgit.api.RebaseCommand.Operation;
import org.eclipse.jgit.api.RebaseResult.Status; 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.JGitInternalException;
import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.api.errors.RefNotFoundException;
import org.eclipse.jgit.api.errors.UnmergedPathsException; 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.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.FileUtils; 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.Before;
import org.junit.Test; import org.junit.Test;
@ -1938,7 +1941,7 @@ public class RebaseCommandTest extends RepositoryTestCase {
return ""; // not used return ""; // not used
} }
}).call(); }).call();
assertEquals(Status.STOPPED, res.getStatus()); assertEquals(Status.EDIT, res.getStatus());
RevCommit toBeEditted = git.log().call().iterator().next(); RevCommit toBeEditted = git.log().call().iterator().next();
assertEquals("updated file1 on master", toBeEditted.getFullMessage()); assertEquals("updated file1 on master", toBeEditted.getFullMessage());
@ -1957,6 +1960,340 @@ public class RebaseCommandTest extends RepositoryTestCase {
assertEquals("edited commit message", actualCommitMag); 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() { private File getTodoFile() {
File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO); File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO);
return todoFile; return todoFile;

1
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties

@ -78,6 +78,7 @@ cannotReadObject=Cannot read object
cannotReadTree=Cannot read tree {0} cannotReadTree=Cannot read tree {0}
cannotRebaseWithoutCurrentHead=Can not rebase without a current HEAD cannotRebaseWithoutCurrentHead=Can not rebase without a current HEAD
cannotResolveLocalTrackingRefForUpdating=Cannot resolve local tracking ref {0} for updating. cannotResolveLocalTrackingRefForUpdating=Cannot resolve local tracking ref {0} for updating.
cannotSquashFixupWithoutPreviousCommit=Cannot {0} without previous commit.
cannotStoreObjects=cannot store objects cannotStoreObjects=cannot store objects
cannotUnloadAModifiedTree=Cannot unload a modified tree. cannotUnloadAModifiedTree=Cannot unload a modified tree.
cannotWorkWithOtherStagesThanZeroRightNow=Cannot work with other stages than zero right now. Won't write corrupt index. cannotWorkWithOtherStagesThanZeroRightNow=Cannot work with other stages than zero right now. Won't write corrupt index.

209
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.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; 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.RebaseResult.Status;
import org.eclipse.jgit.api.ResetCommand.ResetType;
import org.eclipse.jgit.api.errors.CheckoutConflictException; import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.api.errors.GitAPIException; 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.InvalidRefNameException;
import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.NoHeadException; 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 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 * The available operations
*/ */
@ -281,7 +289,9 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO), repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
steps, false); steps, false);
} }
for (RebaseTodoLine step : steps) { checkSteps(steps);
for (int i = 0; i < steps.size(); i++) {
RebaseTodoLine step = steps.get(i);
popSteps(1); popSteps(1);
if (Action.COMMENT.equals(step.getAction())) if (Action.COMMENT.equals(step.getAction()))
continue; continue;
@ -292,7 +302,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
RevCommit commitToPick = walk RevCommit commitToPick = walk
.parseCommit(ids.iterator().next()); .parseCommit(ids.iterator().next());
if (monitor.isCancelled()) if (monitor.isCancelled())
return new RebaseResult(commitToPick); return new RebaseResult(commitToPick, Status.STOPPED);
try { try {
monitor.beginTask(MessageFormat.format( monitor.beginTask(MessageFormat.format(
JGitText.get().applyingCommit, JGitText.get().applyingCommit,
@ -318,13 +328,14 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return abort(new RebaseResult( return abort(new RebaseResult(
cherryPickResult.getFailingPaths())); cherryPickResult.getFailingPaths()));
else else
return stop(commitToPick); return stop(commitToPick, Status.STOPPED);
case CONFLICTING: case CONFLICTING:
return stop(commitToPick); return stop(commitToPick, Status.STOPPED);
case OK: case OK:
newHead = cherryPickResult.getNewHead(); newHead = cherryPickResult.getNewHead();
} }
} }
boolean isSquash = false;
switch (step.getAction()) { switch (step.getAction()) {
case PICK: case PICK:
continue; // continue rebase process on pick command continue; // continue rebase process on pick command
@ -337,9 +348,23 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
continue; continue;
case EDIT: case EDIT:
rebaseState.createFile(AMEND, commitToPick.name()); rebaseState.createFile(AMEND, commitToPick.name());
return stop(commitToPick); return stop(commitToPick, Status.EDIT);
case COMMENT: case COMMENT:
break; 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 { } finally {
monitor.endTask(); 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() { private String getOurCommitName() {
// If onto is different from upstream, this should say "onto", but // If onto is different from upstream, this should say "onto", but
// RebaseCommand doesn't support a different "onto" at the moment. // RebaseCommand doesn't support a different "onto" at the moment.
@ -479,7 +673,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return parseAuthor(raw); return parseAuthor(raw);
} }
private RebaseResult stop(RevCommit commitToPick) throws IOException { private RebaseResult stop(RevCommit commitToPick, RebaseResult.Status status)
throws IOException {
PersonIdent author = commitToPick.getAuthorIdent(); PersonIdent author = commitToPick.getAuthorIdent();
String authorScript = toAuthorScript(author); String authorScript = toAuthorScript(author);
rebaseState.createFile(AUTHOR_SCRIPT, authorScript); 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 // Remove cherry pick state file created by CherryPickCommand, it's not
// needed for rebase // needed for rebase
repo.writeCherryPickHead(null); repo.writeCherryPickHead(null);
return new RebaseResult(commitToPick); return new RebaseResult(commitToPick, status);
} }
String toAuthorScript(PersonIdent author) { String toAuthorScript(PersonIdent author) {

14
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java

@ -84,6 +84,15 @@ public class RebaseResult {
return false; return false;
} }
}, },
/**
* Stopped for editing in the context of an interactive rebase
*/
EDIT {
@Override
public boolean isSuccessful() {
return false;
}
},
/** /**
* Failed; the original HEAD was restored * Failed; the original HEAD was restored
*/ */
@ -183,9 +192,10 @@ public class RebaseResult {
* *
* @param commit * @param commit
* current commit * current commit
* @param status
*/ */
RebaseResult(RevCommit commit) { RebaseResult(RevCommit commit, RebaseResult.Status status) {
status = Status.STOPPED; this.status = status;
currentCommit = commit; currentCommit = commit;
} }

60
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);
}
}

1
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java

@ -140,6 +140,7 @@ public class JGitText extends TranslationBundle {
/***/ public String cannotReadTree; /***/ public String cannotReadTree;
/***/ public String cannotRebaseWithoutCurrentHead; /***/ public String cannotRebaseWithoutCurrentHead;
/***/ public String cannotResolveLocalTrackingRefForUpdating; /***/ public String cannotResolveLocalTrackingRefForUpdating;
/***/ public String cannotSquashFixupWithoutPreviousCommit;
/***/ public String cannotStoreObjects; /***/ public String cannotStoreObjects;
/***/ public String cannotUnloadAModifiedTree; /***/ public String cannotUnloadAModifiedTree;
/***/ public String cannotWorkWithOtherStagesThanZeroRightNow; /***/ public String cannotWorkWithOtherStagesThanZeroRightNow;

6
org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java

@ -66,7 +66,11 @@ public class RebaseTodoLine {
/** Use commit, but stop for amending */ /** Use commit, but stop for amending */
EDIT("edit", "e"), 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 * A comment in the file. Also blank lines (or lines containing only

Loading…
Cancel
Save