| /* |
| * Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com> |
| * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> |
| * and other copyright owners as documented in the project's IP log. |
| * |
| * This program and the accompanying materials are made available |
| * under the terms of the Eclipse Distribution License v1.0 which |
| * accompanies this distribution, is reproduced below, and is |
| * available at http://www.eclipse.org/org/documents/edl-v10.php |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or |
| * without modification, are permitted provided that the following |
| * conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * |
| * - Neither the name of the Eclipse Foundation, Inc. nor the |
| * names of its contributors may be used to endorse or promote |
| * products derived from this software without specific prior |
| * written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
| * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
| * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| package org.eclipse.jgit.api; |
| |
| 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.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.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.MergeStrategy; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| 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 boolean stopAfterInitialization = false; |
| |
| private RevCommit newHead; |
| |
| private boolean lastStepWasForward; |
| |
| private MergeStrategy strategy = MergeStrategy.RECURSIVE; |
| |
| 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(); |
| 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 (int i = 0; i < steps.size(); i++) { |
| RebaseTodoLine step = steps.get(i); |
| 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(); |
| String newMessage = interactiveHandler |
| .modifyCommitMessage(oldMessage); |
| try (Git git = new Git(repo)) { |
| newHead = git.commit().setMessage(newMessage).setAmend(true) |
| .setNoVerify(true).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); |
| RebaseTodoLine nextStep = steps.isEmpty() ? null : steps.get(0); |
| File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP); |
| File messageSquashFile = rebaseState.getFile(MESSAGE_SQUASH); |
| if (isSquash && messageFixupFile.exists()) |
| messageFixupFile.delete(); |
| newHead = doSquashFixup(isSquash, commitToPick, nextStep, |
| messageFixupFile, messageSquashFile); |
| } |
| return null; |
| } |
| |
| 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) |
| .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); |
| 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) |
| .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, |
| RebaseTodoLine nextStep, File messageFixup, File messageSquash) |
| throws IOException, GitAPIException { |
| |
| if (!messageSquash.exists()) { |
| // init squash/fixup sequence |
| ObjectId headId = repo.resolve(Constants.HEAD); |
| RevCommit previousCommit = walk.parseCommit(headId); |
| |
| initializeSquashFixupFile(MESSAGE_SQUASH, |
| previousCommit.getFullMessage()); |
| if (!isSquash) |
| initializeSquashFixupFile(MESSAGE_FIXUP, |
| previousCommit.getFullMessage()); |
| } |
| String currSquashMessage = rebaseState |
| .readFile(MESSAGE_SQUASH); |
| |
| int count = parseSquashFixupSequenceCount(currSquashMessage) + 1; |
| |
| String content = composeSquashMessage(isSquash, |
| commitToPick, currSquashMessage, count); |
| rebaseState.createFile(MESSAGE_SQUASH, content); |
| if (messageFixup.exists()) |
| rebaseState.createFile(MESSAGE_FIXUP, content); |
| |
| return squashIntoPrevious( |
| !messageFixup.exists(), |
| nextStep); |
| } |
| |
| private void resetSoftToParent() throws IOException, |
| GitAPIException, CheckoutConflictException { |
| Ref 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, |
| RebaseTodoLine nextStep) |
| throws IOException, GitAPIException { |
| RevCommit retNewHead; |
| String commitMessage = rebaseState |
| .readFile(MESSAGE_SQUASH); |
| |
| 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 (sequenceContainsSquash) { |
| commitMessage = interactiveHandler |
| .modifyCommitMessage(commitMessage); |
| } |
| retNewHead = git.commit() |
| .setMessage(stripCommentLines(commitMessage)) |
| .setAmend(true).setNoVerify(true).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; |
| } |
| |
| 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$ |
| int bufferSize = result.length(); |
| if (bufferSize > 0 && result.charAt(bufferSize - 1) == '\n') { |
| result.deleteCharAt(bufferSize - 1); |
| } |
| } |
| return result.toString(); |
| } |
| |
| @SuppressWarnings("nls") |
| private static String composeSquashMessage(boolean isSquash, |
| RevCommit commitToPick, String currSquashMessage, int count) { |
| StringBuilder sb = new StringBuilder(); |
| String ordinal = getOrdinal(count); |
| sb.setLength(0); |
| sb.append("# This is a combination of ").append(count) |
| .append(" commits.\n"); |
| // 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()); |
| } else { |
| sb.append("# The ").append(count).append(ordinal) |
| .append(" commit message will be skipped:\n# "); |
| sb.append(commitToPick.getFullMessage().replaceAll("([\n\r])", |
| "$1# ")); |
| } |
| return sb.toString(); |
| } |
| |
| private static String getOrdinal(int count) { |
| switch (count % 10) { |
| case 1: |
| return "st"; //$NON-NLS-1$ |
| case 2: |
| return "nd"; //$NON-NLS-1$ |
| case 3: |
| return "rd"; //$NON-NLS-1$ |
| default: |
| return "th"; //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Parse the count from squashed commit messages |
| * |
| * @param currSquashMessage |
| * the squashed commit message to be parsed |
| * @return the count of squashed messages in the given string |
| */ |
| static int parseSquashFixupSequenceCount(String currSquashMessage) { |
| String regex = "This is a combination of (.*) commits"; //$NON-NLS-1$ |
| String firstLine = currSquashMessage.substring(0, |
| currSquashMessage.indexOf("\n")); //$NON-NLS-1$ |
| Pattern pattern = Pattern.compile(regex); |
| Matcher matcher = pattern.matcher(firstLine); |
| if (!matcher.find()) |
| throw new IllegalArgumentException(); |
| return Integer.parseInt(matcher.group(1)); |
| } |
| |
| private void initializeSquashFixupFile(String messageFile, |
| String fullMessage) throws IOException { |
| rebaseState |
| .createFile( |
| messageFile, |
| "# This is a combination of 1 commits.\n# The first commit's message is:\n" + fullMessage); //$NON-NLS-1$); |
| } |
| |
| private String getOurCommitName() { |
| // If onto is different from upstream, this should say "onto", but |
| // RebaseCommand doesn't support a different "onto" at the moment. |
| 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 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<>(); |
| for (RevCommit commit : commitsToUse) { |
| 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; |
| } |
| |
| /** |
| * 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 configure rebase interactive process and modify commit message |
| */ |
| public interface InteractiveHandler { |
| /** |
| * Given list of {@code steps} should be modified according to user |
| * rebase configuration |
| * @param steps |
| * initial configuration of rebase interactive |
| */ |
| void prepareSteps(List<RebaseTodoLine> steps); |
| |
| /** |
| * Used for editing commit message on REWORD |
| * |
| * @param commit |
| * @return new commit message |
| */ |
| String modifyCommitMessage(String commit); |
| } |
| |
| |
| 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'); |
| } |
| } |
| } |
| } |