CherryPickCommand.java

  1. /*
  2.  * Copyright (C) 2010, 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 java.io.IOException;
  12. import java.text.MessageFormat;
  13. import java.util.LinkedList;
  14. import java.util.List;

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

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

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

  55.     private String ourCommitName = null;

  56.     private MergeStrategy strategy = MergeStrategy.RECURSIVE;

  57.     private Integer mainlineParentNumber;

  58.     private boolean noCommit = false;

  59.     private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;

  60.     /**
  61.      * Constructor for CherryPickCommand
  62.      *
  63.      * @param repo
  64.      *            the {@link org.eclipse.jgit.lib.Repository}
  65.      */
  66.     protected CherryPickCommand(Repository repo) {
  67.         super(repo);
  68.     }

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

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

  85.             // get the head commit
  86.             Ref headRef = repo.exactRef(Constants.HEAD);
  87.             if (headRef == null) {
  88.                 throw new NoHeadException(
  89.                         JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
  90.             }

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

  92.             // loop through all refs to be cherry-picked
  93.             for (Ref src : commits) {
  94.                 // get the commit to be cherry-picked
  95.                 // handle annotated tags
  96.                 ObjectId srcObjectId = src.getPeeledObjectId();
  97.                 if (srcObjectId == null) {
  98.                     srcObjectId = src.getObjectId();
  99.                 }
  100.                 RevCommit srcCommit = revWalk.parseCommit(srcObjectId);

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

  103.                 String ourName = calculateOurName(headRef);
  104.                 String cherryPickName = srcCommit.getId().abbreviate(7).name()
  105.                         + " " + srcCommit.getShortMessage(); //$NON-NLS-1$

  106.                 ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo);
  107.                 merger.setWorkingTreeIterator(new FileTreeIterator(repo));
  108.                 merger.setBase(srcParent.getTree());
  109.                 merger.setCommitNames(new String[] { "BASE", ourName, //$NON-NLS-1$
  110.                         cherryPickName });
  111.                 if (merger.merge(newHead, srcCommit)) {
  112.                     if (!merger.getModifiedFiles().isEmpty()) {
  113.                         repo.fireEvent(new WorkingTreeModifiedEvent(
  114.                                 merger.getModifiedFiles(), null));
  115.                     }
  116.                     if (AnyObjectId.isEqual(newHead.getTree().getId(),
  117.                             merger.getResultTreeId())) {
  118.                         continue;
  119.                     }
  120.                     DirCacheCheckout dco = new DirCacheCheckout(repo,
  121.                             newHead.getTree(), repo.lockDirCache(),
  122.                             merger.getResultTreeId());
  123.                     dco.setFailOnConflict(true);
  124.                     dco.setProgressMonitor(monitor);
  125.                     dco.checkout();
  126.                     if (!noCommit) {
  127.                         try (Git git = new Git(getRepository())) {
  128.                             newHead = git.commit()
  129.                                     .setMessage(srcCommit.getFullMessage())
  130.                                     .setReflogComment(reflogPrefix + " " //$NON-NLS-1$
  131.                                             + srcCommit.getShortMessage())
  132.                                     .setAuthor(srcCommit.getAuthorIdent())
  133.                                     .setNoVerify(true).call();
  134.                         }
  135.                     }
  136.                     cherryPickedRefs.add(src);
  137.                 } else {
  138.                     if (merger.failed()) {
  139.                         return new CherryPickResult(merger.getFailingPaths());
  140.                     }

  141.                     // there are merge conflicts

  142.                     String message = new MergeMessageFormatter()
  143.                             .formatWithConflicts(srcCommit.getFullMessage(),
  144.                                     merger.getUnmergedPaths());

  145.                     if (!noCommit) {
  146.                         repo.writeCherryPickHead(srcCommit.getId());
  147.                     }
  148.                     repo.writeMergeCommitMsg(message);

  149.                     repo.fireEvent(new WorkingTreeModifiedEvent(
  150.                             merger.getModifiedFiles(), null));

  151.                     return CherryPickResult.CONFLICT;
  152.                 }
  153.             }
  154.         } catch (IOException e) {
  155.             throw new JGitInternalException(
  156.                     MessageFormat.format(
  157.                             JGitText.get().exceptionCaughtDuringExecutionOfCherryPickCommand,
  158.                             e), e);
  159.         }
  160.         return new CherryPickResult(newHead, cherryPickedRefs);
  161.     }

  162.     private RevCommit getParentCommit(RevCommit srcCommit, RevWalk revWalk)
  163.             throws MultipleParentsNotAllowedException, MissingObjectException,
  164.             IOException {
  165.         final RevCommit srcParent;
  166.         if (mainlineParentNumber == null) {
  167.             if (srcCommit.getParentCount() != 1)
  168.                 throw new MultipleParentsNotAllowedException(
  169.                         MessageFormat.format(
  170.                                 JGitText.get().canOnlyCherryPickCommitsWithOneParent,
  171.                                 srcCommit.name(),
  172.                                 Integer.valueOf(srcCommit.getParentCount())));
  173.             srcParent = srcCommit.getParent(0);
  174.         } else {
  175.             if (mainlineParentNumber.intValue() > srcCommit.getParentCount()) {
  176.                 throw new JGitInternalException(MessageFormat.format(
  177.                         JGitText.get().commitDoesNotHaveGivenParent, srcCommit,
  178.                         mainlineParentNumber));
  179.             }
  180.             srcParent = srcCommit
  181.                     .getParent(mainlineParentNumber.intValue() - 1);
  182.         }

  183.         revWalk.parseHeaders(srcParent);
  184.         return srcParent;
  185.     }

  186.     /**
  187.      * Include a reference to a commit
  188.      *
  189.      * @param commit
  190.      *            a reference to a commit which is cherry-picked to the current
  191.      *            head
  192.      * @return {@code this}
  193.      */
  194.     public CherryPickCommand include(Ref commit) {
  195.         checkCallable();
  196.         commits.add(commit);
  197.         return this;
  198.     }

  199.     /**
  200.      * Include a commit
  201.      *
  202.      * @param commit
  203.      *            the Id of a commit which is cherry-picked to the current head
  204.      * @return {@code this}
  205.      */
  206.     public CherryPickCommand include(AnyObjectId commit) {
  207.         return include(commit.getName(), commit);
  208.     }

  209.     /**
  210.      * Include a commit
  211.      *
  212.      * @param name
  213.      *            a name given to the commit
  214.      * @param commit
  215.      *            the Id of a commit which is cherry-picked to the current head
  216.      * @return {@code this}
  217.      */
  218.     public CherryPickCommand include(String name, AnyObjectId commit) {
  219.         return include(new ObjectIdRef.Unpeeled(Storage.LOOSE, name,
  220.                 commit.copy()));
  221.     }

  222.     /**
  223.      * Set the name that should be used in the "OURS" place for conflict markers
  224.      *
  225.      * @param ourCommitName
  226.      *            the name that should be used in the "OURS" place for conflict
  227.      *            markers
  228.      * @return {@code this}
  229.      */
  230.     public CherryPickCommand setOurCommitName(String ourCommitName) {
  231.         this.ourCommitName = ourCommitName;
  232.         return this;
  233.     }

  234.     /**
  235.      * Set the prefix to use in the reflog.
  236.      * <p>
  237.      * This is primarily needed for implementing rebase in terms of
  238.      * cherry-picking
  239.      *
  240.      * @param prefix
  241.      *            including ":"
  242.      * @return {@code this}
  243.      * @since 3.1
  244.      */
  245.     public CherryPickCommand setReflogPrefix(String prefix) {
  246.         this.reflogPrefix = prefix;
  247.         return this;
  248.     }

  249.     /**
  250.      * Set the {@code MergeStrategy}
  251.      *
  252.      * @param strategy
  253.      *            The merge strategy to use during this Cherry-pick.
  254.      * @return {@code this}
  255.      * @since 3.4
  256.      */
  257.     public CherryPickCommand setStrategy(MergeStrategy strategy) {
  258.         this.strategy = strategy;
  259.         return this;
  260.     }

  261.     /**
  262.      * Set the (1-based) parent number to diff against
  263.      *
  264.      * @param mainlineParentNumber
  265.      *            the (1-based) parent number to diff against. This allows
  266.      *            cherry-picking of merges.
  267.      * @return {@code this}
  268.      * @since 3.4
  269.      */
  270.     public CherryPickCommand setMainlineParentNumber(int mainlineParentNumber) {
  271.         this.mainlineParentNumber = Integer.valueOf(mainlineParentNumber);
  272.         return this;
  273.     }

  274.     /**
  275.      * Allows cherry-picking changes without committing them.
  276.      * <p>
  277.      * NOTE: The behavior of cherry-pick is undefined if you pick multiple
  278.      * commits or if HEAD does not match the index state before cherry-picking.
  279.      *
  280.      * @param noCommit
  281.      *            true to cherry-pick without committing, false to commit after
  282.      *            each pick (default)
  283.      * @return {@code this}
  284.      * @since 3.5
  285.      */
  286.     public CherryPickCommand setNoCommit(boolean noCommit) {
  287.         this.noCommit = noCommit;
  288.         return this;
  289.     }

  290.     /**
  291.      * The progress monitor associated with the cherry-pick operation. By
  292.      * default, this is set to <code>NullProgressMonitor</code>
  293.      *
  294.      * @see NullProgressMonitor
  295.      * @param monitor
  296.      *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
  297.      * @return {@code this}
  298.      * @since 4.11
  299.      */
  300.     public CherryPickCommand setProgressMonitor(ProgressMonitor monitor) {
  301.         if (monitor == null) {
  302.             monitor = NullProgressMonitor.INSTANCE;
  303.         }
  304.         this.monitor = monitor;
  305.         return this;
  306.     }

  307.     private String calculateOurName(Ref headRef) {
  308.         if (ourCommitName != null)
  309.             return ourCommitName;

  310.         String targetRefName = headRef.getTarget().getName();
  311.         String headName = Repository.shortenRefName(targetRefName);
  312.         return headName;
  313.     }

  314.     /** {@inheritDoc} */
  315.     @SuppressWarnings("nls")
  316.     @Override
  317.     public String toString() {
  318.         return "CherryPickCommand [repo=" + repo + ",\ncommits=" + commits
  319.                 + ",\nmainlineParentNumber=" + mainlineParentNumber
  320.                 + ", noCommit=" + noCommit + ", ourCommitName=" + ourCommitName
  321.                 + ", reflogPrefix=" + reflogPrefix + ", strategy=" + strategy
  322.                 + "]";
  323.     }

  324. }