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 						try (Gitit.html#Git">Git git = new Git(getRepository())) {
146 							newHead = git.commit()
147 									.setMessage(srcCommit.getFullMessage())
148 									.setReflogComment(reflogPrefix + " " //$NON-NLS-1$
149 											+ srcCommit.getShortMessage())
150 									.setAuthor(srcCommit.getAuthorIdent())
151 									.setNoVerify(true).call();
152 						}
153 					}
154 					cherryPickedRefs.add(src);
155 				} else {
156 					if (merger.failed()) {
157 						return new CherryPickResult(merger.getFailingPaths());
158 					}
159 
160 					// there are merge conflicts
161 
162 					String message = new MergeMessageFormatter()
163 							.formatWithConflicts(srcCommit.getFullMessage(),
164 									merger.getUnmergedPaths());
165 
166 					if (!noCommit) {
167 						repo.writeCherryPickHead(srcCommit.getId());
168 					}
169 					repo.writeMergeCommitMsg(message);
170 
171 					repo.fireEvent(new WorkingTreeModifiedEvent(
172 							merger.getModifiedFiles(), null));
173 
174 					return CherryPickResult.CONFLICT;
175 				}
176 			}
177 		} catch (IOException e) {
178 			throw new JGitInternalException(
179 					MessageFormat.format(
180 							JGitText.get().exceptionCaughtDuringExecutionOfCherryPickCommand,
181 							e), e);
182 		}
183 		return new CherryPickResult(newHead, cherryPickedRefs);
184 	}
185 
186 	private RevCommit../org/eclipse/jgit/revwalk/RevCommit.html#RevCommit">RevCommit getParentCommit(RevCommit srcCommit, RevWalk revWalk)
187 			throws MultipleParentsNotAllowedException, MissingObjectException,
188 			IOException {
189 		final RevCommit srcParent;
190 		if (mainlineParentNumber == null) {
191 			if (srcCommit.getParentCount() != 1)
192 				throw new MultipleParentsNotAllowedException(
193 						MessageFormat.format(
194 								JGitText.get().canOnlyCherryPickCommitsWithOneParent,
195 								srcCommit.name(),
196 								Integer.valueOf(srcCommit.getParentCount())));
197 			srcParent = srcCommit.getParent(0);
198 		} else {
199 			if (mainlineParentNumber.intValue() > srcCommit.getParentCount()) {
200 				throw new JGitInternalException(MessageFormat.format(
201 						JGitText.get().commitDoesNotHaveGivenParent, srcCommit,
202 						mainlineParentNumber));
203 			}
204 			srcParent = srcCommit
205 					.getParent(mainlineParentNumber.intValue() - 1);
206 		}
207 
208 		revWalk.parseHeaders(srcParent);
209 		return srcParent;
210 	}
211 
212 	/**
213 	 * Include a reference to a commit
214 	 *
215 	 * @param commit
216 	 *            a reference to a commit which is cherry-picked to the current
217 	 *            head
218 	 * @return {@code this}
219 	 */
220 	public CherryPickCommand include(Ref commit) {
221 		checkCallable();
222 		commits.add(commit);
223 		return this;
224 	}
225 
226 	/**
227 	 * Include a commit
228 	 *
229 	 * @param commit
230 	 *            the Id of a commit which is cherry-picked to the current head
231 	 * @return {@code this}
232 	 */
233 	public CherryPickCommand include(AnyObjectId commit) {
234 		return include(commit.getName(), commit);
235 	}
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 	/**
252 	 * Set the name that should be used in the "OURS" place for conflict markers
253 	 *
254 	 * @param ourCommitName
255 	 *            the name that should be used in the "OURS" place for conflict
256 	 *            markers
257 	 * @return {@code this}
258 	 */
259 	public CherryPickCommand setOurCommitName(String ourCommitName) {
260 		this.ourCommitName = ourCommitName;
261 		return this;
262 	}
263 
264 	/**
265 	 * Set the prefix to use in the reflog.
266 	 * <p>
267 	 * This is primarily needed for implementing rebase in terms of
268 	 * cherry-picking
269 	 *
270 	 * @param prefix
271 	 *            including ":"
272 	 * @return {@code this}
273 	 * @since 3.1
274 	 */
275 	public CherryPickCommand setReflogPrefix(String prefix) {
276 		this.reflogPrefix = prefix;
277 		return this;
278 	}
279 
280 	/**
281 	 * Set the {@code MergeStrategy}
282 	 *
283 	 * @param strategy
284 	 *            The merge strategy to use during this Cherry-pick.
285 	 * @return {@code this}
286 	 * @since 3.4
287 	 */
288 	public CherryPickCommand setStrategy(MergeStrategy strategy) {
289 		this.strategy = strategy;
290 		return this;
291 	}
292 
293 	/**
294 	 * Set the (1-based) parent number to diff against
295 	 *
296 	 * @param mainlineParentNumber
297 	 *            the (1-based) parent number to diff against. This allows
298 	 *            cherry-picking of merges.
299 	 * @return {@code this}
300 	 * @since 3.4
301 	 */
302 	public CherryPickCommand setMainlineParentNumber(int mainlineParentNumber) {
303 		this.mainlineParentNumber = Integer.valueOf(mainlineParentNumber);
304 		return this;
305 	}
306 
307 	/**
308 	 * Allows cherry-picking changes without committing them.
309 	 * <p>
310 	 * NOTE: The behavior of cherry-pick is undefined if you pick multiple
311 	 * commits or if HEAD does not match the index state before cherry-picking.
312 	 *
313 	 * @param noCommit
314 	 *            true to cherry-pick without committing, false to commit after
315 	 *            each pick (default)
316 	 * @return {@code this}
317 	 * @since 3.5
318 	 */
319 	public CherryPickCommand setNoCommit(boolean noCommit) {
320 		this.noCommit = noCommit;
321 		return this;
322 	}
323 
324 	/**
325 	 * The progress monitor associated with the cherry-pick operation. By
326 	 * default, this is set to <code>NullProgressMonitor</code>
327 	 *
328 	 * @see NullProgressMonitor
329 	 * @param monitor
330 	 *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
331 	 * @return {@code this}
332 	 * @since 4.11
333 	 */
334 	public CherryPickCommand setProgressMonitor(ProgressMonitor monitor) {
335 		if (monitor == null) {
336 			monitor = NullProgressMonitor.INSTANCE;
337 		}
338 		this.monitor = monitor;
339 		return this;
340 	}
341 
342 	private String calculateOurName(Ref headRef) {
343 		if (ourCommitName != null)
344 			return ourCommitName;
345 
346 		String targetRefName = headRef.getTarget().getName();
347 		String headName = Repository.shortenRefName(targetRefName);
348 		return headName;
349 	}
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 
362 }