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