View Javadoc
1   /*
2    * Copyright (C) 2012, 2017 GitHub Inc. 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.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
13  
14  import java.io.IOException;
15  import java.text.MessageFormat;
16  import java.util.HashSet;
17  import java.util.List;
18  import java.util.Set;
19  
20  import org.eclipse.jgit.api.errors.GitAPIException;
21  import org.eclipse.jgit.api.errors.InvalidRefNameException;
22  import org.eclipse.jgit.api.errors.JGitInternalException;
23  import org.eclipse.jgit.api.errors.NoHeadException;
24  import org.eclipse.jgit.api.errors.StashApplyFailureException;
25  import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
26  import org.eclipse.jgit.dircache.DirCache;
27  import org.eclipse.jgit.dircache.DirCacheBuilder;
28  import org.eclipse.jgit.dircache.DirCacheCheckout;
29  import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
30  import org.eclipse.jgit.dircache.DirCacheEntry;
31  import org.eclipse.jgit.dircache.DirCacheIterator;
32  import org.eclipse.jgit.errors.CheckoutConflictException;
33  import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
34  import org.eclipse.jgit.internal.JGitText;
35  import org.eclipse.jgit.lib.Constants;
36  import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
37  import org.eclipse.jgit.lib.ObjectId;
38  import org.eclipse.jgit.lib.ObjectReader;
39  import org.eclipse.jgit.lib.Repository;
40  import org.eclipse.jgit.lib.RepositoryState;
41  import org.eclipse.jgit.merge.MergeStrategy;
42  import org.eclipse.jgit.merge.ResolveMerger;
43  import org.eclipse.jgit.revwalk.RevCommit;
44  import org.eclipse.jgit.revwalk.RevTree;
45  import org.eclipse.jgit.revwalk.RevWalk;
46  import org.eclipse.jgit.treewalk.AbstractTreeIterator;
47  import org.eclipse.jgit.treewalk.FileTreeIterator;
48  import org.eclipse.jgit.treewalk.TreeWalk;
49  
50  /**
51   * Command class to apply a stashed commit.
52   *
53   * This class behaves like <em>git stash apply --index</em>, i.e. it tries to
54   * recover the stashed index state in addition to the working tree state.
55   *
56   * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-stash.html"
57   *      >Git documentation about Stash</a>
58   * @since 2.0
59   */
60  public class StashApplyCommand extends GitCommand<ObjectId> {
61  
62  	private static final String DEFAULT_REF = Constants.STASH + "@{0}"; //$NON-NLS-1$
63  
64  	private String stashRef;
65  
66  	private boolean restoreIndex = true;
67  
68  	private boolean restoreUntracked = true;
69  
70  	private boolean ignoreRepositoryState;
71  
72  	private MergeStrategy strategy = MergeStrategy.RECURSIVE;
73  
74  	/**
75  	 * Create command to apply the changes of a stashed commit
76  	 *
77  	 * @param repo
78  	 *            the {@link org.eclipse.jgit.lib.Repository} to apply the stash
79  	 *            to
80  	 */
81  	public StashApplyCommand(Repository repo) {
82  		super(repo);
83  	}
84  
85  	/**
86  	 * Set the stash reference to apply
87  	 * <p>
88  	 * This will default to apply the latest stashed commit (stash@{0}) if
89  	 * unspecified
90  	 *
91  	 * @param stashRef
92  	 *            name of the stash {@code Ref} to apply
93  	 * @return {@code this}
94  	 */
95  	public StashApplyCommand setStashRef(String stashRef) {
96  		this.stashRef = stashRef;
97  		return this;
98  	}
99  
100 	/**
101 	 * Whether to ignore the repository state when applying the stash
102 	 *
103 	 * @param willIgnoreRepositoryState
104 	 *            whether to ignore the repository state when applying the stash
105 	 * @return {@code this}
106 	 * @since 3.2
107 	 */
108 	public StashApplyCommand ignoreRepositoryState(boolean willIgnoreRepositoryState) {
109 		this.ignoreRepositoryState = willIgnoreRepositoryState;
110 		return this;
111 	}
112 
113 	private ObjectId getStashId() throws GitAPIException {
114 		final String revision = stashRef != null ? stashRef : DEFAULT_REF;
115 		final ObjectId stashId;
116 		try {
117 			stashId = repo.resolve(revision);
118 		} catch (IOException e) {
119 			throw new InvalidRefNameException(MessageFormat.format(
120 					JGitText.get().stashResolveFailed, revision), e);
121 		}
122 		if (stashId == null)
123 			throw new InvalidRefNameException(MessageFormat.format(
124 					JGitText.get().stashResolveFailed, revision));
125 		return stashId;
126 	}
127 
128 	/**
129 	 * {@inheritDoc}
130 	 * <p>
131 	 * Apply the changes in a stashed commit to the working directory and index
132 	 */
133 	@Override
134 	public ObjectId call() throws GitAPIException,
135 			WrongRepositoryStateException, NoHeadException,
136 			StashApplyFailureException {
137 		checkCallable();
138 
139 		if (!ignoreRepositoryState
140 				&& repo.getRepositoryState() != RepositoryState.SAFE)
141 			throw new WrongRepositoryStateException(MessageFormat.format(
142 					JGitText.get().stashApplyOnUnsafeRepository,
143 					repo.getRepositoryState()));
144 
145 		try (ObjectReader reader = repo.newObjectReader();
146 				RevWalk revWalk = new RevWalk(reader)) {
147 
148 			ObjectId headCommit = repo.resolve(Constants.HEAD);
149 			if (headCommit == null)
150 				throw new NoHeadException(JGitText.get().stashApplyWithoutHead);
151 
152 			final ObjectId stashId = getStashId();
153 			RevCommit stashCommit = revWalk.parseCommit(stashId);
154 			if (stashCommit.getParentCount() < 2
155 					|| stashCommit.getParentCount() > 3)
156 				throw new JGitInternalException(MessageFormat.format(
157 						JGitText.get().stashCommitIncorrectNumberOfParents,
158 						stashId.name(),
159 						Integer.valueOf(stashCommit.getParentCount())));
160 
161 			ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}"); //$NON-NLS-1$
162 			ObjectId stashIndexCommit = revWalk.parseCommit(stashCommit
163 					.getParent(1));
164 			ObjectId stashHeadCommit = stashCommit.getParent(0);
165 			ObjectId untrackedCommit = null;
166 			if (restoreUntracked && stashCommit.getParentCount() == 3)
167 				untrackedCommit = revWalk.parseCommit(stashCommit.getParent(2));
168 
169 			ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo);
170 			merger.setCommitNames(new String[] { "stashed HEAD", "HEAD", //$NON-NLS-1$ //$NON-NLS-2$
171 					"stash" }); //$NON-NLS-1$
172 			merger.setBase(stashHeadCommit);
173 			merger.setWorkingTreeIterator(new FileTreeIterator(repo));
174 			boolean mergeSucceeded = merger.merge(headCommit, stashCommit);
175 			List<String> modifiedByMerge = merger.getModifiedFiles();
176 			if (!modifiedByMerge.isEmpty()) {
177 				repo.fireEvent(
178 						new WorkingTreeModifiedEvent(modifiedByMerge, null));
179 			}
180 			if (mergeSucceeded) {
181 				DirCache dc = repo.lockDirCache();
182 				DirCacheCheckout dco = new DirCacheCheckout(repo, headTree,
183 						dc, merger.getResultTreeId());
184 				dco.setFailOnConflict(true);
185 				dco.checkout(); // Ignoring failed deletes....
186 				if (restoreIndex) {
187 					ResolveMerger ixMerger = (ResolveMerger) strategy
188 							.newMerger(repo, true);
189 					ixMerger.setCommitNames(new String[] { "stashed HEAD", //$NON-NLS-1$
190 							"HEAD", "stashed index" }); //$NON-NLS-1$//$NON-NLS-2$
191 					ixMerger.setBase(stashHeadCommit);
192 					boolean ok = ixMerger.merge(headCommit, stashIndexCommit);
193 					if (ok) {
194 						resetIndex(revWalk
195 								.parseTree(ixMerger.getResultTreeId()));
196 					} else {
197 						throw new StashApplyFailureException(
198 								JGitText.get().stashApplyConflict);
199 					}
200 				}
201 
202 				if (untrackedCommit != null) {
203 					ResolveMerger untrackedMerger = (ResolveMerger) strategy
204 							.newMerger(repo, true);
205 					untrackedMerger.setCommitNames(new String[] {
206 							"null", "HEAD", "untracked files" }); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
207 					// There is no common base for HEAD & untracked files
208 					// because the commit for untracked files has no parent. If
209 					// we use stashHeadCommit as common base (as in the other
210 					// merges) we potentially report conflicts for files
211 					// which are not even member of untracked files commit
212 					untrackedMerger.setBase(null);
213 					boolean ok = untrackedMerger.merge(headCommit,
214 							untrackedCommit);
215 					if (ok) {
216 						try {
217 							RevTree untrackedTree = revWalk
218 									.parseTree(untrackedCommit);
219 							resetUntracked(untrackedTree);
220 						} catch (CheckoutConflictException e) {
221 							throw new StashApplyFailureException(
222 									JGitText.get().stashApplyConflict, e);
223 						}
224 					} else {
225 						throw new StashApplyFailureException(
226 								JGitText.get().stashApplyConflict);
227 					}
228 				}
229 			} else {
230 				throw new StashApplyFailureException(
231 						JGitText.get().stashApplyConflict);
232 			}
233 			return stashId;
234 
235 		} catch (JGitInternalException e) {
236 			throw e;
237 		} catch (IOException e) {
238 			throw new JGitInternalException(JGitText.get().stashApplyFailed, e);
239 		}
240 	}
241 
242 	/**
243 	 * Whether to restore the index state
244 	 *
245 	 * @param applyIndex
246 	 *            true (default) if the command should restore the index state
247 	 * @deprecated use {@link #setRestoreIndex} instead
248 	 */
249 	@Deprecated
250 	public void setApplyIndex(boolean applyIndex) {
251 		this.restoreIndex = applyIndex;
252 	}
253 
254 	/**
255 	 * Whether to restore the index state
256 	 *
257 	 * @param restoreIndex
258 	 *            true (default) if the command should restore the index state
259 	 * @return {@code this}
260 	 * @since 5.3
261 	 */
262 	public StashApplyCommand setRestoreIndex(boolean restoreIndex) {
263 		this.restoreIndex = restoreIndex;
264 		return this;
265 	}
266 
267 	/**
268 	 * Set the <code>MergeStrategy</code> to use.
269 	 *
270 	 * @param strategy
271 	 *            The merge strategy to use in order to merge during this
272 	 *            command execution.
273 	 * @return {@code this}
274 	 * @since 3.4
275 	 */
276 	public StashApplyCommand setStrategy(MergeStrategy strategy) {
277 		this.strategy = strategy;
278 		return this;
279 	}
280 
281 	/**
282 	 * Whether the command should restore untracked files
283 	 *
284 	 * @param applyUntracked
285 	 *            true (default) if the command should restore untracked files
286 	 * @since 3.4
287 	 * @deprecated use {@link #setRestoreUntracked} instead
288 	 */
289 	@Deprecated
290 	public void setApplyUntracked(boolean applyUntracked) {
291 		this.restoreUntracked = applyUntracked;
292 	}
293 
294 	/**
295 	 * Whether the command should restore untracked files
296 	 *
297 	 * @param restoreUntracked
298 	 *            true (default) if the command should restore untracked files
299 	 * @return {@code this}
300 	 * @since 5.3
301 	 */
302 	public StashApplyCommand setRestoreUntracked(boolean restoreUntracked) {
303 		this.restoreUntracked = restoreUntracked;
304 		return this;
305 	}
306 
307 	private void resetIndex(RevTree tree) throws IOException {
308 		DirCache dc = repo.lockDirCache();
309 		try (TreeWalkeeWalk.html#TreeWalk">TreeWalk walk = new TreeWalk(repo)) {
310 			DirCacheBuilder builder = dc.builder();
311 
312 			walk.addTree(tree);
313 			walk.addTree(new DirCacheIterator(dc));
314 			walk.setRecursive(true);
315 
316 			while (walk.next()) {
317 				AbstractTreeIterator cIter = walk.getTree(0,
318 						AbstractTreeIterator.class);
319 				if (cIter == null) {
320 					// Not in commit, don't add to new index
321 					continue;
322 				}
323 
324 				final DirCacheEntryEntry.html#DirCacheEntry">DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
325 				entry.setFileMode(cIter.getEntryFileMode());
326 				entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());
327 
328 				DirCacheIterator dcIter = walk.getTree(1,
329 						DirCacheIterator.class);
330 				if (dcIter != null && dcIter.idEqual(cIter)) {
331 					DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
332 					entry.setLastModified(indexEntry.getLastModifiedInstant());
333 					entry.setLength(indexEntry.getLength());
334 				}
335 
336 				builder.add(entry);
337 			}
338 
339 			builder.commit();
340 		} finally {
341 			dc.unlock();
342 		}
343 	}
344 
345 	private void resetUntracked(RevTree tree) throws CheckoutConflictException,
346 			IOException {
347 		Set<String> actuallyModifiedPaths = new HashSet<>();
348 		// TODO maybe NameConflictTreeWalk ?
349 		try (TreeWalkeeWalk.html#TreeWalk">TreeWalk walk = new TreeWalk(repo)) {
350 			walk.addTree(tree);
351 			walk.addTree(new FileTreeIterator(repo));
352 			walk.setRecursive(true);
353 
354 			final ObjectReader reader = walk.getObjectReader();
355 
356 			while (walk.next()) {
357 				final AbstractTreeIterator cIter = walk.getTree(0,
358 						AbstractTreeIterator.class);
359 				if (cIter == null)
360 					// Not in commit, don't create untracked
361 					continue;
362 
363 				final EolStreamType eolStreamType = walk
364 						.getEolStreamType(CHECKOUT_OP);
365 				final DirCacheEntryEntry.html#DirCacheEntry">DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
366 				entry.setFileMode(cIter.getEntryFileMode());
367 				entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());
368 
369 				FileTreeIterator fIter = walk
370 						.getTree(1, FileTreeIterator.class);
371 				if (fIter != null) {
372 					if (fIter.isModified(entry, true, reader)) {
373 						// file exists and is dirty
374 						throw new CheckoutConflictException(
375 								entry.getPathString());
376 					}
377 				}
378 
379 				checkoutPath(entry, reader,
380 						new CheckoutMetadata(eolStreamType, null));
381 				actuallyModifiedPaths.add(entry.getPathString());
382 			}
383 		} finally {
384 			if (!actuallyModifiedPaths.isEmpty()) {
385 				repo.fireEvent(new WorkingTreeModifiedEvent(
386 						actuallyModifiedPaths, null));
387 			}
388 		}
389 	}
390 
391 	private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
392 			CheckoutMetadata checkoutMetadata) {
393 		try {
394 			DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
395 					checkoutMetadata);
396 		} catch (IOException e) {
397 			throw new JGitInternalException(MessageFormat.format(
398 					JGitText.get().checkoutConflictWithFile,
399 					entry.getPathString()), e);
400 		}
401 	}
402 }