aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit')
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java196
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRebaseStepException.java60
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java6
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