diff options
author | Tobias Pfeifer <to.pfeifer@web.de> | 2013-08-07 14:18:23 +0200 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2013-11-05 18:05:02 +0100 |
commit | 765896febb4b63cfe2c37dc0a73c90c79dd1591b (patch) | |
tree | f6c13b3c779d5d64a99a294fd960fb460cf20a6c /org.eclipse.jgit | |
parent | 01b5b999a567691fa9602792f365ba0dd5ae061f (diff) | |
download | jgit-765896febb4b63cfe2c37dc0a73c90c79dd1591b.tar.gz jgit-765896febb4b63cfe2c37dc0a73c90c79dd1591b.zip |
Add Squash/Fixup support for rebase interactive in RebaseCommand
The rebase command now supports squash and fixup. Both actions are not
allowed as the first step of the rebase.
In JGit, before any rebase step is performed, the next commit is
already cherry-picked. This commit keeps that behaviour. In case of
squash or fixup a soft reset to the parent is perfomed afterwards.
CQ: 7684
Bug: 396510
Change-Id: I3c4190940b4d7f19860e223d647fc78705e57203
Signed-off-by: Tobias Pfeifer <to.pfeifer@web.de>
Signed-off-by: Stefan Lay <stefan.lay@sap.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Diffstat (limited to 'org.eclipse.jgit')
5 files changed, 262 insertions, 2 deletions
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..1feb3f2090 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; @@ -325,6 +335,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { newHead = cherryPickResult.getNewHead(); } } + boolean isSquash = false; switch (step.getAction()) { case PICK: continue; // continue rebase process on pick command @@ -340,6 +351,20 @@ public class RebaseCommand extends GitCommand<RebaseResult> { return stop(commitToPick); case COMMENT: break; + case SQUASH: + isSquash = true; + //$FALL-THROUGH$ + case FIXUP: + resetSoftToParent(); + RebaseTodoLine nextStep = (i >= steps.size() - 1 ? null + : steps.get(i + 1)); + File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP); + File messageSquashFile = rebaseState + .getFile(MESSAGE_SQUASH); + if (isSquash && messageFixupFile.exists()) + messageFixupFile.delete(); + newHead = doSquashFixup(isSquash, commitToPick, + nextStep, messageFixupFile, messageSquashFile); } } finally { monitor.endTask(); @@ -361,6 +386,175 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } } + private void checkSteps(List<RebaseTodoLine> steps) + throws InvalidRebaseStepException, IOException { + if (steps.isEmpty()) + return; + if (RebaseTodoLine.Action.SQUASH.equals(steps.get(0).getAction()) + || RebaseTodoLine.Action.FIXUP.equals(steps.get(0).getAction())) { + if (!rebaseState.getFile(DONE).exists() + || rebaseState.readFile(DONE).trim().length() == 0) { + throw new InvalidRebaseStepException(MessageFormat.format( + JGitText.get().cannotSquashFixupWithoutPreviousCommit, + steps.get(0).getAction().name())); + } + } + + } + + private RevCommit doSquashFixup(boolean isSquash, RevCommit commitToPick, + RebaseTodoLine nextStep, File messageFixup, File messageSquash) + throws IOException, GitAPIException { + + if (!messageSquash.exists()) { + // init squash/fixup sequence + ObjectId headId = repo.resolve(Constants.HEAD); + RevCommit previousCommit = walk.parseCommit(headId); + + initializeSquashFixupFile(MESSAGE_SQUASH, + previousCommit.getFullMessage()); + if (!isSquash) + initializeSquashFixupFile(MESSAGE_FIXUP, + previousCommit.getFullMessage()); + } + String currSquashMessage = rebaseState + .readFile(MESSAGE_SQUASH); + + int count = parseSquashFixupSequenceCount(currSquashMessage) + 1; + + String content = composeSquashMessage(isSquash, + commitToPick, currSquashMessage, count); + rebaseState.createFile(MESSAGE_SQUASH, content); + if (messageFixup.exists()) + rebaseState.createFile(MESSAGE_FIXUP, content); + + return squashIntoPrevious( + !messageFixup.exists(), + nextStep); + } + + private void resetSoftToParent() throws IOException, + GitAPIException, CheckoutConflictException { + Ref orig_head = repo.getRef(Constants.ORIG_HEAD); + ObjectId orig_headId = orig_head.getObjectId(); + try { + // we have already commited the cherry-picked commit. + // what we need is to have changes introduced by this + // commit to be on the index + // resetting is a workaround + Git.wrap(repo).reset().setMode(ResetType.SOFT) + .setRef("HEAD~1").call(); //$NON-NLS-1$ + } finally { + // set ORIG_HEAD back to where we started because soft + // reset moved it + repo.writeOrigHead(orig_headId); + } + } + + private RevCommit squashIntoPrevious(boolean sequenceContainsSquash, + RebaseTodoLine nextStep) + throws IOException, GitAPIException { + RevCommit newHead; + String commitMessage = rebaseState + .readFile(MESSAGE_SQUASH); + + if (nextStep == null + || ((nextStep.getAction() != Action.FIXUP) && (nextStep + .getAction() != Action.SQUASH))) { + // this is the last step in this sequence + if (sequenceContainsSquash) { + commitMessage = interactiveHandler + .modifyCommitMessage(commitMessage); + } + newHead = new Git(repo).commit() + .setMessage(stripCommentLines(commitMessage)) + .setAmend(true).call(); + rebaseState.getFile(MESSAGE_SQUASH).delete(); + rebaseState.getFile(MESSAGE_FIXUP).delete(); + + } else { + // Next step is either Squash or Fixup + newHead = new Git(repo).commit() + .setMessage(commitMessage).setAmend(true) + .call(); + } + return newHead; + } + + private static String stripCommentLines(String commitMessage) { + StringBuilder result = new StringBuilder(); + for (String line : commitMessage.split("\n")) { //$NON-NLS-1$ + if (!line.trim().startsWith("#")) //$NON-NLS-1$ + result.append(line).append("\n"); //$NON-NLS-1$ + } + if (!commitMessage.endsWith("\n")) //$NON-NLS-1$ + result.deleteCharAt(result.length() - 1); + return result.toString(); + } + + @SuppressWarnings("nls") + private static String composeSquashMessage(boolean isSquash, + RevCommit commitToPick, String currSquashMessage, int count) { + StringBuilder sb = new StringBuilder(); + String ordinal = getOrdinal(count); + sb.setLength(0); + sb.append("# This is a combination of ").append(count) + .append(" commits.\n"); + if (isSquash) { + sb.append("# This is the ").append(count).append(ordinal) + .append(" commit message:\n"); + sb.append(commitToPick.getFullMessage()); + } else { + sb.append("# The ").append(count).append(ordinal) + .append(" commit message will be skipped:\n# "); + sb.append(commitToPick.getFullMessage().replaceAll("([\n\r]+)", + "$1# ")); + } + // Add the previous message without header (i.e first line) + sb.append("\n"); + sb.append(currSquashMessage.substring(currSquashMessage.indexOf("\n") + 1)); + return sb.toString(); + } + + private static String getOrdinal(int count) { + switch (count % 10) { + case 1: + return "st"; //$NON-NLS-1$ + case 2: + return "nd"; //$NON-NLS-1$ + case 3: + return "rd"; //$NON-NLS-1$ + default: + return "th"; //$NON-NLS-1$ + } + } + + /** + * Parse the count from squashed commit messages + * + * @param currSquashMessage + * the squashed commit message to be parsed + * @return the count of squashed messages in the given string + */ + static int parseSquashFixupSequenceCount(String currSquashMessage) { + String regex = "This is a combination of (.*) commits"; //$NON-NLS-1$ + String firstLine = currSquashMessage.substring(0, + currSquashMessage.indexOf("\n")); //$NON-NLS-1$ + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(firstLine); + if (!matcher.find()) + throw new IllegalArgumentException(); + return Integer.parseInt(matcher.group(1)); + } + + private void initializeSquashFixupFile(String messageFile, + String fullMessage) throws IOException { + rebaseState + .createFile( + messageFile, + "# This is a combination of 1 commits.\n# The first commit's message is:\n" + fullMessage); //$NON-NLS-1$); + } + private String getOurCommitName() { // If onto is different from upstream, this should say "onto", but // RebaseCommand doesn't support a different "onto" at the moment. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRebaseStepException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRebaseStepException.java new file mode 100644 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 |