diff options
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 |