RebaseCommand.java

  1. /*
  2.  * Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com>
  3.  * Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> and others
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */
  11. package org.eclipse.jgit.api;

  12. import static java.nio.charset.StandardCharsets.UTF_8;

  13. import java.io.ByteArrayOutputStream;
  14. import java.io.File;
  15. import java.io.FileNotFoundException;
  16. import java.io.FileOutputStream;
  17. import java.io.IOException;
  18. import java.text.MessageFormat;
  19. import java.util.ArrayList;
  20. import java.util.Collection;
  21. import java.util.Collections;
  22. import java.util.HashMap;
  23. import java.util.Iterator;
  24. import java.util.LinkedList;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.regex.Matcher;
  28. import java.util.regex.Pattern;

  29. import org.eclipse.jgit.annotations.NonNull;
  30. import org.eclipse.jgit.api.RebaseResult.Status;
  31. import org.eclipse.jgit.api.ResetCommand.ResetType;
  32. import org.eclipse.jgit.api.errors.CheckoutConflictException;
  33. import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
  34. import org.eclipse.jgit.api.errors.GitAPIException;
  35. import org.eclipse.jgit.api.errors.InvalidRebaseStepException;
  36. import org.eclipse.jgit.api.errors.InvalidRefNameException;
  37. import org.eclipse.jgit.api.errors.JGitInternalException;
  38. import org.eclipse.jgit.api.errors.NoHeadException;
  39. import org.eclipse.jgit.api.errors.NoMessageException;
  40. import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
  41. import org.eclipse.jgit.api.errors.RefNotFoundException;
  42. import org.eclipse.jgit.api.errors.StashApplyFailureException;
  43. import org.eclipse.jgit.api.errors.UnmergedPathsException;
  44. import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
  45. import org.eclipse.jgit.diff.DiffFormatter;
  46. import org.eclipse.jgit.dircache.DirCache;
  47. import org.eclipse.jgit.dircache.DirCacheCheckout;
  48. import org.eclipse.jgit.dircache.DirCacheIterator;
  49. import org.eclipse.jgit.errors.RevisionSyntaxException;
  50. import org.eclipse.jgit.internal.JGitText;
  51. import org.eclipse.jgit.lib.AbbreviatedObjectId;
  52. import org.eclipse.jgit.lib.AnyObjectId;
  53. import org.eclipse.jgit.lib.CommitConfig;
  54. import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
  55. import org.eclipse.jgit.lib.ConfigConstants;
  56. import org.eclipse.jgit.lib.Constants;
  57. import org.eclipse.jgit.lib.NullProgressMonitor;
  58. import org.eclipse.jgit.lib.ObjectId;
  59. import org.eclipse.jgit.lib.ObjectReader;
  60. import org.eclipse.jgit.lib.PersonIdent;
  61. import org.eclipse.jgit.lib.ProgressMonitor;
  62. import org.eclipse.jgit.lib.RebaseTodoLine;
  63. import org.eclipse.jgit.lib.RebaseTodoLine.Action;
  64. import org.eclipse.jgit.lib.Ref;
  65. import org.eclipse.jgit.lib.RefUpdate;
  66. import org.eclipse.jgit.lib.RefUpdate.Result;
  67. import org.eclipse.jgit.lib.Repository;
  68. import org.eclipse.jgit.merge.ContentMergeStrategy;
  69. import org.eclipse.jgit.merge.MergeStrategy;
  70. import org.eclipse.jgit.revwalk.RevCommit;
  71. import org.eclipse.jgit.revwalk.RevSort;
  72. import org.eclipse.jgit.revwalk.RevWalk;
  73. import org.eclipse.jgit.revwalk.filter.RevFilter;
  74. import org.eclipse.jgit.submodule.SubmoduleWalk.IgnoreSubmoduleMode;
  75. import org.eclipse.jgit.treewalk.TreeWalk;
  76. import org.eclipse.jgit.treewalk.filter.TreeFilter;
  77. import org.eclipse.jgit.util.FileUtils;
  78. import org.eclipse.jgit.util.IO;
  79. import org.eclipse.jgit.util.RawParseUtils;

  80. /**
  81.  * A class used to execute a {@code Rebase} command. It has setters for all
  82.  * supported options and arguments of this command and a {@link #call()} method
  83.  * to finally execute the command. Each instance of this class should only be
  84.  * used for one invocation of the command (means: one call to {@link #call()})
  85.  * <p>
  86.  *
  87.  * @see <a
  88.  *      href="http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html"
  89.  *      >Git documentation about Rebase</a>
  90.  */
  91. public class RebaseCommand extends GitCommand<RebaseResult> {
  92.     /**
  93.      * The name of the "rebase-merge" folder for interactive rebases.
  94.      */
  95.     public static final String REBASE_MERGE = "rebase-merge"; //$NON-NLS-1$

  96.     /**
  97.      * The name of the "rebase-apply" folder for non-interactive rebases.
  98.      */
  99.     private static final String REBASE_APPLY = "rebase-apply"; //$NON-NLS-1$

  100.     /**
  101.      * The name of the "stopped-sha" file
  102.      */
  103.     public static final String STOPPED_SHA = "stopped-sha"; //$NON-NLS-1$

  104.     private static final String AUTHOR_SCRIPT = "author-script"; //$NON-NLS-1$

  105.     private static final String DONE = "done"; //$NON-NLS-1$

  106.     private static final String GIT_AUTHOR_DATE = "GIT_AUTHOR_DATE"; //$NON-NLS-1$

  107.     private static final String GIT_AUTHOR_EMAIL = "GIT_AUTHOR_EMAIL"; //$NON-NLS-1$

  108.     private static final String GIT_AUTHOR_NAME = "GIT_AUTHOR_NAME"; //$NON-NLS-1$

  109.     private static final String GIT_REBASE_TODO = "git-rebase-todo"; //$NON-NLS-1$

  110.     private static final String HEAD_NAME = "head-name"; //$NON-NLS-1$

  111.     private static final String INTERACTIVE = "interactive"; //$NON-NLS-1$

  112.     private static final String QUIET = "quiet"; //$NON-NLS-1$

  113.     private static final String MESSAGE = "message"; //$NON-NLS-1$

  114.     private static final String ONTO = "onto"; //$NON-NLS-1$

  115.     private static final String ONTO_NAME = "onto_name"; //$NON-NLS-1$

  116.     private static final String PATCH = "patch"; //$NON-NLS-1$

  117.     private static final String REBASE_HEAD = "orig-head"; //$NON-NLS-1$

  118.     /** Pre git 1.7.6 file name for {@link #REBASE_HEAD}. */
  119.     private static final String REBASE_HEAD_LEGACY = "head"; //$NON-NLS-1$

  120.     private static final String AMEND = "amend"; //$NON-NLS-1$

  121.     private static final String MESSAGE_FIXUP = "message-fixup"; //$NON-NLS-1$

  122.     private static final String MESSAGE_SQUASH = "message-squash"; //$NON-NLS-1$

  123.     private static final String AUTOSTASH = "autostash"; //$NON-NLS-1$

  124.     private static final String AUTOSTASH_MSG = "On {0}: autostash"; //$NON-NLS-1$

  125.     /**
  126.      * The folder containing the hashes of (potentially) rewritten commits when
  127.      * --preserve-merges is used.
  128.      * <p>
  129.      * Native git rebase --merge uses a <em>file</em> of that name to record
  130.      * commits to copy notes at the end of the whole rebase.
  131.      * </p>
  132.      */
  133.     private static final String REWRITTEN = "rewritten"; //$NON-NLS-1$

  134.     /**
  135.      * File containing the current commit(s) to cherry pick when --preserve-merges
  136.      * is used.
  137.      */
  138.     private static final String CURRENT_COMMIT = "current-commit"; //$NON-NLS-1$

  139.     private static final String REFLOG_PREFIX = "rebase:"; //$NON-NLS-1$

  140.     /**
  141.      * The available operations
  142.      */
  143.     public enum Operation {
  144.         /**
  145.          * Initiates rebase
  146.          */
  147.         BEGIN,
  148.         /**
  149.          * Continues after a conflict resolution
  150.          */
  151.         CONTINUE,
  152.         /**
  153.          * Skips the "current" commit
  154.          */
  155.         SKIP,
  156.         /**
  157.          * Aborts and resets the current rebase
  158.          */
  159.         ABORT,
  160.         /**
  161.          * Starts processing steps
  162.          * @since 3.2
  163.          */
  164.         PROCESS_STEPS;
  165.     }

  166.     private Operation operation = Operation.BEGIN;

  167.     private RevCommit upstreamCommit;

  168.     private String upstreamCommitName;

  169.     private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;

  170.     private final RevWalk walk;

  171.     private final RebaseState rebaseState;

  172.     private InteractiveHandler interactiveHandler;

  173.     private CommitConfig commitConfig;

  174.     private boolean stopAfterInitialization = false;

  175.     private RevCommit newHead;

  176.     private boolean lastStepWasForward;

  177.     private MergeStrategy strategy = MergeStrategy.RECURSIVE;

  178.     private ContentMergeStrategy contentStrategy;

  179.     private boolean preserveMerges = false;

  180.     /**
  181.      * <p>
  182.      * Constructor for RebaseCommand.
  183.      * </p>
  184.      *
  185.      * @param repo
  186.      *            the {@link org.eclipse.jgit.lib.Repository}
  187.      */
  188.     protected RebaseCommand(Repository repo) {
  189.         super(repo);
  190.         walk = new RevWalk(repo);
  191.         rebaseState = new RebaseState(repo.getDirectory());
  192.     }

  193.     /**
  194.      * {@inheritDoc}
  195.      * <p>
  196.      * Executes the {@code Rebase} command with all the options and parameters
  197.      * collected by the setter methods of this class. Each instance of this
  198.      * class should only be used for one invocation of the command. Don't call
  199.      * this method twice on an instance.
  200.      */
  201.     @Override
  202.     public RebaseResult call() throws GitAPIException, NoHeadException,
  203.             RefNotFoundException, WrongRepositoryStateException {
  204.         newHead = null;
  205.         lastStepWasForward = false;
  206.         checkCallable();
  207.         checkParameters();
  208.         commitConfig = repo.getConfig().get(CommitConfig.KEY);
  209.         try {
  210.             switch (operation) {
  211.             case ABORT:
  212.                 try {
  213.                     return abort(RebaseResult.ABORTED_RESULT);
  214.                 } catch (IOException ioe) {
  215.                     throw new JGitInternalException(ioe.getMessage(), ioe);
  216.                 }
  217.             case PROCESS_STEPS:
  218.             case SKIP:
  219.             case CONTINUE:
  220.                 String upstreamCommitId = rebaseState.readFile(ONTO);
  221.                 try {
  222.                     upstreamCommitName = rebaseState.readFile(ONTO_NAME);
  223.                 } catch (FileNotFoundException e) {
  224.                     // Fall back to commit ID if file doesn't exist (e.g. rebase
  225.                     // was started by C Git)
  226.                     upstreamCommitName = upstreamCommitId;
  227.                 }
  228.                 this.upstreamCommit = walk.parseCommit(repo
  229.                         .resolve(upstreamCommitId));
  230.                 preserveMerges = rebaseState.getRewrittenDir().isDirectory();
  231.                 break;
  232.             case BEGIN:
  233.                 autoStash();
  234.                 if (stopAfterInitialization
  235.                         || !walk.isMergedInto(
  236.                                 walk.parseCommit(repo.resolve(Constants.HEAD)),
  237.                                 upstreamCommit)) {
  238.                     org.eclipse.jgit.api.Status status = Git.wrap(repo)
  239.                             .status().setIgnoreSubmodules(IgnoreSubmoduleMode.ALL).call();
  240.                     if (status.hasUncommittedChanges()) {
  241.                         List<String> list = new ArrayList<>();
  242.                         list.addAll(status.getUncommittedChanges());
  243.                         return RebaseResult.uncommittedChanges(list);
  244.                     }
  245.                 }
  246.                 RebaseResult res = initFilesAndRewind();
  247.                 if (stopAfterInitialization)
  248.                     return RebaseResult.INTERACTIVE_PREPARED_RESULT;
  249.                 if (res != null) {
  250.                     autoStashApply();
  251.                     if (rebaseState.getDir().exists())
  252.                         FileUtils.delete(rebaseState.getDir(),
  253.                                 FileUtils.RECURSIVE);
  254.                     return res;
  255.                 }
  256.             }

  257.             if (monitor.isCancelled())
  258.                 return abort(RebaseResult.ABORTED_RESULT);

  259.             if (operation == Operation.CONTINUE) {
  260.                 newHead = continueRebase();
  261.                 List<RebaseTodoLine> doneLines = repo.readRebaseTodo(
  262.                         rebaseState.getPath(DONE), true);
  263.                 RebaseTodoLine step = doneLines.get(doneLines.size() - 1);
  264.                 if (newHead != null
  265.                         && step.getAction() != Action.PICK) {
  266.                     RebaseTodoLine newStep = new RebaseTodoLine(
  267.                             step.getAction(),
  268.                             AbbreviatedObjectId.fromObjectId(newHead),
  269.                             step.getShortMessage());
  270.                     RebaseResult result = processStep(newStep, false);
  271.                     if (result != null)
  272.                         return result;
  273.                 }
  274.                 File amendFile = rebaseState.getFile(AMEND);
  275.                 boolean amendExists = amendFile.exists();
  276.                 if (amendExists) {
  277.                     FileUtils.delete(amendFile);
  278.                 }
  279.                 if (newHead == null && !amendExists) {
  280.                     // continueRebase() returns null only if no commit was
  281.                     // neccessary. This means that no changes where left over
  282.                     // after resolving all conflicts. In this case, cgit stops
  283.                     // and displays a nice message to the user, telling him to
  284.                     // either do changes or skip the commit instead of continue.
  285.                     return RebaseResult.NOTHING_TO_COMMIT_RESULT;
  286.                 }
  287.             }

  288.             if (operation == Operation.SKIP)
  289.                 newHead = checkoutCurrentHead();

  290.             List<RebaseTodoLine> steps = repo.readRebaseTodo(
  291.                     rebaseState.getPath(GIT_REBASE_TODO), false);
  292.             if (steps.isEmpty()) {
  293.                 return finishRebase(walk.parseCommit(repo.resolve(Constants.HEAD)), false);
  294.             }
  295.             if (isInteractive()) {
  296.                 interactiveHandler.prepareSteps(steps);
  297.                 repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
  298.                         steps, false);
  299.             }
  300.             checkSteps(steps);
  301.             for (RebaseTodoLine step : steps) {
  302.                 popSteps(1);
  303.                 RebaseResult result = processStep(step, true);
  304.                 if (result != null) {
  305.                     return result;
  306.                 }
  307.             }
  308.             return finishRebase(newHead, lastStepWasForward);
  309.         } catch (CheckoutConflictException cce) {
  310.             return RebaseResult.conflicts(cce.getConflictingPaths());
  311.         } catch (IOException ioe) {
  312.             throw new JGitInternalException(ioe.getMessage(), ioe);
  313.         }
  314.     }

  315.     private void autoStash() throws GitAPIException, IOException {
  316.         if (repo.getConfig().getBoolean(ConfigConstants.CONFIG_REBASE_SECTION,
  317.                 ConfigConstants.CONFIG_KEY_AUTOSTASH, false)) {
  318.             String message = MessageFormat.format(
  319.                             AUTOSTASH_MSG,
  320.                             Repository
  321.                                     .shortenRefName(getHeadName(getHead())));
  322.             RevCommit stashCommit = Git.wrap(repo).stashCreate().setRef(null)
  323.                     .setWorkingDirectoryMessage(
  324.                             message)
  325.                     .call();
  326.             if (stashCommit != null) {
  327.                 FileUtils.mkdir(rebaseState.getDir());
  328.                 rebaseState.createFile(AUTOSTASH, stashCommit.getName());
  329.             }
  330.         }
  331.     }

  332.     private boolean autoStashApply() throws IOException, GitAPIException {
  333.         boolean conflicts = false;
  334.         if (rebaseState.getFile(AUTOSTASH).exists()) {
  335.             String stash = rebaseState.readFile(AUTOSTASH);
  336.             try (Git git = Git.wrap(repo)) {
  337.                 git.stashApply().setStashRef(stash)
  338.                         .ignoreRepositoryState(true).setStrategy(strategy)
  339.                         .call();
  340.             } catch (StashApplyFailureException e) {
  341.                 conflicts = true;
  342.                 try (RevWalk rw = new RevWalk(repo)) {
  343.                     ObjectId stashId = repo.resolve(stash);
  344.                     RevCommit commit = rw.parseCommit(stashId);
  345.                     updateStashRef(commit, commit.getAuthorIdent(),
  346.                             commit.getShortMessage());
  347.                 }
  348.             }
  349.         }
  350.         return conflicts;
  351.     }

  352.     private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent,
  353.             String refLogMessage) throws IOException {
  354.         Ref currentRef = repo.exactRef(Constants.R_STASH);
  355.         RefUpdate refUpdate = repo.updateRef(Constants.R_STASH);
  356.         refUpdate.setNewObjectId(commitId);
  357.         refUpdate.setRefLogIdent(refLogIdent);
  358.         refUpdate.setRefLogMessage(refLogMessage, false);
  359.         refUpdate.setForceRefLog(true);
  360.         if (currentRef != null)
  361.             refUpdate.setExpectedOldObjectId(currentRef.getObjectId());
  362.         else
  363.             refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
  364.         refUpdate.forceUpdate();
  365.     }

  366.     private RebaseResult processStep(RebaseTodoLine step, boolean shouldPick)
  367.             throws IOException, GitAPIException {
  368.         if (Action.COMMENT.equals(step.getAction()))
  369.             return null;
  370.         if (preserveMerges
  371.                 && shouldPick
  372.                 && (Action.EDIT.equals(step.getAction()) || Action.PICK
  373.                         .equals(step.getAction()))) {
  374.             writeRewrittenHashes();
  375.         }
  376.         ObjectReader or = repo.newObjectReader();

  377.         Collection<ObjectId> ids = or.resolve(step.getCommit());
  378.         if (ids.size() != 1)
  379.             throw new JGitInternalException(
  380.                     JGitText.get().cannotResolveUniquelyAbbrevObjectId);
  381.         RevCommit commitToPick = walk.parseCommit(ids.iterator().next());
  382.         if (shouldPick) {
  383.             if (monitor.isCancelled())
  384.                 return RebaseResult.result(Status.STOPPED, commitToPick);
  385.             RebaseResult result = cherryPickCommit(commitToPick);
  386.             if (result != null)
  387.                 return result;
  388.         }
  389.         boolean isSquash = false;
  390.         switch (step.getAction()) {
  391.         case PICK:
  392.             return null; // continue rebase process on pick command
  393.         case REWORD:
  394.             String oldMessage = commitToPick.getFullMessage();
  395.             CleanupMode mode = commitConfig.resolve(CleanupMode.DEFAULT, true);
  396.             boolean[] doChangeId = { false };
  397.             String newMessage = editCommitMessage(doChangeId, oldMessage, mode,
  398.                     commitConfig.getCommentChar(oldMessage));
  399.             try (Git git = new Git(repo)) {
  400.                 newHead = git.commit()
  401.                         .setMessage(newMessage)
  402.                         .setAmend(true)
  403.                         .setNoVerify(true)
  404.                         .setInsertChangeId(doChangeId[0])
  405.                         .call();
  406.             }
  407.             return null;
  408.         case EDIT:
  409.             rebaseState.createFile(AMEND, commitToPick.name());
  410.             return stop(commitToPick, Status.EDIT);
  411.         case COMMENT:
  412.             break;
  413.         case SQUASH:
  414.             isSquash = true;
  415.             //$FALL-THROUGH$
  416.         case FIXUP:
  417.             resetSoftToParent();
  418.             List<RebaseTodoLine> steps = repo.readRebaseTodo(
  419.                     rebaseState.getPath(GIT_REBASE_TODO), false);
  420.             boolean isLast = steps.isEmpty();
  421.             if (!isLast) {
  422.                 switch (steps.get(0).getAction()) {
  423.                 case FIXUP:
  424.                 case SQUASH:
  425.                     break;
  426.                 default:
  427.                     isLast = true;
  428.                     break;
  429.                 }
  430.             }
  431.             File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP);
  432.             File messageSquashFile = rebaseState.getFile(MESSAGE_SQUASH);
  433.             if (isSquash && messageFixupFile.exists()) {
  434.                 messageFixupFile.delete();
  435.             }
  436.             newHead = doSquashFixup(isSquash, commitToPick, isLast,
  437.                     messageFixupFile, messageSquashFile);
  438.         }
  439.         return null;
  440.     }

  441.     private String editCommitMessage(boolean[] doChangeId, String message,
  442.             @NonNull CleanupMode mode, char commentChar) {
  443.         String newMessage;
  444.         CommitConfig.CleanupMode cleanup;
  445.         if (interactiveHandler instanceof InteractiveHandler2) {
  446.             InteractiveHandler2.ModifyResult modification = ((InteractiveHandler2) interactiveHandler)
  447.                     .editCommitMessage(message, mode, commentChar);
  448.             newMessage = modification.getMessage();
  449.             cleanup = modification.getCleanupMode();
  450.             if (CleanupMode.DEFAULT.equals(cleanup)) {
  451.                 cleanup = mode;
  452.             }
  453.             doChangeId[0] = modification.shouldAddChangeId();
  454.         } else {
  455.             newMessage = interactiveHandler.modifyCommitMessage(message);
  456.             cleanup = CommitConfig.CleanupMode.STRIP;
  457.             doChangeId[0] = false;
  458.         }
  459.         return CommitConfig.cleanText(newMessage, cleanup, commentChar);
  460.     }

  461.     private RebaseResult cherryPickCommit(RevCommit commitToPick)
  462.             throws IOException, GitAPIException, NoMessageException,
  463.             UnmergedPathsException, ConcurrentRefUpdateException,
  464.             WrongRepositoryStateException, NoHeadException {
  465.         try {
  466.             monitor.beginTask(MessageFormat.format(
  467.                     JGitText.get().applyingCommit,
  468.                     commitToPick.getShortMessage()), ProgressMonitor.UNKNOWN);
  469.             if (preserveMerges) {
  470.                 return cherryPickCommitPreservingMerges(commitToPick);
  471.             }
  472.             return cherryPickCommitFlattening(commitToPick);
  473.         } finally {
  474.             monitor.endTask();
  475.         }
  476.     }

  477.     private RebaseResult cherryPickCommitFlattening(RevCommit commitToPick)
  478.             throws IOException, GitAPIException, NoMessageException,
  479.             UnmergedPathsException, ConcurrentRefUpdateException,
  480.             WrongRepositoryStateException, NoHeadException {
  481.         // If the first parent of commitToPick is the current HEAD,
  482.         // we do a fast-forward instead of cherry-pick to avoid
  483.         // unnecessary object rewriting
  484.         newHead = tryFastForward(commitToPick);
  485.         lastStepWasForward = newHead != null;
  486.         if (!lastStepWasForward) {
  487.             // TODO if the content of this commit is already merged
  488.             // here we should skip this step in order to avoid
  489.             // confusing pseudo-changed
  490.             String ourCommitName = getOurCommitName();
  491.             try (Git git = new Git(repo)) {
  492.                 CherryPickResult cherryPickResult = git.cherryPick()
  493.                     .include(commitToPick)
  494.                     .setOurCommitName(ourCommitName)
  495.                     .setReflogPrefix(REFLOG_PREFIX)
  496.                     .setStrategy(strategy)
  497.                     .setContentMergeStrategy(contentStrategy)
  498.                     .call();
  499.                 switch (cherryPickResult.getStatus()) {
  500.                 case FAILED:
  501.                     if (operation == Operation.BEGIN) {
  502.                         return abort(RebaseResult
  503.                                 .failed(cherryPickResult.getFailingPaths()));
  504.                     }
  505.                     return stop(commitToPick, Status.STOPPED);
  506.                 case CONFLICTING:
  507.                     return stop(commitToPick, Status.STOPPED);
  508.                 case OK:
  509.                     newHead = cherryPickResult.getNewHead();
  510.                 }
  511.             }
  512.         }
  513.         return null;
  514.     }

  515.     private RebaseResult cherryPickCommitPreservingMerges(RevCommit commitToPick)
  516.             throws IOException, GitAPIException, NoMessageException,
  517.             UnmergedPathsException, ConcurrentRefUpdateException,
  518.             WrongRepositoryStateException, NoHeadException {

  519.         writeCurrentCommit(commitToPick);

  520.         List<RevCommit> newParents = getNewParents(commitToPick);
  521.         boolean otherParentsUnchanged = true;
  522.         for (int i = 1; i < commitToPick.getParentCount(); i++)
  523.             otherParentsUnchanged &= newParents.get(i).equals(
  524.                     commitToPick.getParent(i));
  525.         // If the first parent of commitToPick is the current HEAD,
  526.         // we do a fast-forward instead of cherry-pick to avoid
  527.         // unnecessary object rewriting
  528.         newHead = otherParentsUnchanged ? tryFastForward(commitToPick) : null;
  529.         lastStepWasForward = newHead != null;
  530.         if (!lastStepWasForward) {
  531.             ObjectId headId = getHead().getObjectId();
  532.             // getHead() checks for null
  533.             assert headId != null;
  534.             if (!AnyObjectId.isEqual(headId, newParents.get(0)))
  535.                 checkoutCommit(headId.getName(), newParents.get(0));

  536.             // Use the cherry-pick strategy if all non-first parents did not
  537.             // change. This is different from C Git, which always uses the merge
  538.             // strategy (see below).
  539.             try (Git git = new Git(repo)) {
  540.                 if (otherParentsUnchanged) {
  541.                     boolean isMerge = commitToPick.getParentCount() > 1;
  542.                     String ourCommitName = getOurCommitName();
  543.                     CherryPickCommand pickCommand = git.cherryPick()
  544.                             .include(commitToPick)
  545.                             .setOurCommitName(ourCommitName)
  546.                             .setReflogPrefix(REFLOG_PREFIX)
  547.                             .setStrategy(strategy)
  548.                             .setContentMergeStrategy(contentStrategy);
  549.                     if (isMerge) {
  550.                         pickCommand.setMainlineParentNumber(1);
  551.                         // We write a MERGE_HEAD and later commit explicitly
  552.                         pickCommand.setNoCommit(true);
  553.                         writeMergeInfo(commitToPick, newParents);
  554.                     }
  555.                     CherryPickResult cherryPickResult = pickCommand.call();
  556.                     switch (cherryPickResult.getStatus()) {
  557.                     case FAILED:
  558.                         if (operation == Operation.BEGIN) {
  559.                             return abort(RebaseResult.failed(
  560.                                     cherryPickResult.getFailingPaths()));
  561.                         }
  562.                         return stop(commitToPick, Status.STOPPED);
  563.                     case CONFLICTING:
  564.                         return stop(commitToPick, Status.STOPPED);
  565.                     case OK:
  566.                         if (isMerge) {
  567.                             // Commit the merge (setup above using
  568.                             // writeMergeInfo())
  569.                             CommitCommand commit = git.commit();
  570.                             commit.setAuthor(commitToPick.getAuthorIdent());
  571.                             commit.setReflogComment(REFLOG_PREFIX + " " //$NON-NLS-1$
  572.                                     + commitToPick.getShortMessage());
  573.                             newHead = commit.call();
  574.                         } else
  575.                             newHead = cherryPickResult.getNewHead();
  576.                         break;
  577.                     }
  578.                 } else {
  579.                     // Use the merge strategy to redo merges, which had some of
  580.                     // their non-first parents rewritten
  581.                     MergeCommand merge = git.merge()
  582.                             .setFastForward(MergeCommand.FastForwardMode.NO_FF)
  583.                             .setProgressMonitor(monitor)
  584.                             .setStrategy(strategy)
  585.                             .setContentMergeStrategy(contentStrategy)
  586.                             .setCommit(false);
  587.                     for (int i = 1; i < commitToPick.getParentCount(); i++)
  588.                         merge.include(newParents.get(i));
  589.                     MergeResult mergeResult = merge.call();
  590.                     if (mergeResult.getMergeStatus().isSuccessful()) {
  591.                         CommitCommand commit = git.commit();
  592.                         commit.setAuthor(commitToPick.getAuthorIdent());
  593.                         commit.setMessage(commitToPick.getFullMessage());
  594.                         commit.setReflogComment(REFLOG_PREFIX + " " //$NON-NLS-1$
  595.                                 + commitToPick.getShortMessage());
  596.                         newHead = commit.call();
  597.                     } else {
  598.                         if (operation == Operation.BEGIN && mergeResult
  599.                                 .getMergeStatus() == MergeResult.MergeStatus.FAILED)
  600.                             return abort(RebaseResult
  601.                                     .failed(mergeResult.getFailingPaths()));
  602.                         return stop(commitToPick, Status.STOPPED);
  603.                     }
  604.                 }
  605.             }
  606.         }
  607.         return null;
  608.     }

  609.     // Prepare MERGE_HEAD and message for the next commit
  610.     private void writeMergeInfo(RevCommit commitToPick,
  611.             List<RevCommit> newParents) throws IOException {
  612.         repo.writeMergeHeads(newParents.subList(1, newParents.size()));
  613.         repo.writeMergeCommitMsg(commitToPick.getFullMessage());
  614.     }

  615.     // Get the rewritten equivalents for the parents of the given commit
  616.     private List<RevCommit> getNewParents(RevCommit commitToPick)
  617.             throws IOException {
  618.         List<RevCommit> newParents = new ArrayList<>();
  619.         for (int p = 0; p < commitToPick.getParentCount(); p++) {
  620.             String parentHash = commitToPick.getParent(p).getName();
  621.             if (!new File(rebaseState.getRewrittenDir(), parentHash).exists())
  622.                 newParents.add(commitToPick.getParent(p));
  623.             else {
  624.                 String newParent = RebaseState.readFile(
  625.                         rebaseState.getRewrittenDir(), parentHash);
  626.                 if (newParent.length() == 0)
  627.                     newParents.add(walk.parseCommit(repo
  628.                             .resolve(Constants.HEAD)));
  629.                 else
  630.                     newParents.add(walk.parseCommit(ObjectId
  631.                             .fromString(newParent)));
  632.             }
  633.         }
  634.         return newParents;
  635.     }

  636.     private void writeCurrentCommit(RevCommit commit) throws IOException {
  637.         RebaseState.appendToFile(rebaseState.getFile(CURRENT_COMMIT),
  638.                 commit.name());
  639.     }

  640.     private void writeRewrittenHashes() throws RevisionSyntaxException,
  641.             IOException, RefNotFoundException {
  642.         File currentCommitFile = rebaseState.getFile(CURRENT_COMMIT);
  643.         if (!currentCommitFile.exists())
  644.             return;

  645.         ObjectId headId = getHead().getObjectId();
  646.         // getHead() checks for null
  647.         assert headId != null;
  648.         String head = headId.getName();
  649.         String currentCommits = rebaseState.readFile(CURRENT_COMMIT);
  650.         for (String current : currentCommits.split("\n")) //$NON-NLS-1$
  651.             RebaseState
  652.                     .createFile(rebaseState.getRewrittenDir(), current, head);
  653.         FileUtils.delete(currentCommitFile);
  654.     }

  655.     private RebaseResult finishRebase(RevCommit finalHead,
  656.             boolean lastStepIsForward) throws IOException, GitAPIException {
  657.         String headName = rebaseState.readFile(HEAD_NAME);
  658.         updateHead(headName, finalHead, upstreamCommit);
  659.         boolean stashConflicts = autoStashApply();
  660.         getRepository().autoGC(monitor);
  661.         FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE);
  662.         if (stashConflicts)
  663.             return RebaseResult.STASH_APPLY_CONFLICTS_RESULT;
  664.         if (lastStepIsForward || finalHead == null)
  665.             return RebaseResult.FAST_FORWARD_RESULT;
  666.         return RebaseResult.OK_RESULT;
  667.     }

  668.     private void checkSteps(List<RebaseTodoLine> steps)
  669.             throws InvalidRebaseStepException, IOException {
  670.         if (steps.isEmpty())
  671.             return;
  672.         if (RebaseTodoLine.Action.SQUASH.equals(steps.get(0).getAction())
  673.                 || RebaseTodoLine.Action.FIXUP.equals(steps.get(0).getAction())) {
  674.             if (!rebaseState.getFile(DONE).exists()
  675.                     || rebaseState.readFile(DONE).trim().length() == 0) {
  676.                 throw new InvalidRebaseStepException(MessageFormat.format(
  677.                         JGitText.get().cannotSquashFixupWithoutPreviousCommit,
  678.                         steps.get(0).getAction().name()));
  679.             }
  680.         }

  681.     }

  682.     private RevCommit doSquashFixup(boolean isSquash, RevCommit commitToPick,
  683.             boolean isLast, File messageFixup, File messageSquash)
  684.             throws IOException, GitAPIException {

  685.         if (!messageSquash.exists()) {
  686.             // init squash/fixup sequence
  687.             ObjectId headId = repo.resolve(Constants.HEAD);
  688.             RevCommit previousCommit = walk.parseCommit(headId);

  689.             initializeSquashFixupFile(MESSAGE_SQUASH,
  690.                     previousCommit.getFullMessage());
  691.             if (!isSquash) {
  692.                 rebaseState.createFile(MESSAGE_FIXUP,
  693.                         previousCommit.getFullMessage());
  694.             }
  695.         }
  696.         String currSquashMessage = rebaseState.readFile(MESSAGE_SQUASH);

  697.         int count = parseSquashFixupSequenceCount(currSquashMessage) + 1;

  698.         String content = composeSquashMessage(isSquash,
  699.                 commitToPick, currSquashMessage, count);
  700.         rebaseState.createFile(MESSAGE_SQUASH, content);

  701.         return squashIntoPrevious(!messageFixup.exists(), isLast);
  702.     }

  703.     private void resetSoftToParent() throws IOException,
  704.             GitAPIException, CheckoutConflictException {
  705.         Ref ref = repo.exactRef(Constants.ORIG_HEAD);
  706.         ObjectId orig_head = ref == null ? null : ref.getObjectId();
  707.         try (Git git = Git.wrap(repo)) {
  708.             // we have already committed the cherry-picked commit.
  709.             // what we need is to have changes introduced by this
  710.             // commit to be on the index
  711.             // resetting is a workaround
  712.             git.reset().setMode(ResetType.SOFT)
  713.                     .setRef("HEAD~1").call(); //$NON-NLS-1$
  714.         } finally {
  715.             // set ORIG_HEAD back to where we started because soft
  716.             // reset moved it
  717.             repo.writeOrigHead(orig_head);
  718.         }
  719.     }

  720.     private RevCommit squashIntoPrevious(boolean sequenceContainsSquash,
  721.             boolean isLast)
  722.             throws IOException, GitAPIException {
  723.         RevCommit retNewHead;
  724.         String commitMessage;
  725.         if (!isLast || sequenceContainsSquash) {
  726.             commitMessage = rebaseState.readFile(MESSAGE_SQUASH);
  727.         } else {
  728.             commitMessage = rebaseState.readFile(MESSAGE_FIXUP);
  729.         }
  730.         try (Git git = new Git(repo)) {
  731.             if (isLast) {
  732.                 boolean[] doChangeId = { false };
  733.                 if (sequenceContainsSquash) {
  734.                     char commentChar = commitMessage.charAt(0);
  735.                     commitMessage = editCommitMessage(doChangeId, commitMessage,
  736.                             CleanupMode.STRIP, commentChar);
  737.                 }
  738.                 retNewHead = git.commit()
  739.                         .setMessage(commitMessage)
  740.                         .setAmend(true)
  741.                         .setNoVerify(true)
  742.                         .setInsertChangeId(doChangeId[0])
  743.                         .call();
  744.                 rebaseState.getFile(MESSAGE_SQUASH).delete();
  745.                 rebaseState.getFile(MESSAGE_FIXUP).delete();
  746.             } else {
  747.                 // Next step is either Squash or Fixup
  748.                 retNewHead = git.commit().setMessage(commitMessage)
  749.                         .setAmend(true).setNoVerify(true).call();
  750.             }
  751.         }
  752.         return retNewHead;
  753.     }

  754.     @SuppressWarnings("nls")
  755.     private String composeSquashMessage(boolean isSquash,
  756.             RevCommit commitToPick, String currSquashMessage, int count) {
  757.         StringBuilder sb = new StringBuilder();
  758.         String ordinal = getOrdinal(count);
  759.         // currSquashMessage is always non-empty here, and the first character
  760.         // is the comment character used so far.
  761.         char commentChar = currSquashMessage.charAt(0);
  762.         String newMessage = commitToPick.getFullMessage();
  763.         if (!isSquash) {
  764.             sb.append(commentChar).append(" This is a combination of ")
  765.                     .append(count).append(" commits.\n");
  766.             // Add the previous message without header (i.e first line)
  767.             sb.append(currSquashMessage
  768.                     .substring(currSquashMessage.indexOf('\n') + 1));
  769.             sb.append('\n');
  770.             sb.append(commentChar).append(" The ").append(count).append(ordinal)
  771.                     .append(" commit message will be skipped:\n")
  772.                     .append(commentChar).append(' ');
  773.             sb.append(newMessage.replaceAll("([\n\r])",
  774.                     "$1" + commentChar + ' '));
  775.         } else {
  776.             String currentMessage = currSquashMessage;
  777.             if (commitConfig.isAutoCommentChar()) {
  778.                 // Figure out a new comment character taking into account the
  779.                 // new message
  780.                 String cleaned = CommitConfig.cleanText(currentMessage,
  781.                         CommitConfig.CleanupMode.STRIP, commentChar) + '\n'
  782.                         + newMessage;
  783.                 char newCommentChar = commitConfig.getCommentChar(cleaned);
  784.                 if (newCommentChar != commentChar) {
  785.                     currentMessage = replaceCommentChar(currentMessage,
  786.                             commentChar, newCommentChar);
  787.                     commentChar = newCommentChar;
  788.                 }
  789.             }
  790.             sb.append(commentChar).append(" This is a combination of ")
  791.                     .append(count).append(" commits.\n");
  792.             // Add the previous message without header (i.e first line)
  793.             sb.append(
  794.                     currentMessage.substring(currentMessage.indexOf('\n') + 1));
  795.             sb.append('\n');
  796.             sb.append(commentChar).append(" This is the ").append(count)
  797.                     .append(ordinal).append(" commit message:\n");
  798.             sb.append(newMessage);
  799.         }
  800.         return sb.toString();
  801.     }

  802.     private String replaceCommentChar(String message, char oldChar,
  803.             char newChar) {
  804.         // (?m) - Switch on multi-line matching; \h - horizontal whitespace
  805.         return message.replaceAll("(?m)^(\\h*)" + oldChar, "$1" + newChar); //$NON-NLS-1$ //$NON-NLS-2$
  806.     }

  807.     private static String getOrdinal(int count) {
  808.         switch (count % 10) {
  809.         case 1:
  810.             return "st"; //$NON-NLS-1$
  811.         case 2:
  812.             return "nd"; //$NON-NLS-1$
  813.         case 3:
  814.             return "rd"; //$NON-NLS-1$
  815.         default:
  816.             return "th"; //$NON-NLS-1$
  817.         }
  818.     }

  819.     /**
  820.      * Parse the count from squashed commit messages
  821.      *
  822.      * @param currSquashMessage
  823.      *            the squashed commit message to be parsed
  824.      * @return the count of squashed messages in the given string
  825.      */
  826.     static int parseSquashFixupSequenceCount(String currSquashMessage) {
  827.         String regex = "This is a combination of (.*) commits"; //$NON-NLS-1$
  828.         String firstLine = currSquashMessage.substring(0,
  829.                 currSquashMessage.indexOf('\n'));
  830.         Pattern pattern = Pattern.compile(regex);
  831.         Matcher matcher = pattern.matcher(firstLine);
  832.         if (!matcher.find())
  833.             throw new IllegalArgumentException();
  834.         return Integer.parseInt(matcher.group(1));
  835.     }

  836.     private void initializeSquashFixupFile(String messageFile,
  837.             String fullMessage) throws IOException {
  838.         char commentChar = commitConfig.getCommentChar(fullMessage);
  839.         rebaseState.createFile(messageFile,
  840.                 commentChar + " This is a combination of 1 commits.\n" //$NON-NLS-1$
  841.                         + commentChar + " The first commit's message is:\n" //$NON-NLS-1$
  842.                         + fullMessage);
  843.     }

  844.     private String getOurCommitName() {
  845.         // If onto is different from upstream, this should say "onto", but
  846.         // RebaseCommand doesn't support a different "onto" at the moment.
  847.         String ourCommitName = "Upstream, based on " //$NON-NLS-1$
  848.                 + Repository.shortenRefName(upstreamCommitName);
  849.         return ourCommitName;
  850.     }

  851.     private void updateHead(String headName, RevCommit aNewHead, RevCommit onto)
  852.             throws IOException {
  853.         // point the previous head (if any) to the new commit

  854.         if (headName.startsWith(Constants.R_REFS)) {
  855.             RefUpdate rup = repo.updateRef(headName);
  856.             rup.setNewObjectId(aNewHead);
  857.             rup.setRefLogMessage("rebase finished: " + headName + " onto " //$NON-NLS-1$ //$NON-NLS-2$
  858.                     + onto.getName(), false);
  859.             Result res = rup.forceUpdate();
  860.             switch (res) {
  861.             case FAST_FORWARD:
  862.             case FORCED:
  863.             case NO_CHANGE:
  864.                 break;
  865.             default:
  866.                 throw new JGitInternalException(
  867.                         JGitText.get().updatingHeadFailed);
  868.             }
  869.             rup = repo.updateRef(Constants.HEAD);
  870.             rup.setRefLogMessage("rebase finished: returning to " + headName, //$NON-NLS-1$
  871.                     false);
  872.             res = rup.link(headName);
  873.             switch (res) {
  874.             case FAST_FORWARD:
  875.             case FORCED:
  876.             case NO_CHANGE:
  877.                 break;
  878.             default:
  879.                 throw new JGitInternalException(
  880.                         JGitText.get().updatingHeadFailed);
  881.             }
  882.         }
  883.     }

  884.     private RevCommit checkoutCurrentHead() throws IOException, NoHeadException {
  885.         ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}"); //$NON-NLS-1$
  886.         if (headTree == null)
  887.             throw new NoHeadException(
  888.                     JGitText.get().cannotRebaseWithoutCurrentHead);
  889.         DirCache dc = repo.lockDirCache();
  890.         try {
  891.             DirCacheCheckout dco = new DirCacheCheckout(repo, dc, headTree);
  892.             dco.setFailOnConflict(false);
  893.             dco.setProgressMonitor(monitor);
  894.             boolean needsDeleteFiles = dco.checkout();
  895.             if (needsDeleteFiles) {
  896.                 List<String> fileList = dco.getToBeDeleted();
  897.                 for (String filePath : fileList) {
  898.                     File fileToDelete = new File(repo.getWorkTree(), filePath);
  899.                     if (repo.getFS().exists(fileToDelete))
  900.                         FileUtils.delete(fileToDelete, FileUtils.RECURSIVE
  901.                                 | FileUtils.RETRY);
  902.                 }
  903.             }
  904.         } finally {
  905.             dc.unlock();
  906.         }
  907.         try (RevWalk rw = new RevWalk(repo)) {
  908.             RevCommit commit = rw.parseCommit(repo.resolve(Constants.HEAD));
  909.             return commit;
  910.         }
  911.     }

  912.     /**
  913.      * @return the commit if we had to do a commit, otherwise null
  914.      * @throws GitAPIException
  915.      * @throws IOException
  916.      */
  917.     private RevCommit continueRebase() throws GitAPIException, IOException {
  918.         // if there are still conflicts, we throw a specific Exception
  919.         DirCache dc = repo.readDirCache();
  920.         boolean hasUnmergedPaths = dc.hasUnmergedPaths();
  921.         if (hasUnmergedPaths)
  922.             throw new UnmergedPathsException();

  923.         // determine whether we need to commit
  924.         boolean needsCommit;
  925.         try (TreeWalk treeWalk = new TreeWalk(repo)) {
  926.             treeWalk.reset();
  927.             treeWalk.setRecursive(true);
  928.             treeWalk.addTree(new DirCacheIterator(dc));
  929.             ObjectId id = repo.resolve(Constants.HEAD + "^{tree}"); //$NON-NLS-1$
  930.             if (id == null)
  931.                 throw new NoHeadException(
  932.                         JGitText.get().cannotRebaseWithoutCurrentHead);

  933.             treeWalk.addTree(id);

  934.             treeWalk.setFilter(TreeFilter.ANY_DIFF);

  935.             needsCommit = treeWalk.next();
  936.         }
  937.         if (needsCommit) {
  938.             try (Git git = new Git(repo)) {
  939.                 CommitCommand commit = git.commit();
  940.                 commit.setMessage(rebaseState.readFile(MESSAGE));
  941.                 commit.setAuthor(parseAuthor());
  942.                 return commit.call();
  943.             }
  944.         }
  945.         return null;
  946.     }

  947.     private PersonIdent parseAuthor() throws IOException {
  948.         File authorScriptFile = rebaseState.getFile(AUTHOR_SCRIPT);
  949.         byte[] raw;
  950.         try {
  951.             raw = IO.readFully(authorScriptFile);
  952.         } catch (FileNotFoundException notFound) {
  953.             if (authorScriptFile.exists()) {
  954.                 throw notFound;
  955.             }
  956.             return null;
  957.         }
  958.         return parseAuthor(raw);
  959.     }

  960.     private RebaseResult stop(RevCommit commitToPick, RebaseResult.Status status)
  961.             throws IOException {
  962.         PersonIdent author = commitToPick.getAuthorIdent();
  963.         String authorScript = toAuthorScript(author);
  964.         rebaseState.createFile(AUTHOR_SCRIPT, authorScript);
  965.         rebaseState.createFile(MESSAGE, commitToPick.getFullMessage());
  966.         ByteArrayOutputStream bos = new ByteArrayOutputStream();
  967.         try (DiffFormatter df = new DiffFormatter(bos)) {
  968.             df.setRepository(repo);
  969.             df.format(commitToPick.getParent(0), commitToPick);
  970.         }
  971.         rebaseState.createFile(PATCH, new String(bos.toByteArray(), UTF_8));
  972.         rebaseState.createFile(STOPPED_SHA,
  973.                 repo.newObjectReader()
  974.                 .abbreviate(
  975.                 commitToPick).name());
  976.         // Remove cherry pick state file created by CherryPickCommand, it's not
  977.         // needed for rebase
  978.         repo.writeCherryPickHead(null);
  979.         return RebaseResult.result(status, commitToPick);
  980.     }

  981.     String toAuthorScript(PersonIdent author) {
  982.         StringBuilder sb = new StringBuilder(100);
  983.         sb.append(GIT_AUTHOR_NAME);
  984.         sb.append("='"); //$NON-NLS-1$
  985.         sb.append(author.getName());
  986.         sb.append("'\n"); //$NON-NLS-1$
  987.         sb.append(GIT_AUTHOR_EMAIL);
  988.         sb.append("='"); //$NON-NLS-1$
  989.         sb.append(author.getEmailAddress());
  990.         sb.append("'\n"); //$NON-NLS-1$
  991.         // the command line uses the "external String"
  992.         // representation for date and timezone
  993.         sb.append(GIT_AUTHOR_DATE);
  994.         sb.append("='"); //$NON-NLS-1$
  995.         sb.append("@"); // @ for time in seconds since 1970 //$NON-NLS-1$
  996.         String externalString = author.toExternalString();
  997.         sb
  998.                 .append(externalString.substring(externalString
  999.                         .lastIndexOf('>') + 2));
  1000.         sb.append("'\n"); //$NON-NLS-1$
  1001.         return sb.toString();
  1002.     }

  1003.     /**
  1004.      * Removes the number of lines given in the parameter from the
  1005.      * <code>git-rebase-todo</code> file but preserves comments and other lines
  1006.      * that can not be parsed as steps
  1007.      *
  1008.      * @param numSteps
  1009.      * @throws IOException
  1010.      */
  1011.     private void popSteps(int numSteps) throws IOException {
  1012.         if (numSteps == 0)
  1013.             return;
  1014.         List<RebaseTodoLine> todoLines = new LinkedList<>();
  1015.         List<RebaseTodoLine> poppedLines = new LinkedList<>();

  1016.         for (RebaseTodoLine line : repo.readRebaseTodo(
  1017.                 rebaseState.getPath(GIT_REBASE_TODO), true)) {
  1018.             if (poppedLines.size() >= numSteps
  1019.                     || RebaseTodoLine.Action.COMMENT.equals(line.getAction()))
  1020.                 todoLines.add(line);
  1021.             else
  1022.                 poppedLines.add(line);
  1023.         }

  1024.         repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
  1025.                 todoLines, false);
  1026.         if (!poppedLines.isEmpty()) {
  1027.             repo.writeRebaseTodoFile(rebaseState.getPath(DONE), poppedLines,
  1028.                     true);
  1029.         }
  1030.     }

  1031.     private RebaseResult initFilesAndRewind() throws IOException,
  1032.             GitAPIException {
  1033.         // we need to store everything into files so that we can implement
  1034.         // --skip, --continue, and --abort

  1035.         Ref head = getHead();

  1036.         ObjectId headId = head.getObjectId();
  1037.         if (headId == null) {
  1038.             throw new RefNotFoundException(MessageFormat.format(
  1039.                     JGitText.get().refNotResolved, Constants.HEAD));
  1040.         }
  1041.         String headName = getHeadName(head);
  1042.         RevCommit headCommit = walk.lookupCommit(headId);
  1043.         RevCommit upstream = walk.lookupCommit(upstreamCommit.getId());

  1044.         if (!isInteractive() && walk.isMergedInto(upstream, headCommit))
  1045.             return RebaseResult.UP_TO_DATE_RESULT;
  1046.         else if (!isInteractive() && walk.isMergedInto(headCommit, upstream)) {
  1047.             // head is already merged into upstream, fast-foward
  1048.             monitor.beginTask(MessageFormat.format(
  1049.                     JGitText.get().resettingHead,
  1050.                     upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN);
  1051.             checkoutCommit(headName, upstreamCommit);
  1052.             monitor.endTask();

  1053.             updateHead(headName, upstreamCommit, upstream);
  1054.             return RebaseResult.FAST_FORWARD_RESULT;
  1055.         }

  1056.         monitor.beginTask(JGitText.get().obtainingCommitsForCherryPick,
  1057.                 ProgressMonitor.UNKNOWN);

  1058.         // create the folder for the meta information
  1059.         FileUtils.mkdir(rebaseState.getDir(), true);

  1060.         repo.writeOrigHead(headId);
  1061.         rebaseState.createFile(REBASE_HEAD, headId.name());
  1062.         rebaseState.createFile(REBASE_HEAD_LEGACY, headId.name());
  1063.         rebaseState.createFile(HEAD_NAME, headName);
  1064.         rebaseState.createFile(ONTO, upstreamCommit.name());
  1065.         rebaseState.createFile(ONTO_NAME, upstreamCommitName);
  1066.         if (isInteractive() || preserveMerges) {
  1067.             // --preserve-merges is an interactive mode for native git. Without
  1068.             // this, native git rebase --continue after a conflict would fall
  1069.             // into merge mode.
  1070.             rebaseState.createFile(INTERACTIVE, ""); //$NON-NLS-1$
  1071.         }
  1072.         rebaseState.createFile(QUIET, ""); //$NON-NLS-1$

  1073.         ArrayList<RebaseTodoLine> toDoSteps = new ArrayList<>();
  1074.         toDoSteps.add(new RebaseTodoLine("# Created by EGit: rebasing " + headId.name() //$NON-NLS-1$
  1075.                         + " onto " + upstreamCommit.name())); //$NON-NLS-1$
  1076.         // determine the commits to be applied
  1077.         List<RevCommit> cherryPickList = calculatePickList(headCommit);
  1078.         ObjectReader reader = walk.getObjectReader();
  1079.         for (RevCommit commit : cherryPickList)
  1080.             toDoSteps.add(new RebaseTodoLine(Action.PICK, reader
  1081.                     .abbreviate(commit), commit.getShortMessage()));
  1082.         repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
  1083.                 toDoSteps, false);

  1084.         monitor.endTask();

  1085.         // we rewind to the upstream commit
  1086.         monitor.beginTask(MessageFormat.format(JGitText.get().rewinding,
  1087.                 upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN);
  1088.         boolean checkoutOk = false;
  1089.         try {
  1090.             checkoutOk = checkoutCommit(headName, upstreamCommit);
  1091.         } finally {
  1092.             if (!checkoutOk)
  1093.                 FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE);
  1094.         }
  1095.         monitor.endTask();

  1096.         return null;
  1097.     }

  1098.     private List<RevCommit> calculatePickList(RevCommit headCommit)
  1099.             throws IOException {
  1100.         List<RevCommit> cherryPickList = new ArrayList<>();
  1101.         try (RevWalk r = new RevWalk(repo)) {
  1102.             r.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true);
  1103.             r.sort(RevSort.COMMIT_TIME_DESC, true);
  1104.             r.markUninteresting(r.lookupCommit(upstreamCommit));
  1105.             r.markStart(r.lookupCommit(headCommit));
  1106.             Iterator<RevCommit> commitsToUse = r.iterator();
  1107.             while (commitsToUse.hasNext()) {
  1108.                 RevCommit commit = commitsToUse.next();
  1109.                 if (preserveMerges || commit.getParentCount() == 1) {
  1110.                     cherryPickList.add(commit);
  1111.                 }
  1112.             }
  1113.         }
  1114.         Collections.reverse(cherryPickList);

  1115.         if (preserveMerges) {
  1116.             // When preserving merges we only rewrite commits which have at
  1117.             // least one parent that is itself rewritten (or a merge base)
  1118.             File rewrittenDir = rebaseState.getRewrittenDir();
  1119.             FileUtils.mkdir(rewrittenDir, false);
  1120.             walk.reset();
  1121.             walk.setRevFilter(RevFilter.MERGE_BASE);
  1122.             walk.markStart(upstreamCommit);
  1123.             walk.markStart(headCommit);
  1124.             RevCommit base;
  1125.             while ((base = walk.next()) != null)
  1126.                 RebaseState.createFile(rewrittenDir, base.getName(),
  1127.                         upstreamCommit.getName());

  1128.             Iterator<RevCommit> iterator = cherryPickList.iterator();
  1129.             pickLoop: while(iterator.hasNext()){
  1130.                 RevCommit commit = iterator.next();
  1131.                 for (int i = 0; i < commit.getParentCount(); i++) {
  1132.                     boolean parentRewritten = new File(rewrittenDir, commit
  1133.                             .getParent(i).getName()).exists();
  1134.                     if (parentRewritten) {
  1135.                         new File(rewrittenDir, commit.getName()).createNewFile();
  1136.                         continue pickLoop;
  1137.                     }
  1138.                 }
  1139.                 // commit is only merged in, needs not be rewritten
  1140.                 iterator.remove();
  1141.             }
  1142.         }
  1143.         return cherryPickList;
  1144.     }

  1145.     private static String getHeadName(Ref head) {
  1146.         String headName;
  1147.         if (head.isSymbolic()) {
  1148.             headName = head.getTarget().getName();
  1149.         } else {
  1150.             ObjectId headId = head.getObjectId();
  1151.             // the callers are checking this already
  1152.             assert headId != null;
  1153.             headName = headId.getName();
  1154.         }
  1155.         return headName;
  1156.     }

  1157.     private Ref getHead() throws IOException, RefNotFoundException {
  1158.         Ref head = repo.exactRef(Constants.HEAD);
  1159.         if (head == null || head.getObjectId() == null)
  1160.             throw new RefNotFoundException(MessageFormat.format(
  1161.                     JGitText.get().refNotResolved, Constants.HEAD));
  1162.         return head;
  1163.     }

  1164.     private boolean isInteractive() {
  1165.         return interactiveHandler != null;
  1166.     }

  1167.     /**
  1168.      * Check if we can fast-forward and returns the new head if it is possible
  1169.      *
  1170.      * @param newCommit
  1171.      *            a {@link org.eclipse.jgit.revwalk.RevCommit} object to check
  1172.      *            if we can fast-forward to.
  1173.      * @return the new head, or null
  1174.      * @throws java.io.IOException
  1175.      * @throws org.eclipse.jgit.api.errors.GitAPIException
  1176.      */
  1177.     public RevCommit tryFastForward(RevCommit newCommit) throws IOException,
  1178.             GitAPIException {
  1179.         Ref head = getHead();

  1180.         ObjectId headId = head.getObjectId();
  1181.         if (headId == null)
  1182.             throw new RefNotFoundException(MessageFormat.format(
  1183.                     JGitText.get().refNotResolved, Constants.HEAD));
  1184.         RevCommit headCommit = walk.lookupCommit(headId);
  1185.         if (walk.isMergedInto(newCommit, headCommit))
  1186.             return newCommit;

  1187.         String headName = getHeadName(head);
  1188.         return tryFastForward(headName, headCommit, newCommit);
  1189.     }

  1190.     private RevCommit tryFastForward(String headName, RevCommit oldCommit,
  1191.             RevCommit newCommit) throws IOException, GitAPIException {
  1192.         boolean tryRebase = false;
  1193.         for (RevCommit parentCommit : newCommit.getParents())
  1194.             if (parentCommit.equals(oldCommit))
  1195.                 tryRebase = true;
  1196.         if (!tryRebase)
  1197.             return null;

  1198.         CheckoutCommand co = new CheckoutCommand(repo);
  1199.         try {
  1200.             co.setProgressMonitor(monitor);
  1201.             co.setName(newCommit.name()).call();
  1202.             if (headName.startsWith(Constants.R_HEADS)) {
  1203.                 RefUpdate rup = repo.updateRef(headName);
  1204.                 rup.setExpectedOldObjectId(oldCommit);
  1205.                 rup.setNewObjectId(newCommit);
  1206.                 rup.setRefLogMessage("Fast-forward from " + oldCommit.name() //$NON-NLS-1$
  1207.                         + " to " + newCommit.name(), false); //$NON-NLS-1$
  1208.                 Result res = rup.update(walk);
  1209.                 switch (res) {
  1210.                 case FAST_FORWARD:
  1211.                 case NO_CHANGE:
  1212.                 case FORCED:
  1213.                     break;
  1214.                 default:
  1215.                     throw new IOException("Could not fast-forward"); //$NON-NLS-1$
  1216.                 }
  1217.             }
  1218.             return newCommit;
  1219.         } catch (RefAlreadyExistsException | RefNotFoundException
  1220.                 | InvalidRefNameException | CheckoutConflictException e) {
  1221.             throw new JGitInternalException(e.getMessage(), e);
  1222.         }
  1223.     }

  1224.     private void checkParameters() throws WrongRepositoryStateException {
  1225.         if (this.operation == Operation.PROCESS_STEPS) {
  1226.             if (rebaseState.getFile(DONE).exists())
  1227.                 throw new WrongRepositoryStateException(MessageFormat.format(
  1228.                         JGitText.get().wrongRepositoryState, repo
  1229.                                 .getRepositoryState().name()));
  1230.         }
  1231.         if (this.operation != Operation.BEGIN) {
  1232.             // these operations are only possible while in a rebasing state
  1233.             switch (repo.getRepositoryState()) {
  1234.             case REBASING_INTERACTIVE:
  1235.             case REBASING:
  1236.             case REBASING_REBASING:
  1237.             case REBASING_MERGE:
  1238.                 break;
  1239.             default:
  1240.                 throw new WrongRepositoryStateException(MessageFormat.format(
  1241.                         JGitText.get().wrongRepositoryState, repo
  1242.                                 .getRepositoryState().name()));
  1243.             }
  1244.         } else
  1245.             switch (repo.getRepositoryState()) {
  1246.             case SAFE:
  1247.                 if (this.upstreamCommit == null)
  1248.                     throw new JGitInternalException(MessageFormat
  1249.                             .format(JGitText.get().missingRequiredParameter,
  1250.                                     "upstream")); //$NON-NLS-1$
  1251.                 return;
  1252.             default:
  1253.                 throw new WrongRepositoryStateException(MessageFormat.format(
  1254.                         JGitText.get().wrongRepositoryState, repo
  1255.                                 .getRepositoryState().name()));

  1256.             }
  1257.     }

  1258.     private RebaseResult abort(RebaseResult result) throws IOException,
  1259.             GitAPIException {
  1260.         ObjectId origHead = getOriginalHead();
  1261.         try {
  1262.             String commitId = origHead != null ? origHead.name() : null;
  1263.             monitor.beginTask(MessageFormat.format(
  1264.                     JGitText.get().abortingRebase, commitId),
  1265.                     ProgressMonitor.UNKNOWN);

  1266.             DirCacheCheckout dco;
  1267.             if (commitId == null)
  1268.                 throw new JGitInternalException(
  1269.                         JGitText.get().abortingRebaseFailedNoOrigHead);
  1270.             ObjectId id = repo.resolve(commitId);
  1271.             RevCommit commit = walk.parseCommit(id);
  1272.             if (result.getStatus().equals(Status.FAILED)) {
  1273.                 RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD));
  1274.                 dco = new DirCacheCheckout(repo, head.getTree(),
  1275.                         repo.lockDirCache(), commit.getTree());
  1276.             } else {
  1277.                 dco = new DirCacheCheckout(repo, repo.lockDirCache(),
  1278.                         commit.getTree());
  1279.             }
  1280.             dco.setFailOnConflict(false);
  1281.             dco.checkout();
  1282.             walk.close();
  1283.         } finally {
  1284.             monitor.endTask();
  1285.         }
  1286.         try {
  1287.             String headName = rebaseState.readFile(HEAD_NAME);
  1288.                 monitor.beginTask(MessageFormat.format(
  1289.                         JGitText.get().resettingHead, headName),
  1290.                         ProgressMonitor.UNKNOWN);

  1291.             Result res = null;
  1292.             RefUpdate refUpdate = repo.updateRef(Constants.HEAD, false);
  1293.             refUpdate.setRefLogMessage("rebase: aborting", false); //$NON-NLS-1$
  1294.             if (headName.startsWith(Constants.R_REFS)) {
  1295.                 // update the HEAD
  1296.                 res = refUpdate.link(headName);
  1297.             } else {
  1298.                 refUpdate.setNewObjectId(origHead);
  1299.                 res = refUpdate.forceUpdate();

  1300.             }
  1301.             switch (res) {
  1302.             case FAST_FORWARD:
  1303.             case FORCED:
  1304.             case NO_CHANGE:
  1305.                 break;
  1306.             default:
  1307.                 throw new JGitInternalException(
  1308.                         JGitText.get().abortingRebaseFailed);
  1309.             }
  1310.             boolean stashConflicts = autoStashApply();
  1311.             // cleanup the files
  1312.             FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE);
  1313.             repo.writeCherryPickHead(null);
  1314.             repo.writeMergeHeads(null);
  1315.             if (stashConflicts)
  1316.                 return RebaseResult.STASH_APPLY_CONFLICTS_RESULT;
  1317.             return result;

  1318.         } finally {
  1319.             monitor.endTask();
  1320.         }
  1321.     }

  1322.     private ObjectId getOriginalHead() throws IOException {
  1323.         try {
  1324.             return ObjectId.fromString(rebaseState.readFile(REBASE_HEAD));
  1325.         } catch (FileNotFoundException e) {
  1326.             try {
  1327.                 return ObjectId
  1328.                         .fromString(rebaseState.readFile(REBASE_HEAD_LEGACY));
  1329.             } catch (FileNotFoundException ex) {
  1330.                 return repo.readOrigHead();
  1331.             }
  1332.         }
  1333.     }

  1334.     private boolean checkoutCommit(String headName, RevCommit commit)
  1335.             throws IOException,
  1336.             CheckoutConflictException {
  1337.         try {
  1338.             RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD));
  1339.             DirCacheCheckout dco = new DirCacheCheckout(repo, head.getTree(),
  1340.                     repo.lockDirCache(), commit.getTree());
  1341.             dco.setFailOnConflict(true);
  1342.             dco.setProgressMonitor(monitor);
  1343.             try {
  1344.                 dco.checkout();
  1345.             } catch (org.eclipse.jgit.errors.CheckoutConflictException cce) {
  1346.                 throw new CheckoutConflictException(dco.getConflicts(), cce);
  1347.             }
  1348.             // update the HEAD
  1349.             RefUpdate refUpdate = repo.updateRef(Constants.HEAD, true);
  1350.             refUpdate.setExpectedOldObjectId(head);
  1351.             refUpdate.setNewObjectId(commit);
  1352.             refUpdate.setRefLogMessage(
  1353.                     "checkout: moving from " //$NON-NLS-1$
  1354.                             + Repository.shortenRefName(headName)
  1355.                             + " to " + commit.getName(), false); //$NON-NLS-1$
  1356.             Result res = refUpdate.forceUpdate();
  1357.             switch (res) {
  1358.             case FAST_FORWARD:
  1359.             case NO_CHANGE:
  1360.             case FORCED:
  1361.                 break;
  1362.             default:
  1363.                 throw new IOException(
  1364.                         JGitText.get().couldNotRewindToUpstreamCommit);
  1365.             }
  1366.         } finally {
  1367.             walk.close();
  1368.             monitor.endTask();
  1369.         }
  1370.         return true;
  1371.     }


  1372.     /**
  1373.      * Set upstream {@code RevCommit}
  1374.      *
  1375.      * @param upstream
  1376.      *            the upstream commit
  1377.      * @return {@code this}
  1378.      */
  1379.     public RebaseCommand setUpstream(RevCommit upstream) {
  1380.         this.upstreamCommit = upstream;
  1381.         this.upstreamCommitName = upstream.name();
  1382.         return this;
  1383.     }

  1384.     /**
  1385.      * Set the upstream commit
  1386.      *
  1387.      * @param upstream
  1388.      *            id of the upstream commit
  1389.      * @return {@code this}
  1390.      */
  1391.     public RebaseCommand setUpstream(AnyObjectId upstream) {
  1392.         try {
  1393.             this.upstreamCommit = walk.parseCommit(upstream);
  1394.             this.upstreamCommitName = upstream.name();
  1395.         } catch (IOException e) {
  1396.             throw new JGitInternalException(MessageFormat.format(
  1397.                     JGitText.get().couldNotReadObjectWhileParsingCommit,
  1398.                     upstream.name()), e);
  1399.         }
  1400.         return this;
  1401.     }

  1402.     /**
  1403.      * Set the upstream branch
  1404.      *
  1405.      * @param upstream
  1406.      *            the name of the upstream branch
  1407.      * @return {@code this}
  1408.      * @throws org.eclipse.jgit.api.errors.RefNotFoundException
  1409.      */
  1410.     public RebaseCommand setUpstream(String upstream)
  1411.             throws RefNotFoundException {
  1412.         try {
  1413.             ObjectId upstreamId = repo.resolve(upstream);
  1414.             if (upstreamId == null)
  1415.                 throw new RefNotFoundException(MessageFormat.format(JGitText
  1416.                         .get().refNotResolved, upstream));
  1417.             upstreamCommit = walk.parseCommit(repo.resolve(upstream));
  1418.             upstreamCommitName = upstream;
  1419.             return this;
  1420.         } catch (IOException ioe) {
  1421.             throw new JGitInternalException(ioe.getMessage(), ioe);
  1422.         }
  1423.     }

  1424.     /**
  1425.      * Optionally override the name of the upstream. If this is used, it has to
  1426.      * come after any {@link #setUpstream} call.
  1427.      *
  1428.      * @param upstreamName
  1429.      *            the name which will be used to refer to upstream in conflicts
  1430.      * @return {@code this}
  1431.      */
  1432.     public RebaseCommand setUpstreamName(String upstreamName) {
  1433.         if (upstreamCommit == null) {
  1434.             throw new IllegalStateException(
  1435.                     "setUpstreamName must be called after setUpstream."); //$NON-NLS-1$
  1436.         }
  1437.         this.upstreamCommitName = upstreamName;
  1438.         return this;
  1439.     }

  1440.     /**
  1441.      * Set the operation to execute during rebase
  1442.      *
  1443.      * @param operation
  1444.      *            the operation to perform
  1445.      * @return {@code this}
  1446.      */
  1447.     public RebaseCommand setOperation(Operation operation) {
  1448.         this.operation = operation;
  1449.         return this;
  1450.     }

  1451.     /**
  1452.      * Set progress monitor
  1453.      *
  1454.      * @param monitor
  1455.      *            a progress monitor
  1456.      * @return this instance
  1457.      */
  1458.     public RebaseCommand setProgressMonitor(ProgressMonitor monitor) {
  1459.         if (monitor == null) {
  1460.             monitor = NullProgressMonitor.INSTANCE;
  1461.         }
  1462.         this.monitor = monitor;
  1463.         return this;
  1464.     }

  1465.     /**
  1466.      * Enable interactive rebase
  1467.      * <p>
  1468.      * Does not stop after initialization of interactive rebase. This is
  1469.      * equivalent to
  1470.      * {@link org.eclipse.jgit.api.RebaseCommand#runInteractively(InteractiveHandler, boolean)
  1471.      * runInteractively(handler, false)};
  1472.      * </p>
  1473.      *
  1474.      * @param handler
  1475.      *            the
  1476.      *            {@link org.eclipse.jgit.api.RebaseCommand.InteractiveHandler}
  1477.      *            to use
  1478.      * @return this
  1479.      */
  1480.     public RebaseCommand runInteractively(InteractiveHandler handler) {
  1481.         return runInteractively(handler, false);
  1482.     }

  1483.     /**
  1484.      * Enable interactive rebase
  1485.      * <p>
  1486.      * If stopAfterRebaseInteractiveInitialization is {@code true} the rebase
  1487.      * stops after initialization of interactive rebase returning
  1488.      * {@link org.eclipse.jgit.api.RebaseResult#INTERACTIVE_PREPARED_RESULT}
  1489.      * </p>
  1490.      *
  1491.      * @param handler
  1492.      *            the
  1493.      *            {@link org.eclipse.jgit.api.RebaseCommand.InteractiveHandler}
  1494.      *            to use
  1495.      * @param stopAfterRebaseInteractiveInitialization
  1496.      *            if {@code true} the rebase stops after initialization
  1497.      * @return this instance
  1498.      * @since 3.2
  1499.      */
  1500.     public RebaseCommand runInteractively(InteractiveHandler handler,
  1501.             final boolean stopAfterRebaseInteractiveInitialization) {
  1502.         this.stopAfterInitialization = stopAfterRebaseInteractiveInitialization;
  1503.         this.interactiveHandler = handler;
  1504.         return this;
  1505.     }

  1506.     /**
  1507.      * Set the <code>MergeStrategy</code>.
  1508.      *
  1509.      * @param strategy
  1510.      *            The merge strategy to use during this rebase operation.
  1511.      * @return {@code this}
  1512.      * @since 3.4
  1513.      */
  1514.     public RebaseCommand setStrategy(MergeStrategy strategy) {
  1515.         this.strategy = strategy;
  1516.         return this;
  1517.     }

  1518.     /**
  1519.      * Sets the content merge strategy to use if the
  1520.      * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
  1521.      * "recursive".
  1522.      *
  1523.      * @param strategy
  1524.      *            the {@link ContentMergeStrategy} to be used
  1525.      * @return {@code this}
  1526.      * @since 5.12
  1527.      */
  1528.     public RebaseCommand setContentMergeStrategy(ContentMergeStrategy strategy) {
  1529.         this.contentStrategy = strategy;
  1530.         return this;
  1531.     }

  1532.     /**
  1533.      * Whether to preserve merges during rebase
  1534.      *
  1535.      * @param preserve
  1536.      *            {@code true} to re-create merges during rebase. Defaults to
  1537.      *            {@code false}, a flattening rebase.
  1538.      * @return {@code this}
  1539.      * @since 3.5
  1540.      */
  1541.     public RebaseCommand setPreserveMerges(boolean preserve) {
  1542.         this.preserveMerges = preserve;
  1543.         return this;
  1544.     }

  1545.     /**
  1546.      * Allows to configure the interactive rebase process steps and to modify
  1547.      * commit messages.
  1548.      */
  1549.     public interface InteractiveHandler {

  1550.         /**
  1551.          * Callback API to modify the initial list of interactive rebase steps.
  1552.          *
  1553.          * @param steps
  1554.          *            initial configuration of interactive rebase
  1555.          */
  1556.         void prepareSteps(List<RebaseTodoLine> steps);

  1557.         /**
  1558.          * Used for editing commit message on REWORD or SQUASH.
  1559.          *
  1560.          * @param message
  1561.          *            existing commit message
  1562.          * @return new commit message
  1563.          */
  1564.         String modifyCommitMessage(String message);
  1565.     }

  1566.     /**
  1567.      * Extends {@link InteractiveHandler} with an enhanced callback for editing
  1568.      * commit messages.
  1569.      *
  1570.      * @since 6.1
  1571.      */
  1572.     public interface InteractiveHandler2 extends InteractiveHandler {

  1573.         /**
  1574.          * Callback API for editing a commit message on REWORD or SQUASH.
  1575.          * <p>
  1576.          * The callback gets the comment character currently set, and the
  1577.          * clean-up mode. It can use this information when presenting the
  1578.          * message to the user, and it also has the possibility to clean the
  1579.          * message itself (in which case the returned {@link ModifyResult}
  1580.          * should have {@link CleanupMode#VERBATIM} set lest JGit cleans the
  1581.          * message again). It can also override the initial clean-up mode by
  1582.          * returning clean-up mode other than {@link CleanupMode#DEFAULT}. If it
  1583.          * does return {@code DEFAULT}, the passed-in {@code mode} will be
  1584.          * applied.
  1585.          * </p>
  1586.          *
  1587.          * @param message
  1588.          *            existing commit message
  1589.          * @param mode
  1590.          *            {@link CleanupMode} currently set
  1591.          * @param commentChar
  1592.          *            comment character used
  1593.          * @return a {@link ModifyResult}
  1594.          */
  1595.         @NonNull
  1596.         ModifyResult editCommitMessage(@NonNull String message,
  1597.                 @NonNull CleanupMode mode, char commentChar);

  1598.         @Override
  1599.         default String modifyCommitMessage(String message) {
  1600.             // Should actually not be called; but do something reasonable anyway
  1601.             ModifyResult result = editCommitMessage(
  1602.                     message == null ? "" : message, CleanupMode.STRIP, //$NON-NLS-1$
  1603.                     '#');
  1604.             return result.getMessage();
  1605.         }

  1606.         /**
  1607.          * Describes the result of editing a commit message: the new message,
  1608.          * and how it should be cleaned.
  1609.          */
  1610.         interface ModifyResult {

  1611.             /**
  1612.              * Retrieves the new commit message.
  1613.              *
  1614.              * @return the message
  1615.              */
  1616.             @NonNull
  1617.             String getMessage();

  1618.             /**
  1619.              * Tells how the message returned by {@link #getMessage()} should be
  1620.              * cleaned.
  1621.              *
  1622.              * @return the {@link CleanupMode}
  1623.              */
  1624.             @NonNull
  1625.             CleanupMode getCleanupMode();

  1626.             /**
  1627.              * Tells whether a Gerrit Change-Id should be computed and added to
  1628.              * the commit message, as with
  1629.              * {@link CommitCommand#setInsertChangeId(boolean)}.
  1630.              *
  1631.              * @return {@code true} if a Change-Id should be handled,
  1632.              *         {@code false} otherwise
  1633.              */
  1634.             boolean shouldAddChangeId();
  1635.         }
  1636.     }

  1637.     PersonIdent parseAuthor(byte[] raw) {
  1638.         if (raw.length == 0)
  1639.             return null;

  1640.         Map<String, String> keyValueMap = new HashMap<>();
  1641.         for (int p = 0; p < raw.length;) {
  1642.             int end = RawParseUtils.nextLF(raw, p);
  1643.             if (end == p)
  1644.                 break;
  1645.             int equalsIndex = RawParseUtils.next(raw, p, '=');
  1646.             if (equalsIndex == end)
  1647.                 break;
  1648.             String key = RawParseUtils.decode(raw, p, equalsIndex - 1);
  1649.             String value = RawParseUtils.decode(raw, equalsIndex + 1, end - 2);
  1650.             p = end;
  1651.             keyValueMap.put(key, value);
  1652.         }

  1653.         String name = keyValueMap.get(GIT_AUTHOR_NAME);
  1654.         String email = keyValueMap.get(GIT_AUTHOR_EMAIL);
  1655.         String time = keyValueMap.get(GIT_AUTHOR_DATE);

  1656.         // the time is saved as <seconds since 1970> <timezone offset>
  1657.         int timeStart = 0;
  1658.         if (time.startsWith("@")) //$NON-NLS-1$
  1659.             timeStart = 1;
  1660.         else
  1661.             timeStart = 0;
  1662.         long when = Long
  1663.                 .parseLong(time.substring(timeStart, time.indexOf(' '))) * 1000;
  1664.         String tzOffsetString = time.substring(time.indexOf(' ') + 1);
  1665.         int multiplier = -1;
  1666.         if (tzOffsetString.charAt(0) == '+')
  1667.             multiplier = 1;
  1668.         int hours = Integer.parseInt(tzOffsetString.substring(1, 3));
  1669.         int minutes = Integer.parseInt(tzOffsetString.substring(3, 5));
  1670.         // this is in format (+/-)HHMM (hours and minutes)
  1671.         // we need to convert into minutes
  1672.         int tz = (hours * 60 + minutes) * multiplier;
  1673.         if (name != null && email != null)
  1674.             return new PersonIdent(name, email, when, tz);
  1675.         return null;
  1676.     }

  1677.     private static class RebaseState {

  1678.         private final File repoDirectory;
  1679.         private File dir;

  1680.         public RebaseState(File repoDirectory) {
  1681.             this.repoDirectory = repoDirectory;
  1682.         }

  1683.         public File getDir() {
  1684.             if (dir == null) {
  1685.                 File rebaseApply = new File(repoDirectory, REBASE_APPLY);
  1686.                 if (rebaseApply.exists()) {
  1687.                     dir = rebaseApply;
  1688.                 } else {
  1689.                     File rebaseMerge = new File(repoDirectory, REBASE_MERGE);
  1690.                     dir = rebaseMerge;
  1691.                 }
  1692.             }
  1693.             return dir;
  1694.         }

  1695.         /**
  1696.          * @return Directory with rewritten commit hashes, usually exists if
  1697.          *         {@link RebaseCommand#preserveMerges} is true
  1698.          **/
  1699.         public File getRewrittenDir() {
  1700.             return new File(getDir(), REWRITTEN);
  1701.         }

  1702.         public String readFile(String name) throws IOException {
  1703.             try {
  1704.                 return readFile(getDir(), name);
  1705.             } catch (FileNotFoundException e) {
  1706.                 if (ONTO_NAME.equals(name)) {
  1707.                     // Older JGit mistakenly wrote a file "onto-name" instead of
  1708.                     // "onto_name". Try that wrong name just in case somebody
  1709.                     // upgraded while a rebase started by JGit was in progress.
  1710.                     File oldFile = getFile(ONTO_NAME.replace('_', '-'));
  1711.                     if (oldFile.exists()) {
  1712.                         return readFile(oldFile);
  1713.                     }
  1714.                 }
  1715.                 throw e;
  1716.             }
  1717.         }

  1718.         public void createFile(String name, String content) throws IOException {
  1719.             createFile(getDir(), name, content);
  1720.         }

  1721.         public File getFile(String name) {
  1722.             return new File(getDir(), name);
  1723.         }

  1724.         public String getPath(String name) {
  1725.             return (getDir().getName() + "/" + name); //$NON-NLS-1$
  1726.         }

  1727.         private static String readFile(File file) throws IOException {
  1728.             byte[] content = IO.readFully(file);
  1729.             // strip off the last LF
  1730.             int end = RawParseUtils.prevLF(content, content.length);
  1731.             return RawParseUtils.decode(content, 0, end + 1);
  1732.         }

  1733.         private static String readFile(File directory, String fileName)
  1734.                 throws IOException {
  1735.             return readFile(new File(directory, fileName));
  1736.         }

  1737.         private static void createFile(File parentDir, String name,
  1738.                 String content)
  1739.                 throws IOException {
  1740.             File file = new File(parentDir, name);
  1741.             try (FileOutputStream fos = new FileOutputStream(file)) {
  1742.                 fos.write(content.getBytes(UTF_8));
  1743.                 fos.write('\n');
  1744.             }
  1745.         }

  1746.         private static void appendToFile(File file, String content)
  1747.                 throws IOException {
  1748.             try (FileOutputStream fos = new FileOutputStream(file, true)) {
  1749.                 fos.write(content.getBytes(UTF_8));
  1750.                 fos.write('\n');
  1751.             }
  1752.         }
  1753.     }
  1754. }