*/
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;
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;
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;
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;
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;
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
*/
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;
newHead = cherryPickResult.getNewHead();
}
}
+ boolean isSquash = false;
switch (step.getAction()) {
case PICK:
continue; // continue rebase process on pick command
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();
}
}
+ 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.