blob: 4e0d9d78c32470595cfdcc455e7ad74059018bfc [file] [log] [blame]
/*
* Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com>
* 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 v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* 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.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;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
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;
import org.eclipse.jgit.api.errors.NoMessageException;
import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
import org.eclipse.jgit.api.errors.RefNotFoundException;
import org.eclipse.jgit.api.errors.StashApplyFailureException;
import org.eclipse.jgit.api.errors.UnmergedPathsException;
import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheIterator;
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;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.RebaseTodoLine;
import org.eclipse.jgit.lib.RebaseTodoLine.Action;
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;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
/**
* A class used to execute a {@code Rebase} command. It has setters for all
* 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"
* >Git documentation about Rebase</a>
*/
public class RebaseCommand extends GitCommand<RebaseResult> {
/**
* The name of the "rebase-merge" folder for interactive rebases.
*/
public static final String REBASE_MERGE = "rebase-merge"; //$NON-NLS-1$
/**
* The name of the "rebase-apply" folder for non-interactive rebases.
*/
private static final String REBASE_APPLY = "rebase-apply"; //$NON-NLS-1$
/**
* The name of the "stopped-sha" file
*/
public static final String STOPPED_SHA = "stopped-sha"; //$NON-NLS-1$
private static final String AUTHOR_SCRIPT = "author-script"; //$NON-NLS-1$
private static final String DONE = "done"; //$NON-NLS-1$
private static final String GIT_AUTHOR_DATE = "GIT_AUTHOR_DATE"; //$NON-NLS-1$
private static final String GIT_AUTHOR_EMAIL = "GIT_AUTHOR_EMAIL"; //$NON-NLS-1$
private static final String GIT_AUTHOR_NAME = "GIT_AUTHOR_NAME"; //$NON-NLS-1$
private static final String GIT_REBASE_TODO = "git-rebase-todo"; //$NON-NLS-1$
private static final String HEAD_NAME = "head-name"; //$NON-NLS-1$
private static final String INTERACTIVE = "interactive"; //$NON-NLS-1$
private static final String QUIET = "quiet"; //$NON-NLS-1$
private static final String MESSAGE = "message"; //$NON-NLS-1$
private static final String ONTO = "onto"; //$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 = "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$
private static final String MESSAGE_FIXUP = "message-fixup"; //$NON-NLS-1$
private static final String MESSAGE_SQUASH = "message-squash"; //$NON-NLS-1$
private static final String AUTOSTASH = "autostash"; //$NON-NLS-1$
private static final String AUTOSTASH_MSG = "On {0}: autostash"; //$NON-NLS-1$
/**
* The folder containing the hashes of (potentially) rewritten commits when
* --preserve-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
* is used.
*/
private static final String CURRENT_COMMIT = "current-commit"; //$NON-NLS-1$
private static final String REFLOG_PREFIX = "rebase:"; //$NON-NLS-1$
/**
* The available operations
*/
public enum Operation {
/**
* Initiates rebase
*/
BEGIN,
/**
* Continues after a conflict resolution
*/
CONTINUE,
/**
* Skips the "current" commit
*/
SKIP,
/**
* Aborts and resets the current rebase
*/
ABORT,
/**
* Starts processing steps
* @since 3.2
*/
PROCESS_STEPS;
}
private Operation operation = Operation.BEGIN;
private RevCommit upstreamCommit;
private String upstreamCommitName;
private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
private final RevWalk walk;
private final RebaseState rebaseState;
private InteractiveHandler interactiveHandler;
private CommitConfig commitConfig;
private boolean stopAfterInitialization = false;
private RevCommit newHead;
private boolean lastStepWasForward;
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);
walk = new RevWalk(repo);
rebaseState = new RebaseState(repo.getDirectory());
}
/**
* {@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.
*/
@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:
try {
return abort(RebaseResult.ABORTED_RESULT);
} catch (IOException ioe) {
throw new JGitInternalException(ioe.getMessage(), ioe);
}
case PROCESS_STEPS:
case SKIP:
case CONTINUE:
String upstreamCommitId = rebaseState.readFile(ONTO);
try {
upstreamCommitName = rebaseState.readFile(ONTO_NAME);
} catch (FileNotFoundException e) {
// Fall back to commit ID if file doesn't exist (e.g. rebase
// was started by C Git)
upstreamCommitName = upstreamCommitId;
}
this.upstreamCommit = walk.parseCommit(repo
.resolve(upstreamCommitId));
preserveMerges = rebaseState.getRewrittenDir().isDirectory();
break;
case BEGIN:
autoStash();
if (stopAfterInitialization
|| !walk.isMergedInto(
walk.parseCommit(repo.resolve(Constants.HEAD)),
upstreamCommit)) {
org.eclipse.jgit.api.Status status = Git.wrap(repo)
.status().setIgnoreSubmodules(IgnoreSubmoduleMode.ALL).call();
if (status.hasUncommittedChanges()) {
List<String> list = new ArrayList<>();
list.addAll(status.getUncommittedChanges());
return RebaseResult.uncommittedChanges(list);
}
}
RebaseResult res = initFilesAndRewind();
if (stopAfterInitialization)
return RebaseResult.INTERACTIVE_PREPARED_RESULT;
if (res != null) {
autoStashApply();
if (rebaseState.getDir().exists())
FileUtils.delete(rebaseState.getDir(),
FileUtils.RECURSIVE);
return res;
}
}
if (monitor.isCancelled())
return abort(RebaseResult.ABORTED_RESULT);
if (operation == Operation.CONTINUE) {
newHead = continueRebase();
List<RebaseTodoLine> doneLines = repo.readRebaseTodo(
rebaseState.getPath(DONE), true);
RebaseTodoLine step = doneLines.get(doneLines.size() - 1);
if (newHead != null
&& step.getAction() != Action.PICK) {
RebaseTodoLine newStep = new RebaseTodoLine(
step.getAction(),
AbbreviatedObjectId.fromObjectId(newHead),
step.getShortMessage());
RebaseResult result = processStep(newStep, false);
if (result != null)
return result;
}
File amendFile = rebaseState.getFile(AMEND);
boolean amendExists = amendFile.exists();
if (amendExists) {
FileUtils.delete(amendFile);
}
if (newHead == null && !amendExists) {
// continueRebase() returns null only if no commit was
// neccessary. This means that no changes where left over
// after resolving all conflicts. In this case, cgit stops
// and displays a nice message to the user, telling him to
// either do changes or skip the commit instead of continue.
return RebaseResult.NOTHING_TO_COMMIT_RESULT;
}
}
if (operation == Operation.SKIP)
newHead = checkoutCurrentHead();
List<RebaseTodoLine> steps = repo.readRebaseTodo(
rebaseState.getPath(GIT_REBASE_TODO), false);
if (steps.isEmpty()) {
return finishRebase(walk.parseCommit(repo.resolve(Constants.HEAD)), false);
}
if (isInteractive()) {
interactiveHandler.prepareSteps(steps);
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
steps, false);
}
checkSteps(steps);
for (RebaseTodoLine step : steps) {
popSteps(1);
RebaseResult result = processStep(step, true);
if (result != null) {
return result;
}
}
return finishRebase(newHead, lastStepWasForward);
} catch (CheckoutConflictException cce) {
return RebaseResult.conflicts(cce.getConflictingPaths());
} catch (IOException ioe) {
throw new JGitInternalException(ioe.getMessage(), ioe);
}
}
private void autoStash() throws GitAPIException, IOException {
if (repo.getConfig().getBoolean(ConfigConstants.CONFIG_REBASE_SECTION,
ConfigConstants.CONFIG_KEY_AUTOSTASH, false)) {
String message = MessageFormat.format(
AUTOSTASH_MSG,
Repository
.shortenRefName(getHeadName(getHead())));
RevCommit stashCommit = Git.wrap(repo).stashCreate().setRef(null)
.setWorkingDirectoryMessage(
message)
.call();
if (stashCommit != null) {
FileUtils.mkdir(rebaseState.getDir());
rebaseState.createFile(AUTOSTASH, stashCommit.getName());
}
}
}
private boolean autoStashApply() throws IOException, GitAPIException {
boolean conflicts = false;
if (rebaseState.getFile(AUTOSTASH).exists()) {
String stash = rebaseState.readFile(AUTOSTASH);
try (Git git = Git.wrap(repo)) {
git.stashApply().setStashRef(stash)
.ignoreRepositoryState(true).setStrategy(strategy)
.call();
} catch (StashApplyFailureException e) {
conflicts = true;
try (RevWalk rw = new RevWalk(repo)) {
ObjectId stashId = repo.resolve(stash);
RevCommit commit = rw.parseCommit(stashId);
updateStashRef(commit, commit.getAuthorIdent(),
commit.getShortMessage());
}
}
}
return conflicts;
}
private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent,
String refLogMessage) throws IOException {
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
refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
refUpdate.forceUpdate();
}
private RebaseResult processStep(RebaseTodoLine step, boolean shouldPick)
throws IOException, GitAPIException {
if (Action.COMMENT.equals(step.getAction()))
return null;
if (preserveMerges
&& shouldPick
&& (Action.EDIT.equals(step.getAction()) || Action.PICK
.equals(step.getAction()))) {
writeRewrittenHashes();
}
ObjectReader or = repo.newObjectReader();
Collection<ObjectId> ids = or.resolve(step.getCommit());
if (ids.size() != 1)
throw new JGitInternalException(
JGitText.get().cannotResolveUniquelyAbbrevObjectId);
RevCommit commitToPick = walk.parseCommit(ids.iterator().next());
if (shouldPick) {
if (monitor.isCancelled())
return RebaseResult.result(Status.STOPPED, commitToPick);
RebaseResult result = cherryPickCommit(commitToPick);
if (result != null)
return result;
}
boolean isSquash = false;
switch (step.getAction()) {
case PICK:
return null; // continue rebase process on pick command
case REWORD:
String oldMessage = commitToPick.getFullMessage();
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());
return stop(commitToPick, Status.EDIT);
case COMMENT:
break;
case SQUASH:
isSquash = true;
//$FALL-THROUGH$
case FIXUP:
resetSoftToParent();
List<RebaseTodoLine> steps = repo.readRebaseTodo(
rebaseState.getPath(GIT_REBASE_TODO), false);
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()) {
messageFixupFile.delete();
}
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,
WrongRepositoryStateException, NoHeadException {
try {
monitor.beginTask(MessageFormat.format(
JGitText.get().applyingCommit,
commitToPick.getShortMessage()), ProgressMonitor.UNKNOWN);
if (preserveMerges) {
return cherryPickCommitPreservingMerges(commitToPick);
}
return cherryPickCommitFlattening(commitToPick);
} finally {
monitor.endTask();
}
}
private RebaseResult cherryPickCommitFlattening(RevCommit commitToPick)
throws IOException, GitAPIException, NoMessageException,
UnmergedPathsException, ConcurrentRefUpdateException,
WrongRepositoryStateException, NoHeadException {
// If the first parent of commitToPick is the current HEAD,
// we do a fast-forward instead of cherry-pick to avoid
// unnecessary object rewriting
newHead = tryFastForward(commitToPick);
lastStepWasForward = newHead != null;
if (!lastStepWasForward) {
// TODO if the content of this commit is already merged
// here we should skip this step in order to avoid
// confusing pseudo-changed
String ourCommitName = getOurCommitName();
try (Git git = new Git(repo)) {
CherryPickResult cherryPickResult = git.cherryPick()
.include(commitToPick)
.setOurCommitName(ourCommitName)
.setReflogPrefix(REFLOG_PREFIX)
.setStrategy(strategy)
.setContentMergeStrategy(contentStrategy)
.call();
switch (cherryPickResult.getStatus()) {
case FAILED:
if (operation == Operation.BEGIN) {
return abort(RebaseResult
.failed(cherryPickResult.getFailingPaths()));
}
return stop(commitToPick, Status.STOPPED);
case CONFLICTING:
return stop(commitToPick, Status.STOPPED);
case OK:
newHead = cherryPickResult.getNewHead();
}
}
}
return null;
}
private RebaseResult cherryPickCommitPreservingMerges(RevCommit commitToPick)
throws IOException, GitAPIException, NoMessageException,
UnmergedPathsException, ConcurrentRefUpdateException,
WrongRepositoryStateException, NoHeadException {
writeCurrentCommit(commitToPick);
List<RevCommit> newParents = getNewParents(commitToPick);
boolean otherParentsUnchanged = true;
for (int i = 1; i < commitToPick.getParentCount(); i++)
otherParentsUnchanged &= newParents.get(i).equals(
commitToPick.getParent(i));
// If the first parent of commitToPick is the current HEAD,
// we do a fast-forward instead of cherry-pick to avoid
// unnecessary object rewriting
newHead = otherParentsUnchanged ? tryFastForward(commitToPick) : null;
lastStepWasForward = newHead != null;
if (!lastStepWasForward) {
ObjectId headId = getHead().getObjectId();
// 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
// change. This is different from C Git, which always uses the merge
// strategy (see below).
try (Git git = new Git(repo)) {
if (otherParentsUnchanged) {
boolean isMerge = commitToPick.getParentCount() > 1;
String ourCommitName = getOurCommitName();
CherryPickCommand pickCommand = git.cherryPick()
.include(commitToPick)
.setOurCommitName(ourCommitName)
.setReflogPrefix(REFLOG_PREFIX)
.setStrategy(strategy)
.setContentMergeStrategy(contentStrategy);
if (isMerge) {
pickCommand.setMainlineParentNumber(1);
// We write a MERGE_HEAD and later commit explicitly
pickCommand.setNoCommit(true);
writeMergeInfo(commitToPick, newParents);
}
CherryPickResult cherryPickResult = pickCommand.call();
switch (cherryPickResult.getStatus()) {
case FAILED:
if (operation == Operation.BEGIN) {
return abort(RebaseResult.failed(
cherryPickResult.getFailingPaths()));
}
return stop(commitToPick, Status.STOPPED);
case CONFLICTING:
return stop(commitToPick, Status.STOPPED);
case OK:
if (isMerge) {
// Commit the merge (setup above using
// writeMergeInfo())
CommitCommand commit = git.commit();
commit.setAuthor(commitToPick.getAuthorIdent());
commit.setReflogComment(REFLOG_PREFIX + " " //$NON-NLS-1$
+ commitToPick.getShortMessage());
newHead = commit.call();
} else
newHead = cherryPickResult.getNewHead();
break;
}
} else {
// Use the merge strategy to redo merges, which had some of
// 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));
MergeResult mergeResult = merge.call();
if (mergeResult.getMergeStatus().isSuccessful()) {
CommitCommand commit = git.commit();
commit.setAuthor(commitToPick.getAuthorIdent());
commit.setMessage(commitToPick.getFullMessage());
commit.setReflogComment(REFLOG_PREFIX + " " //$NON-NLS-1$
+ commitToPick.getShortMessage());
newHead = commit.call();
} else {
if (operation == Operation.BEGIN && mergeResult
.getMergeStatus() == MergeResult.MergeStatus.FAILED)
return abort(RebaseResult
.failed(mergeResult.getFailingPaths()));
return stop(commitToPick, Status.STOPPED);
}
}
}
}
return null;
}
// Prepare MERGE_HEAD and message for the next commit
private void writeMergeInfo(RevCommit commitToPick,
List<RevCommit> newParents) throws IOException {
repo.writeMergeHeads(newParents.subList(1, newParents.size()));
repo.writeMergeCommitMsg(commitToPick.getFullMessage());
}
// Get the rewritten equivalents for the parents of the given commit
private List<RevCommit> getNewParents(RevCommit commitToPick)
throws IOException {
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())
newParents.add(commitToPick.getParent(p));
else {
String newParent = RebaseState.readFile(
rebaseState.getRewrittenDir(), parentHash);
if (newParent.length() == 0)
newParents.add(walk.parseCommit(repo
.resolve(Constants.HEAD)));
else
newParents.add(walk.parseCommit(ObjectId
.fromString(newParent)));
}
}
return newParents;
}
private void writeCurrentCommit(RevCommit commit) throws IOException {
RebaseState.appendToFile(rebaseState.getFile(CURRENT_COMMIT),
commit.name());
}
private void writeRewrittenHashes() throws RevisionSyntaxException,
IOException, RefNotFoundException {
File currentCommitFile = rebaseState.getFile(CURRENT_COMMIT);
if (!currentCommitFile.exists())
return;
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
.createFile(rebaseState.getRewrittenDir(), current, head);
FileUtils.delete(currentCommitFile);
}
private RebaseResult finishRebase(RevCommit finalHead,
boolean lastStepIsForward) throws IOException, GitAPIException {
String headName = rebaseState.readFile(HEAD_NAME);
updateHead(headName, finalHead, upstreamCommit);
boolean stashConflicts = autoStashApply();
getRepository().autoGC(monitor);
FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE);
if (stashConflicts)
return RebaseResult.STASH_APPLY_CONFLICTS_RESULT;
if (lastStepIsForward || finalHead == null)
return RebaseResult.FAST_FORWARD_RESULT;
return RebaseResult.OK_RESULT;
}
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,
boolean isLast, 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) {
rebaseState.createFile(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);
return squashIntoPrevious(!messageFixup.exists(), isLast);
}
private void resetSoftToParent() throws IOException,
GitAPIException, CheckoutConflictException {
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.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_head);
}
}
private RevCommit squashIntoPrevious(boolean sequenceContainsSquash,
boolean isLast)
throws IOException, GitAPIException {
RevCommit retNewHead;
String commitMessage;
if (!isLast || sequenceContainsSquash) {
commitMessage = rebaseState.readFile(MESSAGE_SQUASH);
} else {
commitMessage = rebaseState.readFile(MESSAGE_FIXUP);
}
try (Git git = new Git(repo)) {
if (isLast) {
boolean[] doChangeId = { false };
if (sequenceContainsSquash) {
char commentChar = commitMessage.charAt(0);
commitMessage = editCommitMessage(doChangeId, commitMessage,
CleanupMode.STRIP, commentChar);
}
retNewHead = git.commit()
.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)
.setAmend(true).setNoVerify(true).call();
}
}
return retNewHead;
}
@SuppressWarnings("nls")
private String composeSquashMessage(boolean isSquash,
RevCommit commitToPick, String currSquashMessage, int count) {
StringBuilder sb = new StringBuilder();
String ordinal = getOrdinal(count);
// 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 {
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:
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'));
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 {
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() {
// If onto is different from upstream, this should say "onto", but
// RebaseCommand doesn't support a different "onto" at the moment.
String ourCommitName = "Upstream, based on " //$NON-NLS-1$
+ Repository.shortenRefName(upstreamCommitName);
return ourCommitName;
}
private void updateHead(String headName, RevCommit aNewHead, RevCommit onto)
throws IOException {
// point the previous head (if any) to the new commit
if (headName.startsWith(Constants.R_REFS)) {
RefUpdate rup = repo.updateRef(headName);
rup.setNewObjectId(aNewHead);
rup.setRefLogMessage("rebase finished: " + headName + " onto " //$NON-NLS-1$ //$NON-NLS-2$
+ onto.getName(), false);
Result res = rup.forceUpdate();
switch (res) {
case FAST_FORWARD:
case FORCED:
case NO_CHANGE:
break;
default:
throw new JGitInternalException(
JGitText.get().updatingHeadFailed);
}
rup = repo.updateRef(Constants.HEAD);
rup.setRefLogMessage("rebase finished: returning to " + headName, //$NON-NLS-1$
false);
res = rup.link(headName);
switch (res) {
case FAST_FORWARD:
case FORCED:
case NO_CHANGE:
break;
default:
throw new JGitInternalException(
JGitText.get().updatingHeadFailed);
}
}
}
private RevCommit checkoutCurrentHead() throws IOException, NoHeadException {
ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}"); //$NON-NLS-1$
if (headTree == null)
throw new NoHeadException(
JGitText.get().cannotRebaseWithoutCurrentHead);
DirCache dc = repo.lockDirCache();
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();
for (String filePath : fileList) {
File fileToDelete = new File(repo.getWorkTree(), filePath);
if (repo.getFS().exists(fileToDelete))
FileUtils.delete(fileToDelete, FileUtils.RECURSIVE
| FileUtils.RETRY);
}
}
} finally {
dc.unlock();
}
try (RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(repo.resolve(Constants.HEAD));
return commit;
}
}
/**
* @return the commit if we had to do a commit, otherwise null
* @throws GitAPIException
* @throws IOException
*/
private RevCommit continueRebase() throws GitAPIException, IOException {
// if there are still conflicts, we throw a specific Exception
DirCache dc = repo.readDirCache();
boolean hasUnmergedPaths = dc.hasUnmergedPaths();
if (hasUnmergedPaths)
throw new UnmergedPathsException();
// determine whether we need to commit
boolean needsCommit;
try (TreeWalk treeWalk = new TreeWalk(repo)) {
treeWalk.reset();
treeWalk.setRecursive(true);
treeWalk.addTree(new DirCacheIterator(dc));
ObjectId id = repo.resolve(Constants.HEAD + "^{tree}"); //$NON-NLS-1$
if (id == null)
throw new NoHeadException(
JGitText.get().cannotRebaseWithoutCurrentHead);
treeWalk.addTree(id);
treeWalk.setFilter(TreeFilter.ANY_DIFF);
needsCommit = treeWalk.next();
}
if (needsCommit) {
try (Git git = new Git(repo)) {
CommitCommand commit = git.commit();
commit.setMessage(rebaseState.readFile(MESSAGE));
commit.setAuthor(parseAuthor());
return commit.call();
}
}
return null;
}
private PersonIdent parseAuthor() throws IOException {
File authorScriptFile = rebaseState.getFile(AUTHOR_SCRIPT);
byte[] raw;
try {
raw = IO.readFully(authorScriptFile);
} catch (FileNotFoundException notFound) {
if (authorScriptFile.exists()) {
throw notFound;
}
return null;
}
return parseAuthor(raw);
}
private RebaseResult stop(RevCommit commitToPick, RebaseResult.Status status)
throws IOException {
PersonIdent author = commitToPick.getAuthorIdent();
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);
}
rebaseState.createFile(PATCH, new String(bos.toByteArray(), UTF_8));
rebaseState.createFile(STOPPED_SHA,
repo.newObjectReader()
.abbreviate(
commitToPick).name());
// Remove cherry pick state file created by CherryPickCommand, it's not
// needed for rebase
repo.writeCherryPickHead(null);
return RebaseResult.result(status, commitToPick);
}
String toAuthorScript(PersonIdent author) {
StringBuilder sb = new StringBuilder(100);
sb.append(GIT_AUTHOR_NAME);
sb.append("='"); //$NON-NLS-1$
sb.append(author.getName());
sb.append("'\n"); //$NON-NLS-1$
sb.append(GIT_AUTHOR_EMAIL);
sb.append("='"); //$NON-NLS-1$
sb.append(author.getEmailAddress());
sb.append("'\n"); //$NON-NLS-1$
// the command line uses the "external String"
// representation for date and timezone
sb.append(GIT_AUTHOR_DATE);
sb.append("='"); //$NON-NLS-1$
sb.append("@"); // @ for time in seconds since 1970 //$NON-NLS-1$
String externalString = author.toExternalString();
sb
.append(externalString.substring(externalString
.lastIndexOf('>') + 2));
sb.append("'\n"); //$NON-NLS-1$
return sb.toString();
}
/**
* Removes the number of lines given in the parameter from the
* <code>git-rebase-todo</code> file but preserves comments and other lines
* that can not be parsed as steps
*
* @param numSteps
* @throws IOException
*/
private void popSteps(int numSteps) throws IOException {
if (numSteps == 0)
return;
List<RebaseTodoLine> todoLines = new LinkedList<>();
List<RebaseTodoLine> poppedLines = new LinkedList<>();
for (RebaseTodoLine line : repo.readRebaseTodo(
rebaseState.getPath(GIT_REBASE_TODO), true)) {
if (poppedLines.size() >= numSteps
|| RebaseTodoLine.Action.COMMENT.equals(line.getAction()))
todoLines.add(line);
else
poppedLines.add(line);
}
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
todoLines, false);
if (!poppedLines.isEmpty()) {
repo.writeRebaseTodoFile(rebaseState.getPath(DONE), poppedLines,
true);
}
}
private RebaseResult initFilesAndRewind() throws IOException,
GitAPIException {
// we need to store everything into files so that we can implement
// --skip, --continue, and --abort
Ref head = getHead();
ObjectId headId = head.getObjectId();
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
monitor.beginTask(MessageFormat.format(
JGitText.get().resettingHead,
upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN);
checkoutCommit(headName, upstreamCommit);
monitor.endTask();
updateHead(headName, upstreamCommit, upstream);
return RebaseResult.FAST_FORWARD_RESULT;
}
monitor.beginTask(JGitText.get().obtainingCommitsForCherryPick,
ProgressMonitor.UNKNOWN);
// create the folder for the meta information
FileUtils.mkdir(rebaseState.getDir(), true);
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() || 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<>();
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
List<RevCommit> cherryPickList = calculatePickList(headCommit);
ObjectReader reader = walk.getObjectReader();
for (RevCommit commit : cherryPickList)
toDoSteps.add(new RebaseTodoLine(Action.PICK, reader
.abbreviate(commit), commit.getShortMessage()));
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
toDoSteps, false);
monitor.endTask();
// we rewind to the upstream commit
monitor.beginTask(MessageFormat.format(JGitText.get().rewinding,
upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN);
boolean checkoutOk = false;
try {
checkoutOk = checkoutCommit(headName, upstreamCommit);
} finally {
if (!checkoutOk)
FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE);
}
monitor.endTask();
return null;
}
private List<RevCommit> calculatePickList(RevCommit headCommit)
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);
if (preserveMerges) {
// When preserving merges we only rewrite commits which have at
// least one parent that is itself rewritten (or a merge base)
File rewrittenDir = rebaseState.getRewrittenDir();
FileUtils.mkdir(rewrittenDir, false);
walk.reset();
walk.setRevFilter(RevFilter.MERGE_BASE);
walk.markStart(upstreamCommit);
walk.markStart(headCommit);
RevCommit base;
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;
}
}
// commit is only merged in, needs not be rewritten
iterator.remove();
}
}
return cherryPickList;
}
private static String getHeadName(Ref head) {
String headName;
if (head.isSymbolic()) {
headName = head.getTarget().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.exactRef(Constants.HEAD);
if (head == null || head.getObjectId() == null)
throw new RefNotFoundException(MessageFormat.format(
JGitText.get().refNotResolved, Constants.HEAD));
return head;
}
private boolean isInteractive() {
return interactiveHandler != null;
}
/**
* 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 java.io.IOException
* @throws org.eclipse.jgit.api.errors.GitAPIException
*/
public RevCommit tryFastForward(RevCommit newCommit) throws IOException,
GitAPIException {
Ref head = getHead();
ObjectId headId = head.getObjectId();
if (headId == null)
throw new RefNotFoundException(MessageFormat.format(
JGitText.get().refNotResolved, Constants.HEAD));
RevCommit headCommit = walk.lookupCommit(headId);
if (walk.isMergedInto(newCommit, headCommit))
return newCommit;
String headName = getHeadName(head);
return tryFastForward(headName, headCommit, newCommit);
}
private RevCommit tryFastForward(String headName, RevCommit oldCommit,
RevCommit newCommit) throws IOException, GitAPIException {
boolean tryRebase = false;
for (RevCommit parentCommit : newCommit.getParents())
if (parentCommit.equals(oldCommit))
tryRebase = true;
if (!tryRebase)
return null;
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);
rup.setExpectedOldObjectId(oldCommit);
rup.setNewObjectId(newCommit);
rup.setRefLogMessage("Fast-forward from " + oldCommit.name() //$NON-NLS-1$
+ " to " + newCommit.name(), false); //$NON-NLS-1$
Result res = rup.update(walk);
switch (res) {
case FAST_FORWARD:
case NO_CHANGE:
case FORCED:
break;
default:
throw new IOException("Could not fast-forward"); //$NON-NLS-1$
}
}
return newCommit;
} catch (RefAlreadyExistsException | RefNotFoundException
| InvalidRefNameException | CheckoutConflictException e) {
throw new JGitInternalException(e.getMessage(), e);
}
}
private void checkParameters() throws WrongRepositoryStateException {
if (this.operation == Operation.PROCESS_STEPS) {
if (rebaseState.getFile(DONE).exists())
throw new WrongRepositoryStateException(MessageFormat.format(
JGitText.get().wrongRepositoryState, repo
.getRepositoryState().name()));
}
if (this.operation != Operation.BEGIN) {
// these operations are only possible while in a rebasing state
switch (repo.getRepositoryState()) {
case REBASING_INTERACTIVE:
case REBASING:
case REBASING_REBASING:
case REBASING_MERGE:
break;
default:
throw new WrongRepositoryStateException(MessageFormat.format(
JGitText.get().wrongRepositoryState, repo
.getRepositoryState().name()));
}
} else
switch (repo.getRepositoryState()) {
case SAFE:
if (this.upstreamCommit == null)
throw new JGitInternalException(MessageFormat
.format(JGitText.get().missingRequiredParameter,
"upstream")); //$NON-NLS-1$
return;
default:
throw new WrongRepositoryStateException(MessageFormat.format(
JGitText.get().wrongRepositoryState, repo
.getRepositoryState().name()));
}
}
private RebaseResult abort(RebaseResult result) throws IOException,
GitAPIException {
ObjectId origHead = getOriginalHead();
try {
String commitId = origHead != null ? origHead.name() : null;
monitor.beginTask(MessageFormat.format(
JGitText.get().abortingRebase, commitId),
ProgressMonitor.UNKNOWN);
DirCacheCheckout dco;
if (commitId == null)
throw new JGitInternalException(
JGitText.get().abortingRebaseFailedNoOrigHead);
ObjectId id = repo.resolve(commitId);
RevCommit commit = walk.parseCommit(id);
if (result.getStatus().equals(Status.FAILED)) {
RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD));
dco = new DirCacheCheckout(repo, head.getTree(),
repo.lockDirCache(), commit.getTree());
} else {
dco = new DirCacheCheckout(repo, repo.lockDirCache(),
commit.getTree());
}
dco.setFailOnConflict(false);
dco.checkout();
walk.close();
} finally {
monitor.endTask();
}
try {
String headName = rebaseState.readFile(HEAD_NAME);
monitor.beginTask(MessageFormat.format(
JGitText.get().resettingHead, headName),
ProgressMonitor.UNKNOWN);
Result res = null;
RefUpdate refUpdate = repo.updateRef(Constants.HEAD, false);
refUpdate.setRefLogMessage("rebase: aborting", false); //$NON-NLS-1$
if (headName.startsWith(Constants.R_REFS)) {
// update the HEAD
res = refUpdate.link(headName);
} else {
refUpdate.setNewObjectId(origHead);
res = refUpdate.forceUpdate();
}
switch (res) {
case FAST_FORWARD:
case FORCED:
case NO_CHANGE:
break;
default:
throw new JGitInternalException(
JGitText.get().abortingRebaseFailed);
}
boolean stashConflicts = autoStashApply();
// cleanup the files
FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE);
repo.writeCherryPickHead(null);
repo.writeMergeHeads(null);
if (stashConflicts)
return RebaseResult.STASH_APPLY_CONFLICTS_RESULT;
return result;
} finally {
monitor.endTask();
}
}
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 {
try {
RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD));
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) {
throw new CheckoutConflictException(dco.getConflicts(), cce);
}
// update the HEAD
RefUpdate refUpdate = repo.updateRef(Constants.HEAD, true);
refUpdate.setExpectedOldObjectId(head);
refUpdate.setNewObjectId(commit);
refUpdate.setRefLogMessage(
"checkout: moving from " //$NON-NLS-1$
+ Repository.shortenRefName(headName)
+ " to " + commit.getName(), false); //$NON-NLS-1$
Result res = refUpdate.forceUpdate();
switch (res) {
case FAST_FORWARD:
case NO_CHANGE:
case FORCED:
break;
default:
throw new IOException(
JGitText.get().couldNotRewindToUpstreamCommit);
}
} finally {
walk.close();
monitor.endTask();
}
return true;
}
/**
* Set upstream {@code RevCommit}
*
* @param upstream
* the upstream commit
* @return {@code this}
*/
public RebaseCommand setUpstream(RevCommit upstream) {
this.upstreamCommit = upstream;
this.upstreamCommitName = upstream.name();
return this;
}
/**
* Set the upstream commit
*
* @param upstream
* id of the upstream commit
* @return {@code this}
*/
public RebaseCommand setUpstream(AnyObjectId upstream) {
try {
this.upstreamCommit = walk.parseCommit(upstream);
this.upstreamCommitName = upstream.name();
} catch (IOException e) {
throw new JGitInternalException(MessageFormat.format(
JGitText.get().couldNotReadObjectWhileParsingCommit,
upstream.name()), e);
}
return this;
}
/**
* Set the upstream branch
*
* @param upstream
* the name of the upstream branch
* @return {@code this}
* @throws org.eclipse.jgit.api.errors.RefNotFoundException
*/
public RebaseCommand setUpstream(String upstream)
throws RefNotFoundException {
try {
ObjectId upstreamId = repo.resolve(upstream);
if (upstreamId == null)
throw new RefNotFoundException(MessageFormat.format(JGitText
.get().refNotResolved, upstream));
upstreamCommit = walk.parseCommit(repo.resolve(upstream));
upstreamCommitName = upstream;
return this;
} catch (IOException ioe) {
throw new JGitInternalException(ioe.getMessage(), ioe);
}
}
/**
* Optionally override the name of the upstream. If this is used, it has to
* come after any {@link #setUpstream} call.
*
* @param upstreamName
* the name which will be used to refer to upstream in conflicts
* @return {@code this}
*/
public RebaseCommand setUpstreamName(String upstreamName) {
if (upstreamCommit == null) {
throw new IllegalStateException(
"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}
*/
public RebaseCommand setOperation(Operation operation) {
this.operation = operation;
return this;
}
/**
* 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;
}
/**
* Enable interactive rebase
* <p>
* Does not stop after initialization of interactive rebase. This is
* equivalent to
* {@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) {
return runInteractively(handler, false);
}
/**
* Enable interactive rebase
* <p>
* If stopAfterRebaseInteractiveInitialization is {@code true} the rebase
* stops after initialization of interactive rebase returning
* {@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
* @since 3.2
*/
public RebaseCommand runInteractively(InteractiveHandler handler,
final boolean stopAfterRebaseInteractiveInitialization) {
this.stopAfterInitialization = stopAfterRebaseInteractiveInitialization;
this.interactiveHandler = handler;
return this;
}
/**
* Set the <code>MergeStrategy</code>.
*
* @param strategy
* The merge strategy to use during this rebase operation.
* @return {@code this}
* @since 3.4
*/
public RebaseCommand setStrategy(MergeStrategy strategy) {
this.strategy = strategy;
return this;
}
/**
* 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
* {@code true} to re-create merges during rebase. Defaults to
* {@code false}, a flattening rebase.
* @return {@code this}
* @since 3.5
*/
public RebaseCommand setPreserveMerges(boolean preserve) {
this.preserveMerges = preserve;
return this;
}
/**
* Allows to configure the interactive rebase process steps and to modify
* commit messages.
*/
public interface InteractiveHandler {
/**
* Callback API to modify the initial list of interactive rebase steps.
*
* @param steps
* initial configuration of interactive rebase
*/
void prepareSteps(List<RebaseTodoLine> steps);
/**
* Used for editing commit message on REWORD or SQUASH.
*
* @param message
* existing commit message
* @return new commit message
*/
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<>();
for (int p = 0; p < raw.length;) {
int end = RawParseUtils.nextLF(raw, p);
if (end == p)
break;
int equalsIndex = RawParseUtils.next(raw, p, '=');
if (equalsIndex == end)
break;
String key = RawParseUtils.decode(raw, p, equalsIndex - 1);
String value = RawParseUtils.decode(raw, equalsIndex + 1, end - 2);
p = end;
keyValueMap.put(key, value);
}
String name = keyValueMap.get(GIT_AUTHOR_NAME);
String email = keyValueMap.get(GIT_AUTHOR_EMAIL);
String time = keyValueMap.get(GIT_AUTHOR_DATE);
// the time is saved as <seconds since 1970> <timezone offset>
int timeStart = 0;
if (time.startsWith("@")) //$NON-NLS-1$
timeStart = 1;
else
timeStart = 0;
long when = Long
.parseLong(time.substring(timeStart, time.indexOf(' '))) * 1000;
String tzOffsetString = time.substring(time.indexOf(' ') + 1);
int multiplier = -1;
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)
return new PersonIdent(name, email, when, tz);
return null;
}
private static class RebaseState {
private final File repoDirectory;
private File dir;
public RebaseState(File repoDirectory) {
this.repoDirectory = repoDirectory;
}
public File getDir() {
if (dir == null) {
File rebaseApply = new File(repoDirectory, REBASE_APPLY);
if (rebaseApply.exists()) {
dir = rebaseApply;
} else {
File rebaseMerge = new File(repoDirectory, REBASE_MERGE);
dir = rebaseMerge;
}
}
return dir;
}
/**
* @return Directory with rewritten commit hashes, usually exists if
* {@link RebaseCommand#preserveMerges} is true
**/
public File getRewrittenDir() {
return new File(getDir(), REWRITTEN);
}
public String readFile(String name) throws IOException {
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 {
createFile(getDir(), name, content);
}
public File getFile(String name) {
return new File(getDir(), name);
}
public String getPath(String name) {
return (getDir().getName() + "/" + name); //$NON-NLS-1$
}
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);
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(content.getBytes(UTF_8));
fos.write('\n');
}
}
private static void appendToFile(File file, String content)
throws IOException {
try (FileOutputStream fos = new FileOutputStream(file, true)) {
fos.write(content.getBytes(UTF_8));
fos.write('\n');
}
}
}
}