You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CherryPickCommand.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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 java.io.IOException;
  12. import java.text.MessageFormat;
  13. import java.util.LinkedList;
  14. import java.util.List;
  15. import java.util.Map;
  16. import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
  17. import org.eclipse.jgit.api.errors.GitAPIException;
  18. import org.eclipse.jgit.api.errors.JGitInternalException;
  19. import org.eclipse.jgit.api.errors.MultipleParentsNotAllowedException;
  20. import org.eclipse.jgit.api.errors.NoHeadException;
  21. import org.eclipse.jgit.api.errors.NoMessageException;
  22. import org.eclipse.jgit.api.errors.UnmergedPathsException;
  23. import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
  24. import org.eclipse.jgit.dircache.DirCacheCheckout;
  25. import org.eclipse.jgit.errors.MissingObjectException;
  26. import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
  27. import org.eclipse.jgit.internal.JGitText;
  28. import org.eclipse.jgit.lib.AnyObjectId;
  29. import org.eclipse.jgit.lib.Constants;
  30. import org.eclipse.jgit.lib.NullProgressMonitor;
  31. import org.eclipse.jgit.lib.ObjectId;
  32. import org.eclipse.jgit.lib.ObjectIdRef;
  33. import org.eclipse.jgit.lib.ProgressMonitor;
  34. import org.eclipse.jgit.lib.Ref;
  35. import org.eclipse.jgit.lib.Ref.Storage;
  36. import org.eclipse.jgit.lib.Repository;
  37. import org.eclipse.jgit.merge.ContentMergeStrategy;
  38. import org.eclipse.jgit.merge.MergeMessageFormatter;
  39. import org.eclipse.jgit.merge.MergeStrategy;
  40. import org.eclipse.jgit.merge.Merger;
  41. import org.eclipse.jgit.merge.ResolveMerger;
  42. import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
  43. import org.eclipse.jgit.revwalk.RevCommit;
  44. import org.eclipse.jgit.revwalk.RevWalk;
  45. import org.eclipse.jgit.treewalk.FileTreeIterator;
  46. /**
  47. * A class used to execute a {@code cherry-pick} command. It has setters for all
  48. * supported options and arguments of this command and a {@link #call()} method
  49. * to finally execute the command. Each instance of this class should only be
  50. * used for one invocation of the command (means: one call to {@link #call()})
  51. *
  52. * @see <a
  53. * href="http://www.kernel.org/pub/software/scm/git/docs/git-cherry-pick.html"
  54. * >Git documentation about cherry-pick</a>
  55. */
  56. public class CherryPickCommand extends GitCommand<CherryPickResult> {
  57. private String reflogPrefix = "cherry-pick:"; //$NON-NLS-1$
  58. private List<Ref> commits = new LinkedList<>();
  59. private String ourCommitName = null;
  60. private MergeStrategy strategy = MergeStrategy.RECURSIVE;
  61. private ContentMergeStrategy contentStrategy;
  62. private Integer mainlineParentNumber;
  63. private boolean noCommit = false;
  64. private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
  65. /**
  66. * Constructor for CherryPickCommand
  67. *
  68. * @param repo
  69. * the {@link org.eclipse.jgit.lib.Repository}
  70. */
  71. protected CherryPickCommand(Repository repo) {
  72. super(repo);
  73. }
  74. /**
  75. * {@inheritDoc}
  76. * <p>
  77. * Executes the {@code Cherry-Pick} command with all the options and
  78. * parameters collected by the setter methods (e.g. {@link #include(Ref)} of
  79. * this class. Each instance of this class should only be used for one
  80. * invocation of the command. Don't call this method twice on an instance.
  81. */
  82. @Override
  83. public CherryPickResult call() throws GitAPIException, NoMessageException,
  84. UnmergedPathsException, ConcurrentRefUpdateException,
  85. WrongRepositoryStateException, NoHeadException {
  86. RevCommit newHead = null;
  87. List<Ref> cherryPickedRefs = new LinkedList<>();
  88. checkCallable();
  89. try (RevWalk revWalk = new RevWalk(repo)) {
  90. // get the head commit
  91. Ref headRef = repo.exactRef(Constants.HEAD);
  92. if (headRef == null) {
  93. throw new NoHeadException(
  94. JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
  95. }
  96. newHead = revWalk.parseCommit(headRef.getObjectId());
  97. // loop through all refs to be cherry-picked
  98. for (Ref src : commits) {
  99. // get the commit to be cherry-picked
  100. // handle annotated tags
  101. ObjectId srcObjectId = src.getPeeledObjectId();
  102. if (srcObjectId == null) {
  103. srcObjectId = src.getObjectId();
  104. }
  105. RevCommit srcCommit = revWalk.parseCommit(srcObjectId);
  106. // get the parent of the commit to cherry-pick
  107. final RevCommit srcParent = getParentCommit(srcCommit, revWalk);
  108. String ourName = calculateOurName(headRef);
  109. String cherryPickName = srcCommit.getId().abbreviate(7).name()
  110. + " " + srcCommit.getShortMessage(); //$NON-NLS-1$
  111. Merger merger = strategy.newMerger(repo);
  112. merger.setProgressMonitor(monitor);
  113. boolean noProblems;
  114. Map<String, MergeFailureReason> failingPaths = null;
  115. List<String> unmergedPaths = null;
  116. if (merger instanceof ResolveMerger) {
  117. ResolveMerger resolveMerger = (ResolveMerger) merger;
  118. resolveMerger.setContentMergeStrategy(contentStrategy);
  119. resolveMerger.setCommitNames(
  120. new String[] { "BASE", ourName, cherryPickName }); //$NON-NLS-1$
  121. resolveMerger
  122. .setWorkingTreeIterator(new FileTreeIterator(repo));
  123. resolveMerger.setBase(srcParent.getTree());
  124. noProblems = merger.merge(newHead, srcCommit);
  125. failingPaths = resolveMerger.getFailingPaths();
  126. unmergedPaths = resolveMerger.getUnmergedPaths();
  127. if (!resolveMerger.getModifiedFiles().isEmpty()) {
  128. repo.fireEvent(new WorkingTreeModifiedEvent(
  129. resolveMerger.getModifiedFiles(), null));
  130. }
  131. } else {
  132. noProblems = merger.merge(newHead, srcCommit);
  133. }
  134. if (noProblems) {
  135. if (AnyObjectId.isEqual(newHead.getTree().getId(),
  136. merger.getResultTreeId())) {
  137. continue;
  138. }
  139. DirCacheCheckout dco = new DirCacheCheckout(repo,
  140. newHead.getTree(), repo.lockDirCache(),
  141. merger.getResultTreeId());
  142. dco.setFailOnConflict(true);
  143. dco.setProgressMonitor(monitor);
  144. dco.checkout();
  145. if (!noCommit) {
  146. try (Git git = new Git(getRepository())) {
  147. newHead = git.commit()
  148. .setMessage(srcCommit.getFullMessage())
  149. .setReflogComment(reflogPrefix + " " //$NON-NLS-1$
  150. + srcCommit.getShortMessage())
  151. .setAuthor(srcCommit.getAuthorIdent())
  152. .setNoVerify(true).call();
  153. }
  154. }
  155. cherryPickedRefs.add(src);
  156. } else {
  157. if (failingPaths != null && !failingPaths.isEmpty()) {
  158. return new CherryPickResult(failingPaths);
  159. }
  160. // there are merge conflicts
  161. String message;
  162. if (unmergedPaths != null) {
  163. message = new MergeMessageFormatter()
  164. .formatWithConflicts(srcCommit.getFullMessage(),
  165. unmergedPaths);
  166. } else {
  167. message = srcCommit.getFullMessage();
  168. }
  169. if (!noCommit) {
  170. repo.writeCherryPickHead(srcCommit.getId());
  171. }
  172. repo.writeMergeCommitMsg(message);
  173. return CherryPickResult.CONFLICT;
  174. }
  175. }
  176. } catch (IOException e) {
  177. throw new JGitInternalException(
  178. MessageFormat.format(
  179. JGitText.get().exceptionCaughtDuringExecutionOfCherryPickCommand,
  180. e), e);
  181. }
  182. return new CherryPickResult(newHead, cherryPickedRefs);
  183. }
  184. private RevCommit getParentCommit(RevCommit srcCommit, RevWalk revWalk)
  185. throws MultipleParentsNotAllowedException, MissingObjectException,
  186. IOException {
  187. final RevCommit srcParent;
  188. if (mainlineParentNumber == null) {
  189. if (srcCommit.getParentCount() != 1)
  190. throw new MultipleParentsNotAllowedException(
  191. MessageFormat.format(
  192. JGitText.get().canOnlyCherryPickCommitsWithOneParent,
  193. srcCommit.name(),
  194. Integer.valueOf(srcCommit.getParentCount())));
  195. srcParent = srcCommit.getParent(0);
  196. } else {
  197. if (mainlineParentNumber.intValue() > srcCommit.getParentCount()) {
  198. throw new JGitInternalException(MessageFormat.format(
  199. JGitText.get().commitDoesNotHaveGivenParent, srcCommit,
  200. mainlineParentNumber));
  201. }
  202. srcParent = srcCommit
  203. .getParent(mainlineParentNumber.intValue() - 1);
  204. }
  205. revWalk.parseHeaders(srcParent);
  206. return srcParent;
  207. }
  208. /**
  209. * Include a reference to a commit
  210. *
  211. * @param commit
  212. * a reference to a commit which is cherry-picked to the current
  213. * head
  214. * @return {@code this}
  215. */
  216. public CherryPickCommand include(Ref commit) {
  217. checkCallable();
  218. commits.add(commit);
  219. return this;
  220. }
  221. /**
  222. * Include a commit
  223. *
  224. * @param commit
  225. * the Id of a commit which is cherry-picked to the current head
  226. * @return {@code this}
  227. */
  228. public CherryPickCommand include(AnyObjectId commit) {
  229. return include(commit.getName(), commit);
  230. }
  231. /**
  232. * Include a commit
  233. *
  234. * @param name
  235. * a name given to the commit
  236. * @param commit
  237. * the Id of a commit which is cherry-picked to the current head
  238. * @return {@code this}
  239. */
  240. public CherryPickCommand include(String name, AnyObjectId commit) {
  241. return include(new ObjectIdRef.Unpeeled(Storage.LOOSE, name,
  242. commit.copy()));
  243. }
  244. /**
  245. * Set the name that should be used in the "OURS" place for conflict markers
  246. *
  247. * @param ourCommitName
  248. * the name that should be used in the "OURS" place for conflict
  249. * markers
  250. * @return {@code this}
  251. */
  252. public CherryPickCommand setOurCommitName(String ourCommitName) {
  253. this.ourCommitName = ourCommitName;
  254. return this;
  255. }
  256. /**
  257. * Set the prefix to use in the reflog.
  258. * <p>
  259. * This is primarily needed for implementing rebase in terms of
  260. * cherry-picking
  261. *
  262. * @param prefix
  263. * including ":"
  264. * @return {@code this}
  265. * @since 3.1
  266. */
  267. public CherryPickCommand setReflogPrefix(String prefix) {
  268. this.reflogPrefix = prefix;
  269. return this;
  270. }
  271. /**
  272. * Set the {@code MergeStrategy}
  273. *
  274. * @param strategy
  275. * The merge strategy to use during this Cherry-pick.
  276. * @return {@code this}
  277. * @since 3.4
  278. */
  279. public CherryPickCommand setStrategy(MergeStrategy strategy) {
  280. this.strategy = strategy;
  281. return this;
  282. }
  283. /**
  284. * Sets the content merge strategy to use if the
  285. * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
  286. * "recursive".
  287. *
  288. * @param strategy
  289. * the {@link ContentMergeStrategy} to be used
  290. * @return {@code this}
  291. * @since 5.12
  292. */
  293. public CherryPickCommand setContentMergeStrategy(
  294. ContentMergeStrategy strategy) {
  295. this.contentStrategy = strategy;
  296. return this;
  297. }
  298. /**
  299. * Set the (1-based) parent number to diff against
  300. *
  301. * @param mainlineParentNumber
  302. * the (1-based) parent number to diff against. This allows
  303. * cherry-picking of merges.
  304. * @return {@code this}
  305. * @since 3.4
  306. */
  307. public CherryPickCommand setMainlineParentNumber(int mainlineParentNumber) {
  308. this.mainlineParentNumber = Integer.valueOf(mainlineParentNumber);
  309. return this;
  310. }
  311. /**
  312. * Allows cherry-picking changes without committing them.
  313. * <p>
  314. * NOTE: The behavior of cherry-pick is undefined if you pick multiple
  315. * commits or if HEAD does not match the index state before cherry-picking.
  316. *
  317. * @param noCommit
  318. * true to cherry-pick without committing, false to commit after
  319. * each pick (default)
  320. * @return {@code this}
  321. * @since 3.5
  322. */
  323. public CherryPickCommand setNoCommit(boolean noCommit) {
  324. this.noCommit = noCommit;
  325. return this;
  326. }
  327. /**
  328. * The progress monitor associated with the cherry-pick operation. By
  329. * default, this is set to <code>NullProgressMonitor</code>
  330. *
  331. * @see NullProgressMonitor
  332. * @param monitor
  333. * a {@link org.eclipse.jgit.lib.ProgressMonitor}
  334. * @return {@code this}
  335. * @since 4.11
  336. */
  337. public CherryPickCommand setProgressMonitor(ProgressMonitor monitor) {
  338. if (monitor == null) {
  339. monitor = NullProgressMonitor.INSTANCE;
  340. }
  341. this.monitor = monitor;
  342. return this;
  343. }
  344. private String calculateOurName(Ref headRef) {
  345. if (ourCommitName != null)
  346. return ourCommitName;
  347. String targetRefName = headRef.getTarget().getName();
  348. String headName = Repository.shortenRefName(targetRefName);
  349. return headName;
  350. }
  351. /** {@inheritDoc} */
  352. @SuppressWarnings("nls")
  353. @Override
  354. public String toString() {
  355. return "CherryPickCommand [repo=" + repo + ",\ncommits=" + commits
  356. + ",\nmainlineParentNumber=" + mainlineParentNumber
  357. + ", noCommit=" + noCommit + ", ourCommitName=" + ourCommitName
  358. + ", reflogPrefix=" + reflogPrefix + ", strategy=" + strategy
  359. + "]";
  360. }
  361. }