1
2
3
4
5
6
7
8
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.ContentMergeStrategy;
42 import org.eclipse.jgit.merge.MergeStrategy;
43 import org.eclipse.jgit.merge.Merger;
44 import org.eclipse.jgit.merge.ResolveMerger;
45 import org.eclipse.jgit.revwalk.RevCommit;
46 import org.eclipse.jgit.revwalk.RevTree;
47 import org.eclipse.jgit.revwalk.RevWalk;
48 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
49 import org.eclipse.jgit.treewalk.FileTreeIterator;
50 import org.eclipse.jgit.treewalk.TreeWalk;
51
52
53
54
55
56
57
58
59
60
61
62 public class StashApplyCommand extends GitCommand<ObjectId> {
63
64 private static final String DEFAULT_REF = Constants.STASH + "@{0}";
65
66 private String stashRef;
67
68 private boolean restoreIndex = true;
69
70 private boolean restoreUntracked = true;
71
72 private boolean ignoreRepositoryState;
73
74 private MergeStrategy strategy = MergeStrategy.RECURSIVE;
75
76 private ContentMergeStrategy contentStrategy;
77
78
79
80
81
82
83
84
85 public StashApplyCommand(Repository repo) {
86 super(repo);
87 }
88
89
90
91
92
93
94
95
96
97
98
99 public StashApplyCommand setStashRef(String stashRef) {
100 this.stashRef = stashRef;
101 return this;
102 }
103
104
105
106
107
108
109
110
111
112 public StashApplyCommand ignoreRepositoryState(boolean willIgnoreRepositoryState) {
113 this.ignoreRepositoryState = willIgnoreRepositoryState;
114 return this;
115 }
116
117 private ObjectId getStashId() throws GitAPIException {
118 final String revision = stashRef != null ? stashRef : DEFAULT_REF;
119 final ObjectId stashId;
120 try {
121 stashId = repo.resolve(revision);
122 } catch (IOException e) {
123 throw new InvalidRefNameException(MessageFormat.format(
124 JGitText.get().stashResolveFailed, revision), e);
125 }
126 if (stashId == null)
127 throw new InvalidRefNameException(MessageFormat.format(
128 JGitText.get().stashResolveFailed, revision));
129 return stashId;
130 }
131
132
133
134
135
136
137 @Override
138 public ObjectId call() throws GitAPIException,
139 WrongRepositoryStateException, NoHeadException,
140 StashApplyFailureException {
141 checkCallable();
142
143 if (!ignoreRepositoryState
144 && repo.getRepositoryState() != RepositoryState.SAFE)
145 throw new WrongRepositoryStateException(MessageFormat.format(
146 JGitText.get().stashApplyOnUnsafeRepository,
147 repo.getRepositoryState()));
148
149 try (ObjectReader reader = repo.newObjectReader();
150 RevWalk revWalk = new RevWalk(reader)) {
151
152 ObjectId headCommit = repo.resolve(Constants.HEAD);
153 if (headCommit == null)
154 throw new NoHeadException(JGitText.get().stashApplyWithoutHead);
155
156 final ObjectId stashId = getStashId();
157 RevCommit stashCommit = revWalk.parseCommit(stashId);
158 if (stashCommit.getParentCount() < 2
159 || stashCommit.getParentCount() > 3)
160 throw new JGitInternalException(MessageFormat.format(
161 JGitText.get().stashCommitIncorrectNumberOfParents,
162 stashId.name(),
163 Integer.valueOf(stashCommit.getParentCount())));
164
165 ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}");
166 ObjectId stashIndexCommit = revWalk.parseCommit(stashCommit
167 .getParent(1));
168 ObjectId stashHeadCommit = stashCommit.getParent(0);
169 ObjectId untrackedCommit = null;
170 if (restoreUntracked && stashCommit.getParentCount() == 3)
171 untrackedCommit = revWalk.parseCommit(stashCommit.getParent(2));
172
173 Merger merger = strategy.newMerger(repo);
174 boolean mergeSucceeded;
175 if (merger instanceof ResolveMerger) {
176 ResolveMerger resolveMerger = (ResolveMerger) merger;
177 resolveMerger
178 .setCommitNames(new String[] { "stashed HEAD", "HEAD",
179 "stash" });
180 resolveMerger.setBase(stashHeadCommit);
181 resolveMerger
182 .setWorkingTreeIterator(new FileTreeIterator(repo));
183 resolveMerger.setContentMergeStrategy(contentStrategy);
184 mergeSucceeded = resolveMerger.merge(headCommit, stashCommit);
185 List<String> modifiedByMerge = resolveMerger.getModifiedFiles();
186 if (!modifiedByMerge.isEmpty()) {
187 repo.fireEvent(new WorkingTreeModifiedEvent(modifiedByMerge,
188 null));
189 }
190 } else {
191 mergeSucceeded = merger.merge(headCommit, stashCommit);
192 }
193 if (mergeSucceeded) {
194 DirCache dc = repo.lockDirCache();
195 DirCacheCheckout dco = new DirCacheCheckout(repo, headTree,
196 dc, merger.getResultTreeId());
197 dco.setFailOnConflict(true);
198 dco.checkout();
199 if (restoreIndex) {
200 Merger ixMerger = strategy.newMerger(repo, true);
201 if (ixMerger instanceof ResolveMerger) {
202 ResolveMerger resolveMerger = (ResolveMerger) ixMerger;
203 resolveMerger.setCommitNames(new String[] { "stashed HEAD",
204 "HEAD", "stashed index" });
205 resolveMerger.setBase(stashHeadCommit);
206 resolveMerger.setContentMergeStrategy(contentStrategy);
207 }
208 boolean ok = ixMerger.merge(headCommit, stashIndexCommit);
209 if (ok) {
210 resetIndex(revWalk
211 .parseTree(ixMerger.getResultTreeId()));
212 } else {
213 throw new StashApplyFailureException(
214 JGitText.get().stashApplyConflict);
215 }
216 }
217
218 if (untrackedCommit != null) {
219 Merger untrackedMerger = strategy.newMerger(repo, true);
220 if (untrackedMerger instanceof ResolveMerger) {
221 ResolveMerger resolveMerger = (ResolveMerger) untrackedMerger;
222 resolveMerger.setCommitNames(new String[] { "null", "HEAD",
223 "untracked files" });
224
225
226
227
228
229
230 resolveMerger.setBase(null);
231 resolveMerger.setContentMergeStrategy(contentStrategy);
232 }
233 boolean ok = untrackedMerger.merge(headCommit,
234 untrackedCommit);
235 if (ok) {
236 try {
237 RevTree untrackedTree = revWalk
238 .parseTree(untrackedCommit);
239 resetUntracked(untrackedTree);
240 } catch (CheckoutConflictException e) {
241 throw new StashApplyFailureException(
242 JGitText.get().stashApplyConflict, e);
243 }
244 } else {
245 throw new StashApplyFailureException(
246 JGitText.get().stashApplyConflict);
247 }
248 }
249 } else {
250 throw new StashApplyFailureException(
251 JGitText.get().stashApplyConflict);
252 }
253 return stashId;
254
255 } catch (JGitInternalException e) {
256 throw e;
257 } catch (IOException e) {
258 throw new JGitInternalException(JGitText.get().stashApplyFailed, e);
259 }
260 }
261
262
263
264
265
266
267
268
269 @Deprecated
270 public void setApplyIndex(boolean applyIndex) {
271 this.restoreIndex = applyIndex;
272 }
273
274
275
276
277
278
279
280
281
282 public StashApplyCommand setRestoreIndex(boolean restoreIndex) {
283 this.restoreIndex = restoreIndex;
284 return this;
285 }
286
287
288
289
290
291
292
293
294
295
296 public StashApplyCommand setStrategy(MergeStrategy strategy) {
297 this.strategy = strategy;
298 return this;
299 }
300
301
302
303
304
305
306
307
308
309
310
311 public StashApplyCommand setContentMergeStrategy(
312 ContentMergeStrategy strategy) {
313 checkCallable();
314 this.contentStrategy = strategy;
315 return this;
316 }
317
318
319
320
321
322
323
324
325
326 @Deprecated
327 public void setApplyUntracked(boolean applyUntracked) {
328 this.restoreUntracked = applyUntracked;
329 }
330
331
332
333
334
335
336
337
338
339 public StashApplyCommand setRestoreUntracked(boolean restoreUntracked) {
340 this.restoreUntracked = restoreUntracked;
341 return this;
342 }
343
344 private void resetIndex(RevTree tree) throws IOException {
345 DirCache dc = repo.lockDirCache();
346 try (TreeWalk walk = new TreeWalk(repo)) {
347 DirCacheBuilder builder = dc.builder();
348
349 walk.addTree(tree);
350 walk.addTree(new DirCacheIterator(dc));
351 walk.setRecursive(true);
352
353 while (walk.next()) {
354 AbstractTreeIterator cIter = walk.getTree(0,
355 AbstractTreeIterator.class);
356 if (cIter == null) {
357
358 continue;
359 }
360
361 final DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
362 entry.setFileMode(cIter.getEntryFileMode());
363 entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());
364
365 DirCacheIterator dcIter = walk.getTree(1,
366 DirCacheIterator.class);
367 if (dcIter != null && dcIter.idEqual(cIter)) {
368 DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
369 entry.setLastModified(indexEntry.getLastModifiedInstant());
370 entry.setLength(indexEntry.getLength());
371 }
372
373 builder.add(entry);
374 }
375
376 builder.commit();
377 } finally {
378 dc.unlock();
379 }
380 }
381
382 private void resetUntracked(RevTree tree) throws CheckoutConflictException,
383 IOException {
384 Set<String> actuallyModifiedPaths = new HashSet<>();
385
386 try (TreeWalk walk = new TreeWalk(repo)) {
387 walk.addTree(tree);
388 walk.addTree(new FileTreeIterator(repo));
389 walk.setRecursive(true);
390
391 final ObjectReader reader = walk.getObjectReader();
392
393 while (walk.next()) {
394 final AbstractTreeIterator cIter = walk.getTree(0,
395 AbstractTreeIterator.class);
396 if (cIter == null)
397
398 continue;
399
400 final EolStreamType eolStreamType = walk
401 .getEolStreamType(CHECKOUT_OP);
402 final DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
403 entry.setFileMode(cIter.getEntryFileMode());
404 entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());
405
406 FileTreeIterator fIter = walk
407 .getTree(1, FileTreeIterator.class);
408 if (fIter != null) {
409 if (fIter.isModified(entry, true, reader)) {
410
411 throw new CheckoutConflictException(
412 entry.getPathString());
413 }
414 }
415
416 checkoutPath(entry, reader,
417 new CheckoutMetadata(eolStreamType, null));
418 actuallyModifiedPaths.add(entry.getPathString());
419 }
420 } finally {
421 if (!actuallyModifiedPaths.isEmpty()) {
422 repo.fireEvent(new WorkingTreeModifiedEvent(
423 actuallyModifiedPaths, null));
424 }
425 }
426 }
427
428 private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
429 CheckoutMetadata checkoutMetadata) {
430 try {
431 DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
432 checkoutMetadata);
433 } catch (IOException e) {
434 throw new JGitInternalException(MessageFormat.format(
435 JGitText.get().checkoutConflictWithFile,
436 entry.getPathString()), e);
437 }
438 }
439 }