diff options
author | Matthias Sohn <matthias.sohn@sap.com> | 2013-11-05 17:03:35 -0500 |
---|---|---|
committer | Gerrit Code Review @ Eclipse.org <gerrit@eclipse.org> | 2013-11-05 17:03:35 -0500 |
commit | 34fbd814d40a18f8be57e3d8a766e854f2fe2d00 (patch) | |
tree | c6704e24f935fa998a29f8ee445bd26230284e1f | |
parent | b8eac43c0f57e3f1829ab6981bfe2aeeb39e78c5 (diff) | |
parent | cce2561e9fe2ce1cf60182f9d95c8537ce13de92 (diff) | |
download | jgit-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
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 |