aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java')
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java745
1 files changed, 498 insertions, 247 deletions
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 a11822975a..3ae7a6c81e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -1,64 +1,36 @@
/*
* Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com>
- * and other copyright owners as documented in the project's IP log.
+ * Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> and others
*
- * 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
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://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.
+ * SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.api;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.MessageFormat;
+import java.time.Instant;
+import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
-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.annotations.NonNull;
import org.eclipse.jgit.api.RebaseResult.Status;
import org.eclipse.jgit.api.ResetCommand.ResetType;
import org.eclipse.jgit.api.errors.CheckoutConflictException;
@@ -82,6 +54,8 @@ import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitConfig;
+import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -95,8 +69,10 @@ import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.submodule.SubmoduleWalk.IgnoreSubmoduleMode;
@@ -111,7 +87,6 @@ import org.eclipse.jgit.util.RawParseUtils;
* supported options and arguments of this command and a {@link #call()} method
* to finally execute the command. Each instance of this class should only be
* used for one invocation of the command (means: one call to {@link #call()})
- * <p>
*
* @see <a
* href="http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html"
@@ -155,11 +130,14 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
private static final String ONTO = "onto"; //$NON-NLS-1$
- private static final String ONTO_NAME = "onto-name"; //$NON-NLS-1$
+ private static final String ONTO_NAME = "onto_name"; //$NON-NLS-1$
private static final String PATCH = "patch"; //$NON-NLS-1$
- private static final String REBASE_HEAD = "head"; //$NON-NLS-1$
+ private static final String REBASE_HEAD = "orig-head"; //$NON-NLS-1$
+
+ /** Pre git 1.7.6 file name for {@link #REBASE_HEAD}. */
+ private static final String REBASE_HEAD_LEGACY = "head"; //$NON-NLS-1$
private static final String AMEND = "amend"; //$NON-NLS-1$
@@ -173,12 +151,16 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
/**
* The folder containing the hashes of (potentially) rewritten commits when
- * --preserve-merges is used.
+ * --rebase-merges is used.
+ * <p>
+ * Native git rebase --merge uses a <em>file</em> of that name to record
+ * commits to copy notes at the end of the whole rebase.
+ * </p>
*/
private static final String REWRITTEN = "rewritten"; //$NON-NLS-1$
/**
- * File containing the current commit(s) to cherry pick when --preserve-merges
+ * File containing the current commit(s) to cherry pick when --rebase-merges
* is used.
*/
private static final String CURRENT_COMMIT = "current-commit"; //$NON-NLS-1$
@@ -226,6 +208,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
private InteractiveHandler interactiveHandler;
+ private CommitConfig commitConfig;
+
private boolean stopAfterInitialization = false;
private RevCommit newHead;
@@ -234,10 +218,17 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
private MergeStrategy strategy = MergeStrategy.RECURSIVE;
+ private ContentMergeStrategy contentStrategy;
+
private boolean preserveMerges = false;
/**
+ * <p>
+ * Constructor for RebaseCommand.
+ * </p>
+ *
* @param repo
+ * the {@link org.eclipse.jgit.lib.Repository}
*/
protected RebaseCommand(Repository repo) {
super(repo);
@@ -246,23 +237,21 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
+ * {@inheritDoc}
+ * <p>
* Executes the {@code Rebase} command with all the options and parameters
* collected by the setter methods of this class. Each instance of this
* class should only be used for one invocation of the command. Don't call
* this method twice on an instance.
- *
- * @return an object describing the result of this command
- * @throws GitAPIException
- * @throws WrongRepositoryStateException
- * @throws NoHeadException
- * @throws RefNotFoundException
*/
+ @Override
public RebaseResult call() throws GitAPIException, NoHeadException,
RefNotFoundException, WrongRepositoryStateException {
newHead = null;
lastStepWasForward = false;
checkCallable();
checkParameters();
+ commitConfig = repo.getConfig().get(CommitConfig.KEY);
try {
switch (operation) {
case ABORT:
@@ -272,9 +261,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
throw new JGitInternalException(ioe.getMessage(), ioe);
}
case PROCESS_STEPS:
- // fall through
case SKIP:
- // fall through
case CONTINUE:
String upstreamCommitId = rebaseState.readFile(ONTO);
try {
@@ -286,7 +273,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
this.upstreamCommit = walk.parseCommit(repo
.resolve(upstreamCommitId));
- preserveMerges = rebaseState.getRewrittenDir().exists();
+ preserveMerges = rebaseState.getRewrittenDir().isDirectory();
break;
case BEGIN:
autoStash();
@@ -297,19 +284,23 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
org.eclipse.jgit.api.Status status = Git.wrap(repo)
.status().setIgnoreSubmodules(IgnoreSubmoduleMode.ALL).call();
if (status.hasUncommittedChanges()) {
- List<String> list = new ArrayList<String>();
+ List<String> list = new ArrayList<>();
list.addAll(status.getUncommittedChanges());
return RebaseResult.uncommittedChanges(list);
}
}
RebaseResult res = initFilesAndRewind();
- if (stopAfterInitialization)
+ if (stopAfterInitialization) {
return RebaseResult.INTERACTIVE_PREPARED_RESULT;
+ }
if (res != null) {
- autoStashApply();
- if (rebaseState.getDir().exists())
+ if (!autoStashApply()) {
+ res = RebaseResult.STASH_APPLY_CONFLICTS_RESULT;
+ }
+ if (rebaseState.getDir().exists()) {
FileUtils.delete(rebaseState.getDir(),
FileUtils.RECURSIVE);
+ }
return res;
}
}
@@ -352,7 +343,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
List<RebaseTodoLine> steps = repo.readRebaseTodo(
rebaseState.getPath(GIT_REBASE_TODO), false);
- if (steps.size() == 0) {
+ if (steps.isEmpty()) {
return finishRebase(walk.parseCommit(repo.resolve(Constants.HEAD)), false);
}
if (isInteractive()) {
@@ -361,8 +352,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
steps, false);
}
checkSteps(steps);
- for (int i = 0; i < steps.size(); i++) {
- RebaseTodoLine step = steps.get(i);
+ for (RebaseTodoLine step : steps) {
popSteps(1);
RebaseResult result = processStep(step, true);
if (result != null) {
@@ -396,15 +386,15 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
private boolean autoStashApply() throws IOException, GitAPIException {
- boolean conflicts = false;
+ boolean success = true;
if (rebaseState.getFile(AUTOSTASH).exists()) {
String stash = rebaseState.readFile(AUTOSTASH);
- try {
- Git.wrap(repo).stashApply().setStashRef(stash)
+ try (Git git = Git.wrap(repo)) {
+ git.stashApply().setStashRef(stash)
.ignoreRepositoryState(true).setStrategy(strategy)
.call();
} catch (StashApplyFailureException e) {
- conflicts = true;
+ success = false;
try (RevWalk rw = new RevWalk(repo)) {
ObjectId stashId = repo.resolve(stash);
RevCommit commit = rw.parseCommit(stashId);
@@ -413,16 +403,17 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
}
}
- return conflicts;
+ return success;
}
private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent,
String refLogMessage) throws IOException {
- Ref currentRef = repo.getRef(Constants.R_STASH);
+ Ref currentRef = repo.exactRef(Constants.R_STASH);
RefUpdate refUpdate = repo.updateRef(Constants.R_STASH);
refUpdate.setNewObjectId(commitId);
refUpdate.setRefLogIdent(refLogIdent);
refUpdate.setRefLogMessage(refLogMessage, false);
+ refUpdate.setForceRefLog(true);
if (currentRef != null)
refUpdate.setExpectedOldObjectId(currentRef.getObjectId());
else
@@ -445,7 +436,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
Collection<ObjectId> ids = or.resolve(step.getCommit());
if (ids.size() != 1)
throw new JGitInternalException(
- "Could not resolve uniquely the abbreviated object ID");
+ JGitText.get().cannotResolveUniquelyAbbrevObjectId);
RevCommit commitToPick = walk.parseCommit(ids.iterator().next());
if (shouldPick) {
if (monitor.isCancelled())
@@ -460,10 +451,18 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return null; // continue rebase process on pick command
case REWORD:
String oldMessage = commitToPick.getFullMessage();
- String newMessage = interactiveHandler
- .modifyCommitMessage(oldMessage);
- newHead = new Git(repo).commit().setMessage(newMessage)
- .setAmend(true).setNoVerify(true).call();
+ CleanupMode mode = commitConfig.resolve(CleanupMode.DEFAULT, true);
+ boolean[] doChangeId = { false };
+ String newMessage = editCommitMessage(doChangeId, oldMessage, mode,
+ commitConfig.getCommentChar(oldMessage));
+ try (Git git = new Git(repo)) {
+ newHead = git.commit()
+ .setMessage(newMessage)
+ .setAmend(true)
+ .setNoVerify(true)
+ .setInsertChangeId(doChangeId[0])
+ .call();
+ }
return null;
case EDIT:
rebaseState.createFile(AMEND, commitToPick.name());
@@ -477,17 +476,49 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
resetSoftToParent();
List<RebaseTodoLine> steps = repo.readRebaseTodo(
rebaseState.getPath(GIT_REBASE_TODO), false);
- RebaseTodoLine nextStep = steps.size() > 0 ? steps.get(0) : null;
+ boolean isLast = steps.isEmpty();
+ if (!isLast) {
+ switch (steps.get(0).getAction()) {
+ case FIXUP:
+ case SQUASH:
+ break;
+ default:
+ isLast = true;
+ break;
+ }
+ }
File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP);
File messageSquashFile = rebaseState.getFile(MESSAGE_SQUASH);
- if (isSquash && messageFixupFile.exists())
+ if (isSquash && messageFixupFile.exists()) {
messageFixupFile.delete();
- newHead = doSquashFixup(isSquash, commitToPick, nextStep,
+ }
+ newHead = doSquashFixup(isSquash, commitToPick, isLast,
messageFixupFile, messageSquashFile);
}
return null;
}
+ private String editCommitMessage(boolean[] doChangeId, String message,
+ @NonNull CleanupMode mode, char commentChar) {
+ String newMessage;
+ CommitConfig.CleanupMode cleanup;
+ if (interactiveHandler instanceof InteractiveHandler2) {
+ InteractiveHandler2.ModifyResult modification = ((InteractiveHandler2) interactiveHandler)
+ .editCommitMessage(message, mode, commentChar);
+ newMessage = modification.getMessage();
+ cleanup = modification.getCleanupMode();
+ if (CleanupMode.DEFAULT.equals(cleanup)) {
+ cleanup = mode;
+ }
+ doChangeId[0] = modification.shouldAddChangeId();
+ } else {
+ newMessage = interactiveHandler.modifyCommitMessage(message);
+ cleanup = CommitConfig.CleanupMode.STRIP;
+ doChangeId[0] = false;
+ }
+ return CommitConfig.cleanText(newMessage, cleanup, commentChar);
+ }
+
private RebaseResult cherryPickCommit(RevCommit commitToPick)
throws IOException, GitAPIException, NoMessageException,
UnmergedPathsException, ConcurrentRefUpdateException,
@@ -496,10 +527,10 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
monitor.beginTask(MessageFormat.format(
JGitText.get().applyingCommit,
commitToPick.getShortMessage()), ProgressMonitor.UNKNOWN);
- if (preserveMerges)
+ if (preserveMerges) {
return cherryPickCommitPreservingMerges(commitToPick);
- else
- return cherryPickCommitFlattening(commitToPick);
+ }
+ return cherryPickCommitFlattening(commitToPick);
} finally {
monitor.endTask();
}
@@ -521,16 +552,19 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
String ourCommitName = getOurCommitName();
try (Git git = new Git(repo)) {
CherryPickResult cherryPickResult = git.cherryPick()
- .include(commitToPick).setOurCommitName(ourCommitName)
- .setReflogPrefix(REFLOG_PREFIX).setStrategy(strategy)
+ .include(commitToPick)
+ .setOurCommitName(ourCommitName)
+ .setReflogPrefix(REFLOG_PREFIX)
+ .setStrategy(strategy)
+ .setContentMergeStrategy(contentStrategy)
.call();
switch (cherryPickResult.getStatus()) {
case FAILED:
- if (operation == Operation.BEGIN)
+ if (operation == Operation.BEGIN) {
return abort(RebaseResult
.failed(cherryPickResult.getFailingPaths()));
- else
- return stop(commitToPick, Status.STOPPED);
+ }
+ return stop(commitToPick, Status.STOPPED);
case CONFLICTING:
return stop(commitToPick, Status.STOPPED);
case OK:
@@ -560,7 +594,9 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
lastStepWasForward = newHead != null;
if (!lastStepWasForward) {
ObjectId headId = getHead().getObjectId();
- if (!AnyObjectId.equals(headId, newParents.get(0)))
+ // getHead() checks for null
+ assert headId != null;
+ if (!AnyObjectId.isEqual(headId, newParents.get(0)))
checkoutCommit(headId.getName(), newParents.get(0));
// Use the cherry-pick strategy if all non-first parents did not
@@ -574,7 +610,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
.include(commitToPick)
.setOurCommitName(ourCommitName)
.setReflogPrefix(REFLOG_PREFIX)
- .setStrategy(strategy);
+ .setStrategy(strategy)
+ .setContentMergeStrategy(contentStrategy);
if (isMerge) {
pickCommand.setMainlineParentNumber(1);
// We write a MERGE_HEAD and later commit explicitly
@@ -584,11 +621,11 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
CherryPickResult cherryPickResult = pickCommand.call();
switch (cherryPickResult.getStatus()) {
case FAILED:
- if (operation == Operation.BEGIN)
+ if (operation == Operation.BEGIN) {
return abort(RebaseResult.failed(
cherryPickResult.getFailingPaths()));
- else
- return stop(commitToPick, Status.STOPPED);
+ }
+ return stop(commitToPick, Status.STOPPED);
case CONFLICTING:
return stop(commitToPick, Status.STOPPED);
case OK:
@@ -609,6 +646,9 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
// their non-first parents rewritten
MergeCommand merge = git.merge()
.setFastForward(MergeCommand.FastForwardMode.NO_FF)
+ .setProgressMonitor(monitor)
+ .setStrategy(strategy)
+ .setContentMergeStrategy(contentStrategy)
.setCommit(false);
for (int i = 1; i < commitToPick.getParentCount(); i++)
merge.include(newParents.get(i));
@@ -643,7 +683,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
// Get the rewritten equivalents for the parents of the given commit
private List<RevCommit> getNewParents(RevCommit commitToPick)
throws IOException {
- List<RevCommit> newParents = new ArrayList<RevCommit>();
+ List<RevCommit> newParents = new ArrayList<>();
for (int p = 0; p < commitToPick.getParentCount(); p++) {
String parentHash = commitToPick.getParent(p).getName();
if (!new File(rebaseState.getRewrittenDir(), parentHash).exists())
@@ -668,12 +708,15 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
private void writeRewrittenHashes() throws RevisionSyntaxException,
- IOException {
+ IOException, RefNotFoundException {
File currentCommitFile = rebaseState.getFile(CURRENT_COMMIT);
if (!currentCommitFile.exists())
return;
- String head = repo.resolve(Constants.HEAD).getName();
+ ObjectId headId = getHead().getObjectId();
+ // getHead() checks for null
+ assert headId != null;
+ String head = headId.getName();
String currentCommits = rebaseState.readFile(CURRENT_COMMIT);
for (String current : currentCommits.split("\n")) //$NON-NLS-1$
RebaseState
@@ -685,12 +728,15 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
boolean lastStepIsForward) throws IOException, GitAPIException {
String headName = rebaseState.readFile(HEAD_NAME);
updateHead(headName, finalHead, upstreamCommit);
- boolean stashConflicts = autoStashApply();
+ boolean unstashSuccessful = autoStashApply();
+ getRepository().autoGC(monitor);
FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE);
- if (stashConflicts)
+ if (!unstashSuccessful) {
return RebaseResult.STASH_APPLY_CONFLICTS_RESULT;
- if (lastStepIsForward || finalHead == null)
+ }
+ if (lastStepIsForward || finalHead == null) {
return RebaseResult.FAST_FORWARD_RESULT;
+ }
return RebaseResult.OK_RESULT;
}
@@ -711,7 +757,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
private RevCommit doSquashFixup(boolean isSquash, RevCommit commitToPick,
- RebaseTodoLine nextStep, File messageFixup, File messageSquash)
+ boolean isLast, File messageFixup, File messageSquash)
throws IOException, GitAPIException {
if (!messageSquash.exists()) {
@@ -721,65 +767,66 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
initializeSquashFixupFile(MESSAGE_SQUASH,
previousCommit.getFullMessage());
- if (!isSquash)
- initializeSquashFixupFile(MESSAGE_FIXUP,
- previousCommit.getFullMessage());
+ if (!isSquash) {
+ rebaseState.createFile(MESSAGE_FIXUP,
+ previousCommit.getFullMessage());
+ }
}
- String currSquashMessage = rebaseState
- .readFile(MESSAGE_SQUASH);
+ 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);
+ return squashIntoPrevious(!messageFixup.exists(), isLast);
}
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.
+ Ref ref = repo.exactRef(Constants.ORIG_HEAD);
+ ObjectId orig_head = ref == null ? null : ref.getObjectId();
+ try (Git git = Git.wrap(repo)) {
+ // we have already committed 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)
+ git.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);
+ repo.writeOrigHead(orig_head);
}
}
private RevCommit squashIntoPrevious(boolean sequenceContainsSquash,
- RebaseTodoLine nextStep)
+ boolean isLast)
throws IOException, GitAPIException {
RevCommit retNewHead;
- String commitMessage = rebaseState
- .readFile(MESSAGE_SQUASH);
-
+ String commitMessage;
+ if (!isLast || sequenceContainsSquash) {
+ commitMessage = rebaseState.readFile(MESSAGE_SQUASH);
+ } else {
+ commitMessage = rebaseState.readFile(MESSAGE_FIXUP);
+ }
try (Git git = new Git(repo)) {
- if (nextStep == null || ((nextStep.getAction() != Action.FIXUP)
- && (nextStep.getAction() != Action.SQUASH))) {
- // this is the last step in this sequence
+ if (isLast) {
+ boolean[] doChangeId = { false };
if (sequenceContainsSquash) {
- commitMessage = interactiveHandler
- .modifyCommitMessage(commitMessage);
+ char commentChar = commitMessage.charAt(0);
+ commitMessage = editCommitMessage(doChangeId, commitMessage,
+ CleanupMode.STRIP, commentChar);
}
retNewHead = git.commit()
- .setMessage(stripCommentLines(commitMessage))
- .setAmend(true).setNoVerify(true).call();
+ .setMessage(commitMessage)
+ .setAmend(true)
+ .setNoVerify(true)
+ .setInsertChangeId(doChangeId[0])
+ .call();
rebaseState.getFile(MESSAGE_SQUASH).delete();
rebaseState.getFile(MESSAGE_FIXUP).delete();
-
} else {
// Next step is either Squash or Fixup
retNewHead = git.commit().setMessage(commitMessage)
@@ -789,41 +836,61 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return retNewHead;
}
- 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,
+ private 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");
- // Add the previous message without header (i.e first line)
- sb.append(currSquashMessage.substring(currSquashMessage.indexOf("\n") + 1));
- sb.append("\n");
- if (isSquash) {
- sb.append("# This is the ").append(count).append(ordinal)
- .append(" commit message:\n");
- sb.append(commitToPick.getFullMessage());
+ // currSquashMessage is always non-empty here, and the first character
+ // is the comment character used so far.
+ char commentChar = currSquashMessage.charAt(0);
+ String newMessage = commitToPick.getFullMessage();
+ if (!isSquash) {
+ sb.append(commentChar).append(" This is a combination of ")
+ .append(count).append(" commits.\n");
+ // Add the previous message without header (i.e first line)
+ sb.append(currSquashMessage
+ .substring(currSquashMessage.indexOf('\n') + 1));
+ sb.append('\n');
+ sb.append(commentChar).append(" The ").append(count).append(ordinal)
+ .append(" commit message will be skipped:\n")
+ .append(commentChar).append(' ');
+ sb.append(newMessage.replaceAll("([\n\r])",
+ "$1" + commentChar + ' '));
} else {
- sb.append("# The ").append(count).append(ordinal)
- .append(" commit message will be skipped:\n# ");
- sb.append(commitToPick.getFullMessage().replaceAll("([\n\r])",
- "$1# "));
+ String currentMessage = currSquashMessage;
+ if (commitConfig.isAutoCommentChar()) {
+ // Figure out a new comment character taking into account the
+ // new message
+ String cleaned = CommitConfig.cleanText(currentMessage,
+ CommitConfig.CleanupMode.STRIP, commentChar) + '\n'
+ + newMessage;
+ char newCommentChar = commitConfig.getCommentChar(cleaned);
+ if (newCommentChar != commentChar) {
+ currentMessage = replaceCommentChar(currentMessage,
+ commentChar, newCommentChar);
+ commentChar = newCommentChar;
+ }
+ }
+ sb.append(commentChar).append(" This is a combination of ")
+ .append(count).append(" commits.\n");
+ // Add the previous message without header (i.e first line)
+ sb.append(
+ currentMessage.substring(currentMessage.indexOf('\n') + 1));
+ sb.append('\n');
+ sb.append(commentChar).append(" This is the ").append(count)
+ .append(ordinal).append(" commit message:\n");
+ sb.append(newMessage);
}
return sb.toString();
}
+ private String replaceCommentChar(String message, char oldChar,
+ char newChar) {
+ // (?m) - Switch on multi-line matching; \h - horizontal whitespace
+ return message.replaceAll("(?m)^(\\h*)" + oldChar, "$1" + newChar); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
private static String getOrdinal(int count) {
switch (count % 10) {
case 1:
@@ -847,7 +914,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
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$
+ currSquashMessage.indexOf('\n'));
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(firstLine);
if (!matcher.find())
@@ -857,10 +924,11 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
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$);
+ char commentChar = commitConfig.getCommentChar(fullMessage);
+ rebaseState.createFile(messageFile,
+ commentChar + " This is a combination of 1 commits.\n" //$NON-NLS-1$
+ + commentChar + " The first commit's message is:\n" //$NON-NLS-1$
+ + fullMessage);
}
private String getOurCommitName() {
@@ -887,7 +955,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
case NO_CHANGE:
break;
default:
- throw new JGitInternalException("Updating HEAD failed");
+ throw new JGitInternalException(
+ JGitText.get().updatingHeadFailed);
}
rup = repo.updateRef(Constants.HEAD);
rup.setRefLogMessage("rebase finished: returning to " + headName, //$NON-NLS-1$
@@ -899,7 +968,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
case NO_CHANGE:
break;
default:
- throw new JGitInternalException("Updating HEAD failed");
+ throw new JGitInternalException(
+ JGitText.get().updatingHeadFailed);
}
}
}
@@ -913,6 +983,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
try {
DirCacheCheckout dco = new DirCacheCheckout(repo, dc, headTree);
dco.setFailOnConflict(false);
+ dco.setProgressMonitor(monitor);
boolean needsDeleteFiles = dco.checkout();
if (needsDeleteFiles) {
List<String> fileList = dco.getToBeDeleted();
@@ -935,7 +1006,9 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
/**
* @return the commit if we had to do a commit, otherwise null
* @throws GitAPIException
+ * if JGit API failed
* @throws IOException
+ * if an IO error occurred
*/
private RevCommit continueRebase() throws GitAPIException, IOException {
// if there are still conflicts, we throw a specific Exception
@@ -978,6 +1051,9 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
try {
raw = IO.readFully(authorScriptFile);
} catch (FileNotFoundException notFound) {
+ if (authorScriptFile.exists()) {
+ throw notFound;
+ }
return null;
}
return parseAuthor(raw);
@@ -989,13 +1065,16 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
String authorScript = toAuthorScript(author);
rebaseState.createFile(AUTHOR_SCRIPT, authorScript);
rebaseState.createFile(MESSAGE, commitToPick.getFullMessage());
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- try (DiffFormatter df = new DiffFormatter(bos)) {
- df.setRepository(repo);
- df.format(commitToPick.getParent(0), commitToPick);
+ if (commitToPick.getParentCount() > 0) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (DiffFormatter df = new DiffFormatter(bos)) {
+ df.setRepository(repo);
+ df.format(commitToPick.getParent(0), commitToPick);
+ }
+ rebaseState.createFile(PATCH, new String(bos.toByteArray(), UTF_8));
+ } else {
+ rebaseState.createFile(PATCH, ""); //$NON-NLS-1$
}
- rebaseState.createFile(PATCH, new String(bos.toByteArray(),
- Constants.CHARACTER_ENCODING));
rebaseState.createFile(STOPPED_SHA,
repo.newObjectReader()
.abbreviate(
@@ -1035,13 +1114,15 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
* that can not be parsed as steps
*
* @param numSteps
+ * number of steps to remove
* @throws IOException
+ * if an IO error occurred
*/
private void popSteps(int numSteps) throws IOException {
if (numSteps == 0)
return;
- List<RebaseTodoLine> todoLines = new LinkedList<RebaseTodoLine>();
- List<RebaseTodoLine> poppedLines = new LinkedList<RebaseTodoLine>();
+ List<RebaseTodoLine> todoLines = new ArrayList<>();
+ List<RebaseTodoLine> poppedLines = new ArrayList<>();
for (RebaseTodoLine line : repo.readRebaseTodo(
rebaseState.getPath(GIT_REBASE_TODO), true)) {
@@ -1054,7 +1135,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
todoLines, false);
- if (poppedLines.size() > 0) {
+ if (!poppedLines.isEmpty()) {
repo.writeRebaseTodoFile(rebaseState.getPath(DONE), poppedLines,
true);
}
@@ -1067,18 +1148,19 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
Ref head = getHead();
- String headName = getHeadName(head);
ObjectId headId = head.getObjectId();
- if (headId == null)
+ if (headId == null) {
throw new RefNotFoundException(MessageFormat.format(
JGitText.get().refNotResolved, Constants.HEAD));
+ }
+ String headName = getHeadName(head);
RevCommit headCommit = walk.lookupCommit(headId);
RevCommit upstream = walk.lookupCommit(upstreamCommit.getId());
if (!isInteractive() && walk.isMergedInto(upstream, headCommit))
return RebaseResult.UP_TO_DATE_RESULT;
else if (!isInteractive() && walk.isMergedInto(headCommit, upstream)) {
- // head is already merged into upstream, fast-foward
+ // head is already merged into upstream, fast-forward
monitor.beginTask(MessageFormat.format(
JGitText.get().resettingHead,
upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN);
@@ -1097,15 +1179,19 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
repo.writeOrigHead(headId);
rebaseState.createFile(REBASE_HEAD, headId.name());
+ rebaseState.createFile(REBASE_HEAD_LEGACY, headId.name());
rebaseState.createFile(HEAD_NAME, headName);
rebaseState.createFile(ONTO, upstreamCommit.name());
rebaseState.createFile(ONTO_NAME, upstreamCommitName);
- if (isInteractive()) {
+ if (isInteractive() || preserveMerges) {
+ // --preserve-merges is an interactive mode for native git. Without
+ // this, native git rebase --continue after a conflict would fall
+ // into merge mode.
rebaseState.createFile(INTERACTIVE, ""); //$NON-NLS-1$
}
rebaseState.createFile(QUIET, ""); //$NON-NLS-1$
- ArrayList<RebaseTodoLine> toDoSteps = new ArrayList<RebaseTodoLine>();
+ ArrayList<RebaseTodoLine> toDoSteps = new ArrayList<>();
toDoSteps.add(new RebaseTodoLine("# Created by EGit: rebasing " + headId.name() //$NON-NLS-1$
+ " onto " + upstreamCommit.name())); //$NON-NLS-1$
// determine the commits to be applied
@@ -1135,16 +1221,20 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
private List<RevCommit> calculatePickList(RevCommit headCommit)
- throws GitAPIException, NoHeadException, IOException {
- Iterable<RevCommit> commitsToUse;
- try (Git git = new Git(repo)) {
- LogCommand cmd = git.log().addRange(upstreamCommit, headCommit);
- commitsToUse = cmd.call();
- }
- List<RevCommit> cherryPickList = new ArrayList<RevCommit>();
- for (RevCommit commit : commitsToUse) {
- if (preserveMerges || commit.getParentCount() == 1)
- cherryPickList.add(commit);
+ throws IOException {
+ List<RevCommit> cherryPickList = new ArrayList<>();
+ try (RevWalk r = new RevWalk(repo)) {
+ r.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true);
+ r.sort(RevSort.COMMIT_TIME_DESC, true);
+ r.markUninteresting(r.lookupCommit(upstreamCommit));
+ r.markStart(r.lookupCommit(headCommit));
+ Iterator<RevCommit> commitsToUse = r.iterator();
+ while (commitsToUse.hasNext()) {
+ RevCommit commit = commitsToUse.next();
+ if (preserveMerges || commit.getParentCount() <= 1) {
+ cherryPickList.add(commit);
+ }
+ }
}
Collections.reverse(cherryPickList);
@@ -1158,23 +1248,31 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
walk.markStart(upstreamCommit);
walk.markStart(headCommit);
RevCommit base;
- while ((base = walk.next()) != null)
+ while ((base = walk.next()) != null) {
RebaseState.createFile(rewrittenDir, base.getName(),
upstreamCommit.getName());
-
+ }
Iterator<RevCommit> iterator = cherryPickList.iterator();
pickLoop: while(iterator.hasNext()){
RevCommit commit = iterator.next();
- for (int i = 0; i < commit.getParentCount(); i++) {
- boolean parentRewritten = new File(rewrittenDir, commit
- .getParent(i).getName()).exists();
- if (parentRewritten) {
- new File(rewrittenDir, commit.getName()).createNewFile();
- continue pickLoop;
+ int nOfParents = commit.getParentCount();
+ if (nOfParents == 0) {
+ // Must be the very first commit in the cherryPickList. We
+ // have independent branches.
+ new File(rewrittenDir, commit.getName()).createNewFile();
+ } else {
+ for (int i = 0; i < nOfParents; i++) {
+ boolean parentRewritten = new File(rewrittenDir,
+ commit.getParent(i).getName()).exists();
+ if (parentRewritten) {
+ new File(rewrittenDir, commit.getName())
+ .createNewFile();
+ continue pickLoop;
+ }
}
+ // commit is only merged in, needs not be rewritten
+ iterator.remove();
}
- // commit is only merged in, needs not be rewritten
- iterator.remove();
}
}
return cherryPickList;
@@ -1182,15 +1280,19 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
private static String getHeadName(Ref head) {
String headName;
- if (head.isSymbolic())
+ if (head.isSymbolic()) {
headName = head.getTarget().getName();
- else
- headName = head.getObjectId().getName();
+ } else {
+ ObjectId headId = head.getObjectId();
+ // the callers are checking this already
+ assert headId != null;
+ headName = headId.getName();
+ }
return headName;
}
private Ref getHead() throws IOException, RefNotFoundException {
- Ref head = repo.getRef(Constants.HEAD);
+ Ref head = repo.exactRef(Constants.HEAD);
if (head == null || head.getObjectId() == null)
throw new RefNotFoundException(MessageFormat.format(
JGitText.get().refNotResolved, Constants.HEAD));
@@ -1202,12 +1304,16 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
- * checks if we can fast-forward and returns the new head if it is possible
+ * Check if we can fast-forward and returns the new head if it is possible
*
* @param newCommit
+ * a {@link org.eclipse.jgit.revwalk.RevCommit} object to check
+ * if we can fast-forward to.
* @return the new head, or null
- * @throws IOException
- * @throws GitAPIException
+ * @throws java.io.IOException
+ * if an IO error occurred
+ * @throws org.eclipse.jgit.api.errors.GitAPIException
+ * if a JGit API exception occurred
*/
public RevCommit tryFastForward(RevCommit newCommit) throws IOException,
GitAPIException {
@@ -1236,6 +1342,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
CheckoutCommand co = new CheckoutCommand(repo);
try {
+ co.setProgressMonitor(monitor);
co.setName(newCommit.name()).call();
if (headName.startsWith(Constants.R_HEADS)) {
RefUpdate rup = repo.updateRef(headName);
@@ -1254,13 +1361,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
}
return newCommit;
- } catch (RefAlreadyExistsException e) {
- throw new JGitInternalException(e.getMessage(), e);
- } catch (RefNotFoundException e) {
- throw new JGitInternalException(e.getMessage(), e);
- } catch (InvalidRefNameException e) {
- throw new JGitInternalException(e.getMessage(), e);
- } catch (CheckoutConflictException e) {
+ } catch (RefAlreadyExistsException | RefNotFoundException
+ | InvalidRefNameException | CheckoutConflictException e) {
throw new JGitInternalException(e.getMessage(), e);
}
}
@@ -1291,7 +1393,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
if (this.upstreamCommit == null)
throw new JGitInternalException(MessageFormat
.format(JGitText.get().missingRequiredParameter,
- "upstream"));
+ "upstream")); //$NON-NLS-1$
return;
default:
throw new WrongRepositoryStateException(MessageFormat.format(
@@ -1303,8 +1405,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
private RebaseResult abort(RebaseResult result) throws IOException,
GitAPIException {
+ ObjectId origHead = getOriginalHead();
try {
- ObjectId origHead = repo.readOrigHead();
String commitId = origHead != null ? origHead.name() : null;
monitor.beginTask(MessageFormat.format(
JGitText.get().abortingRebase, commitId),
@@ -1343,7 +1445,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
// update the HEAD
res = refUpdate.link(headName);
} else {
- refUpdate.setNewObjectId(repo.readOrigHead());
+ refUpdate.setNewObjectId(origHead);
res = refUpdate.forceUpdate();
}
@@ -1356,13 +1458,14 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
throw new JGitInternalException(
JGitText.get().abortingRebaseFailed);
}
- boolean stashConflicts = autoStashApply();
+ boolean unstashSuccessful = autoStashApply();
// cleanup the files
FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE);
repo.writeCherryPickHead(null);
repo.writeMergeHeads(null);
- if (stashConflicts)
+ if (!unstashSuccessful) {
return RebaseResult.STASH_APPLY_CONFLICTS_RESULT;
+ }
return result;
} finally {
@@ -1370,6 +1473,19 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
}
+ private ObjectId getOriginalHead() throws IOException {
+ try {
+ return ObjectId.fromString(rebaseState.readFile(REBASE_HEAD));
+ } catch (FileNotFoundException e) {
+ try {
+ return ObjectId
+ .fromString(rebaseState.readFile(REBASE_HEAD_LEGACY));
+ } catch (FileNotFoundException ex) {
+ return repo.readOrigHead();
+ }
+ }
+ }
+
private boolean checkoutCommit(String headName, RevCommit commit)
throws IOException,
CheckoutConflictException {
@@ -1378,6 +1494,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
DirCacheCheckout dco = new DirCacheCheckout(repo, head.getTree(),
repo.lockDirCache(), commit.getTree());
dco.setFailOnConflict(true);
+ dco.setProgressMonitor(monitor);
try {
dco.checkout();
} catch (org.eclipse.jgit.errors.CheckoutConflictException cce) {
@@ -1398,7 +1515,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
case FORCED:
break;
default:
- throw new IOException("Could not rewind to upstream commit");
+ throw new IOException(
+ JGitText.get().couldNotRewindToUpstreamCommit);
}
} finally {
walk.close();
@@ -1409,6 +1527,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
/**
+ * Set upstream {@code RevCommit}
+ *
* @param upstream
* the upstream commit
* @return {@code this}
@@ -1420,6 +1540,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
+ * Set the upstream commit
+ *
* @param upstream
* id of the upstream commit
* @return {@code this}
@@ -1437,10 +1559,13 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
+ * Set the upstream branch
+ *
* @param upstream
- * the upstream branch
+ * the name of the upstream branch
* @return {@code this}
- * @throws RefNotFoundException
+ * @throws org.eclipse.jgit.api.errors.RefNotFoundException
+ * if {@code upstream} Ref couldn't be resolved
*/
public RebaseCommand setUpstream(String upstream)
throws RefNotFoundException {
@@ -1468,13 +1593,15 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
public RebaseCommand setUpstreamName(String upstreamName) {
if (upstreamCommit == null) {
throw new IllegalStateException(
- "setUpstreamName must be called after setUpstream.");
+ "setUpstreamName must be called after setUpstream."); //$NON-NLS-1$
}
this.upstreamCommitName = upstreamName;
return this;
}
/**
+ * Set the operation to execute during rebase
+ *
* @param operation
* the operation to perform
* @return {@code this}
@@ -1485,25 +1612,33 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
+ * Set progress monitor
+ *
* @param monitor
* a progress monitor
* @return this instance
*/
public RebaseCommand setProgressMonitor(ProgressMonitor monitor) {
+ if (monitor == null) {
+ monitor = NullProgressMonitor.INSTANCE;
+ }
this.monitor = monitor;
return this;
}
/**
- * Enables interactive rebase
+ * Enable interactive rebase
* <p>
* Does not stop after initialization of interactive rebase. This is
* equivalent to
- * {@link RebaseCommand#runInteractively(InteractiveHandler, boolean)
+ * {@link org.eclipse.jgit.api.RebaseCommand#runInteractively(InteractiveHandler, boolean)
* runInteractively(handler, false)};
* </p>
*
* @param handler
+ * the
+ * {@link org.eclipse.jgit.api.RebaseCommand.InteractiveHandler}
+ * to use
* @return this
*/
public RebaseCommand runInteractively(InteractiveHandler handler) {
@@ -1511,14 +1646,17 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
- * Enables interactive rebase
+ * Enable interactive rebase
* <p>
* If stopAfterRebaseInteractiveInitialization is {@code true} the rebase
* stops after initialization of interactive rebase returning
- * {@link RebaseResult#INTERACTIVE_PREPARED_RESULT}
+ * {@link org.eclipse.jgit.api.RebaseResult#INTERACTIVE_PREPARED_RESULT}
* </p>
*
* @param handler
+ * the
+ * {@link org.eclipse.jgit.api.RebaseCommand.InteractiveHandler}
+ * to use
* @param stopAfterRebaseInteractiveInitialization
* if {@code true} the rebase stops after initialization
* @return this instance
@@ -1532,6 +1670,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
+ * Set the <code>MergeStrategy</code>.
+ *
* @param strategy
* The merge strategy to use during this rebase operation.
* @return {@code this}
@@ -1543,9 +1683,26 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
+ * Sets the content merge strategy to use if the
+ * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+ * "recursive".
+ *
+ * @param strategy
+ * the {@link ContentMergeStrategy} to be used
+ * @return {@code this}
+ * @since 5.12
+ */
+ public RebaseCommand setContentMergeStrategy(ContentMergeStrategy strategy) {
+ this.contentStrategy = strategy;
+ return this;
+ }
+
+ /**
+ * Whether to preserve merges during rebase
+ *
* @param preserve
- * True to re-create merges during rebase. Defaults to false, a
- * flattening rebase.
+ * {@code true} to re-create merges during rebase. Defaults to
+ * {@code false}, a flattening rebase.
* @return {@code this}
* @since 3.5
*/
@@ -1555,32 +1712,112 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
- * Allows configure rebase interactive process and modify commit message
+ * Allows to configure the interactive rebase process steps and to modify
+ * commit messages.
*/
public interface InteractiveHandler {
+
/**
- * Given list of {@code steps} should be modified according to user
- * rebase configuration
+ * Callback API to modify the initial list of interactive rebase steps.
+ *
* @param steps
- * initial configuration of rebase interactive
+ * initial configuration of interactive rebase
*/
void prepareSteps(List<RebaseTodoLine> steps);
/**
- * Used for editing commit message on REWORD
+ * Used for editing commit message on REWORD or SQUASH.
*
- * @param commit
+ * @param message
+ * existing commit message
* @return new commit message
*/
- String modifyCommitMessage(String commit);
+ String modifyCommitMessage(String message);
}
+ /**
+ * Extends {@link InteractiveHandler} with an enhanced callback for editing
+ * commit messages.
+ *
+ * @since 6.1
+ */
+ public interface InteractiveHandler2 extends InteractiveHandler {
+
+ /**
+ * Callback API for editing a commit message on REWORD or SQUASH.
+ * <p>
+ * The callback gets the comment character currently set, and the
+ * clean-up mode. It can use this information when presenting the
+ * message to the user, and it also has the possibility to clean the
+ * message itself (in which case the returned {@link ModifyResult}
+ * should have {@link CleanupMode#VERBATIM} set lest JGit cleans the
+ * message again). It can also override the initial clean-up mode by
+ * returning clean-up mode other than {@link CleanupMode#DEFAULT}. If it
+ * does return {@code DEFAULT}, the passed-in {@code mode} will be
+ * applied.
+ * </p>
+ *
+ * @param message
+ * existing commit message
+ * @param mode
+ * {@link CleanupMode} currently set
+ * @param commentChar
+ * comment character used
+ * @return a {@link ModifyResult}
+ */
+ @NonNull
+ ModifyResult editCommitMessage(@NonNull String message,
+ @NonNull CleanupMode mode, char commentChar);
+
+ @Override
+ default String modifyCommitMessage(String message) {
+ // Should actually not be called; but do something reasonable anyway
+ ModifyResult result = editCommitMessage(
+ message == null ? "" : message, CleanupMode.STRIP, //$NON-NLS-1$
+ '#');
+ return result.getMessage();
+ }
+
+ /**
+ * Describes the result of editing a commit message: the new message,
+ * and how it should be cleaned.
+ */
+ interface ModifyResult {
+
+ /**
+ * Retrieves the new commit message.
+ *
+ * @return the message
+ */
+ @NonNull
+ String getMessage();
+
+ /**
+ * Tells how the message returned by {@link #getMessage()} should be
+ * cleaned.
+ *
+ * @return the {@link CleanupMode}
+ */
+ @NonNull
+ CleanupMode getCleanupMode();
+
+ /**
+ * Tells whether a Gerrit Change-Id should be computed and added to
+ * the commit message, as with
+ * {@link CommitCommand#setInsertChangeId(boolean)}.
+ *
+ * @return {@code true} if a Change-Id should be handled,
+ * {@code false} otherwise
+ */
+ boolean shouldAddChangeId();
+ }
+ }
PersonIdent parseAuthor(byte[] raw) {
if (raw.length == 0)
return null;
- Map<String, String> keyValueMap = new HashMap<String, String>();
+ Map<String, String> keyValueMap = new HashMap<>();
for (int p = 0; p < raw.length;) {
int end = RawParseUtils.nextLF(raw, p);
if (end == p)
@@ -1600,23 +1837,26 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
// the time is saved as <seconds since 1970> <timezone offset>
int timeStart = 0;
- if (time.startsWith("@")) //$NON-NLS-1$
+ if (time.startsWith("@")) { //$NON-NLS-1$
timeStart = 1;
- else
+ } else {
timeStart = 0;
- long when = Long
- .parseLong(time.substring(timeStart, time.indexOf(' '))) * 1000;
+ }
+ Instant when = Instant.ofEpochSecond(
+ Long.parseLong(time.substring(timeStart, time.indexOf(' '))));
String tzOffsetString = time.substring(time.indexOf(' ') + 1);
int multiplier = -1;
- if (tzOffsetString.charAt(0) == '+')
+ if (tzOffsetString.charAt(0) == '+') {
multiplier = 1;
+ }
int hours = Integer.parseInt(tzOffsetString.substring(1, 3));
int minutes = Integer.parseInt(tzOffsetString.substring(3, 5));
// this is in format (+/-)HHMM (hours and minutes)
- // we need to convert into minutes
- int tz = (hours * 60 + minutes) * multiplier;
- if (name != null && email != null)
+ ZoneOffset tz = ZoneOffset.ofHoursMinutes(hours * multiplier,
+ minutes * multiplier);
+ if (name != null && email != null) {
return new PersonIdent(name, email, when, tz);
+ }
return null;
}
@@ -1651,7 +1891,20 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
public String readFile(String name) throws IOException {
- return readFile(getDir(), name);
+ try {
+ return readFile(getDir(), name);
+ } catch (FileNotFoundException e) {
+ if (ONTO_NAME.equals(name)) {
+ // Older JGit mistakenly wrote a file "onto-name" instead of
+ // "onto_name". Try that wrong name just in case somebody
+ // upgraded while a rebase started by JGit was in progress.
+ File oldFile = getFile(ONTO_NAME.replace('_', '-'));
+ if (oldFile.exists()) {
+ return readFile(oldFile);
+ }
+ }
+ throw e;
+ }
}
public void createFile(String name, String content) throws IOException {
@@ -1666,35 +1919,33 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return (getDir().getName() + "/" + name); //$NON-NLS-1$
}
- private static String readFile(File directory, String fileName)
- throws IOException {
- byte[] content = IO.readFully(new File(directory, fileName));
+ private static String readFile(File file) throws IOException {
+ byte[] content = IO.readFully(file);
// strip off the last LF
int end = RawParseUtils.prevLF(content, content.length);
return RawParseUtils.decode(content, 0, end + 1);
}
+ private static String readFile(File directory, String fileName)
+ throws IOException {
+ return readFile(new File(directory, fileName));
+ }
+
private static void createFile(File parentDir, String name,
String content)
throws IOException {
File file = new File(parentDir, name);
- FileOutputStream fos = new FileOutputStream(file);
- try {
- fos.write(content.getBytes(Constants.CHARACTER_ENCODING));
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write(content.getBytes(UTF_8));
fos.write('\n');
- } finally {
- fos.close();
}
}
private static void appendToFile(File file, String content)
throws IOException {
- FileOutputStream fos = new FileOutputStream(file, true);
- try {
- fos.write(content.getBytes(Constants.CHARACTER_ENCODING));
+ try (FileOutputStream fos = new FileOutputStream(file, true)) {
+ fos.write(content.getBytes(UTF_8));
fos.write('\n');
- } finally {
- fos.close();
}
}
}