CherryPickCommand.java

  1. /*
  2.  * Copyright (C) 2010, 2021 Christian Halstrick <christian.halstrick@sap.com> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.api;

  11. import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH;

  12. import java.io.IOException;
  13. import java.text.MessageFormat;
  14. import java.util.LinkedList;
  15. import java.util.List;
  16. import java.util.Map;

  17. import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
  18. import org.eclipse.jgit.api.errors.GitAPIException;
  19. import org.eclipse.jgit.api.errors.JGitInternalException;
  20. import org.eclipse.jgit.api.errors.MultipleParentsNotAllowedException;
  21. import org.eclipse.jgit.api.errors.NoHeadException;
  22. import org.eclipse.jgit.api.errors.NoMessageException;
  23. import org.eclipse.jgit.api.errors.UnmergedPathsException;
  24. import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
  25. import org.eclipse.jgit.dircache.DirCacheCheckout;
  26. import org.eclipse.jgit.errors.MissingObjectException;
  27. import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
  28. import org.eclipse.jgit.internal.JGitText;
  29. import org.eclipse.jgit.lib.AnyObjectId;
  30. import org.eclipse.jgit.lib.CommitConfig;
  31. import org.eclipse.jgit.lib.Constants;
  32. import org.eclipse.jgit.lib.NullProgressMonitor;
  33. import org.eclipse.jgit.lib.ObjectId;
  34. import org.eclipse.jgit.lib.ObjectIdRef;
  35. import org.eclipse.jgit.lib.ProgressMonitor;
  36. import org.eclipse.jgit.lib.Ref;
  37. import org.eclipse.jgit.lib.Ref.Storage;
  38. import org.eclipse.jgit.lib.Repository;
  39. import org.eclipse.jgit.merge.ContentMergeStrategy;
  40. import org.eclipse.jgit.merge.MergeMessageFormatter;
  41. import org.eclipse.jgit.merge.MergeStrategy;
  42. import org.eclipse.jgit.merge.Merger;
  43. import org.eclipse.jgit.merge.ResolveMerger;
  44. import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
  45. import org.eclipse.jgit.revwalk.RevCommit;
  46. import org.eclipse.jgit.revwalk.RevWalk;
  47. import org.eclipse.jgit.treewalk.FileTreeIterator;

  48. /**
  49.  * A class used to execute a {@code cherry-pick} command. It has setters for all
  50.  * supported options and arguments of this command and a {@link #call()} method
  51.  * to finally execute the command. Each instance of this class should only be
  52.  * used for one invocation of the command (means: one call to {@link #call()})
  53.  *
  54.  * @see <a
  55.  *      href="http://www.kernel.org/pub/software/scm/git/docs/git-cherry-pick.html"
  56.  *      >Git documentation about cherry-pick</a>
  57.  */
  58. public class CherryPickCommand extends GitCommand<CherryPickResult> {
  59.     private String reflogPrefix = "cherry-pick:"; //$NON-NLS-1$

  60.     private List<Ref> commits = new LinkedList<>();

  61.     private String ourCommitName = null;

  62.     private MergeStrategy strategy = MergeStrategy.RECURSIVE;

  63.     private ContentMergeStrategy contentStrategy;

  64.     private Integer mainlineParentNumber;

  65.     private boolean noCommit = false;

  66.     private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;

  67.     /**
  68.      * Constructor for CherryPickCommand
  69.      *
  70.      * @param repo
  71.      *            the {@link org.eclipse.jgit.lib.Repository}
  72.      */
  73.     protected CherryPickCommand(Repository repo) {
  74.         super(repo);
  75.     }

  76.     /**
  77.      * {@inheritDoc}
  78.      * <p>
  79.      * Executes the {@code Cherry-Pick} command with all the options and
  80.      * parameters collected by the setter methods (e.g. {@link #include(Ref)} of
  81.      * this class. Each instance of this class should only be used for one
  82.      * invocation of the command. Don't call this method twice on an instance.
  83.      */
  84.     @Override
  85.     public CherryPickResult call() throws GitAPIException, NoMessageException,
  86.             UnmergedPathsException, ConcurrentRefUpdateException,
  87.             WrongRepositoryStateException, NoHeadException {
  88.         RevCommit newHead = null;
  89.         List<Ref> cherryPickedRefs = new LinkedList<>();
  90.         checkCallable();

  91.         try (RevWalk revWalk = new RevWalk(repo)) {

  92.             // get the head commit
  93.             Ref headRef = repo.exactRef(Constants.HEAD);
  94.             if (headRef == null) {
  95.                 throw new NoHeadException(
  96.                         JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
  97.             }

  98.             newHead = revWalk.parseCommit(headRef.getObjectId());

  99.             // loop through all refs to be cherry-picked
  100.             for (Ref src : commits) {
  101.                 // get the commit to be cherry-picked
  102.                 // handle annotated tags
  103.                 ObjectId srcObjectId = src.getPeeledObjectId();
  104.                 if (srcObjectId == null) {
  105.                     srcObjectId = src.getObjectId();
  106.                 }
  107.                 RevCommit srcCommit = revWalk.parseCommit(srcObjectId);

  108.                 // get the parent of the commit to cherry-pick
  109.                 final RevCommit srcParent = getParentCommit(srcCommit, revWalk);

  110.                 String ourName = calculateOurName(headRef);
  111.                 String cherryPickName = srcCommit.getId().abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name()
  112.                         + " " + srcCommit.getShortMessage(); //$NON-NLS-1$

  113.                 Merger merger = strategy.newMerger(repo);
  114.                 merger.setProgressMonitor(monitor);
  115.                 boolean noProblems;
  116.                 Map<String, MergeFailureReason> failingPaths = null;
  117.                 List<String> unmergedPaths = null;
  118.                 if (merger instanceof ResolveMerger) {
  119.                     ResolveMerger resolveMerger = (ResolveMerger) merger;
  120.                     resolveMerger.setContentMergeStrategy(contentStrategy);
  121.                     resolveMerger.setCommitNames(
  122.                             new String[] { "BASE", ourName, cherryPickName }); //$NON-NLS-1$
  123.                     resolveMerger
  124.                             .setWorkingTreeIterator(new FileTreeIterator(repo));
  125.                     resolveMerger.setBase(srcParent.getTree());
  126.                     noProblems = merger.merge(newHead, srcCommit);
  127.                     failingPaths = resolveMerger.getFailingPaths();
  128.                     unmergedPaths = resolveMerger.getUnmergedPaths();
  129.                     if (!resolveMerger.getModifiedFiles().isEmpty()) {
  130.                         repo.fireEvent(new WorkingTreeModifiedEvent(
  131.                                 resolveMerger.getModifiedFiles(), null));
  132.                     }
  133.                 } else {
  134.                     noProblems = merger.merge(newHead, srcCommit);
  135.                 }
  136.                 if (noProblems) {
  137.                     if (AnyObjectId.isEqual(newHead.getTree().getId(),
  138.                             merger.getResultTreeId())) {
  139.                         continue;
  140.                     }
  141.                     DirCacheCheckout dco = new DirCacheCheckout(repo,
  142.                             newHead.getTree(), repo.lockDirCache(),
  143.                             merger.getResultTreeId());
  144.                     dco.setFailOnConflict(true);
  145.                     dco.setProgressMonitor(monitor);
  146.                     dco.checkout();
  147.                     if (!noCommit) {
  148.                         try (Git git = new Git(getRepository())) {
  149.                             newHead = git.commit()
  150.                                     .setMessage(srcCommit.getFullMessage())
  151.                                     .setReflogComment(reflogPrefix + " " //$NON-NLS-1$
  152.                                             + srcCommit.getShortMessage())
  153.                                     .setAuthor(srcCommit.getAuthorIdent())
  154.                                     .setNoVerify(true).call();
  155.                         }
  156.                     }
  157.                     cherryPickedRefs.add(src);
  158.                 } else {
  159.                     if (failingPaths != null && !failingPaths.isEmpty()) {
  160.                         return new CherryPickResult(failingPaths);
  161.                     }

  162.                     // there are merge conflicts

  163.                     String message;
  164.                     if (unmergedPaths != null) {
  165.                         CommitConfig cfg = repo.getConfig()
  166.                                 .get(CommitConfig.KEY);
  167.                         message = srcCommit.getFullMessage();
  168.                         char commentChar = cfg.getCommentChar(message);
  169.                         message = new MergeMessageFormatter()
  170.                                 .formatWithConflicts(message, unmergedPaths,
  171.                                         commentChar);
  172.                     } else {
  173.                         message = srcCommit.getFullMessage();
  174.                     }

  175.                     if (!noCommit) {
  176.                         repo.writeCherryPickHead(srcCommit.getId());
  177.                     }
  178.                     repo.writeMergeCommitMsg(message);

  179.                     return CherryPickResult.CONFLICT;
  180.                 }
  181.             }
  182.         } catch (IOException e) {
  183.             throw new JGitInternalException(
  184.                     MessageFormat.format(
  185.                             JGitText.get().exceptionCaughtDuringExecutionOfCherryPickCommand,
  186.                             e), e);
  187.         }
  188.         return new CherryPickResult(newHead, cherryPickedRefs);
  189.     }

  190.     private RevCommit getParentCommit(RevCommit srcCommit, RevWalk revWalk)
  191.             throws MultipleParentsNotAllowedException, MissingObjectException,
  192.             IOException {
  193.         final RevCommit srcParent;
  194.         if (mainlineParentNumber == null) {
  195.             if (srcCommit.getParentCount() != 1)
  196.                 throw new MultipleParentsNotAllowedException(
  197.                         MessageFormat.format(
  198.                                 JGitText.get().canOnlyCherryPickCommitsWithOneParent,
  199.                                 srcCommit.name(),
  200.                                 Integer.valueOf(srcCommit.getParentCount())));
  201.             srcParent = srcCommit.getParent(0);
  202.         } else {
  203.             if (mainlineParentNumber.intValue() > srcCommit.getParentCount()) {
  204.                 throw new JGitInternalException(MessageFormat.format(
  205.                         JGitText.get().commitDoesNotHaveGivenParent, srcCommit,
  206.                         mainlineParentNumber));
  207.             }
  208.             srcParent = srcCommit
  209.                     .getParent(mainlineParentNumber.intValue() - 1);
  210.         }

  211.         revWalk.parseHeaders(srcParent);
  212.         return srcParent;
  213.     }

  214.     /**
  215.      * Include a reference to a commit
  216.      *
  217.      * @param commit
  218.      *            a reference to a commit which is cherry-picked to the current
  219.      *            head
  220.      * @return {@code this}
  221.      */
  222.     public CherryPickCommand include(Ref commit) {
  223.         checkCallable();
  224.         commits.add(commit);
  225.         return this;
  226.     }

  227.     /**
  228.      * Include a commit
  229.      *
  230.      * @param commit
  231.      *            the Id of a commit which is cherry-picked to the current head
  232.      * @return {@code this}
  233.      */
  234.     public CherryPickCommand include(AnyObjectId commit) {
  235.         return include(commit.getName(), commit);
  236.     }

  237.     /**
  238.      * Include a commit
  239.      *
  240.      * @param name
  241.      *            a name given to the commit
  242.      * @param commit
  243.      *            the Id of a commit which is cherry-picked to the current head
  244.      * @return {@code this}
  245.      */
  246.     public CherryPickCommand include(String name, AnyObjectId commit) {
  247.         return include(new ObjectIdRef.Unpeeled(Storage.LOOSE, name,
  248.                 commit.copy()));
  249.     }

  250.     /**
  251.      * Set the name that should be used in the "OURS" place for conflict markers
  252.      *
  253.      * @param ourCommitName
  254.      *            the name that should be used in the "OURS" place for conflict
  255.      *            markers
  256.      * @return {@code this}
  257.      */
  258.     public CherryPickCommand setOurCommitName(String ourCommitName) {
  259.         this.ourCommitName = ourCommitName;
  260.         return this;
  261.     }

  262.     /**
  263.      * Set the prefix to use in the reflog.
  264.      * <p>
  265.      * This is primarily needed for implementing rebase in terms of
  266.      * cherry-picking
  267.      *
  268.      * @param prefix
  269.      *            including ":"
  270.      * @return {@code this}
  271.      * @since 3.1
  272.      */
  273.     public CherryPickCommand setReflogPrefix(String prefix) {
  274.         this.reflogPrefix = prefix;
  275.         return this;
  276.     }

  277.     /**
  278.      * Set the {@code MergeStrategy}
  279.      *
  280.      * @param strategy
  281.      *            The merge strategy to use during this Cherry-pick.
  282.      * @return {@code this}
  283.      * @since 3.4
  284.      */
  285.     public CherryPickCommand setStrategy(MergeStrategy strategy) {
  286.         this.strategy = strategy;
  287.         return this;
  288.     }

  289.     /**
  290.      * Sets the content merge strategy to use if the
  291.      * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
  292.      * "recursive".
  293.      *
  294.      * @param strategy
  295.      *            the {@link ContentMergeStrategy} to be used
  296.      * @return {@code this}
  297.      * @since 5.12
  298.      */
  299.     public CherryPickCommand setContentMergeStrategy(
  300.             ContentMergeStrategy strategy) {
  301.         this.contentStrategy = strategy;
  302.         return this;
  303.     }

  304.     /**
  305.      * Set the (1-based) parent number to diff against
  306.      *
  307.      * @param mainlineParentNumber
  308.      *            the (1-based) parent number to diff against. This allows
  309.      *            cherry-picking of merges.
  310.      * @return {@code this}
  311.      * @since 3.4
  312.      */
  313.     public CherryPickCommand setMainlineParentNumber(int mainlineParentNumber) {
  314.         this.mainlineParentNumber = Integer.valueOf(mainlineParentNumber);
  315.         return this;
  316.     }

  317.     /**
  318.      * Allows cherry-picking changes without committing them.
  319.      * <p>
  320.      * NOTE: The behavior of cherry-pick is undefined if you pick multiple
  321.      * commits or if HEAD does not match the index state before cherry-picking.
  322.      *
  323.      * @param noCommit
  324.      *            true to cherry-pick without committing, false to commit after
  325.      *            each pick (default)
  326.      * @return {@code this}
  327.      * @since 3.5
  328.      */
  329.     public CherryPickCommand setNoCommit(boolean noCommit) {
  330.         this.noCommit = noCommit;
  331.         return this;
  332.     }

  333.     /**
  334.      * The progress monitor associated with the cherry-pick operation. By
  335.      * default, this is set to <code>NullProgressMonitor</code>
  336.      *
  337.      * @see NullProgressMonitor
  338.      * @param monitor
  339.      *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
  340.      * @return {@code this}
  341.      * @since 4.11
  342.      */
  343.     public CherryPickCommand setProgressMonitor(ProgressMonitor monitor) {
  344.         if (monitor == null) {
  345.             monitor = NullProgressMonitor.INSTANCE;
  346.         }
  347.         this.monitor = monitor;
  348.         return this;
  349.     }

  350.     private String calculateOurName(Ref headRef) {
  351.         if (ourCommitName != null)
  352.             return ourCommitName;

  353.         String targetRefName = headRef.getTarget().getName();
  354.         String headName = Repository.shortenRefName(targetRefName);
  355.         return headName;
  356.     }

  357.     /** {@inheritDoc} */
  358.     @SuppressWarnings("nls")
  359.     @Override
  360.     public String toString() {
  361.         return "CherryPickCommand [repo=" + repo + ",\ncommits=" + commits
  362.                 + ",\nmainlineParentNumber=" + mainlineParentNumber
  363.                 + ", noCommit=" + noCommit + ", ourCommitName=" + ourCommitName
  364.                 + ", reflogPrefix=" + reflogPrefix + ", strategy=" + strategy
  365.                 + "]";
  366.     }

  367. }