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