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