1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 package org.eclipse.jgit.api;
44
45 import java.io.File;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.text.MessageFormat;
49 import java.util.ArrayList;
50 import java.util.List;
51
52 import org.eclipse.jgit.api.ResetCommand.ResetType;
53 import org.eclipse.jgit.api.errors.GitAPIException;
54 import org.eclipse.jgit.api.errors.JGitInternalException;
55 import org.eclipse.jgit.api.errors.NoHeadException;
56 import org.eclipse.jgit.api.errors.UnmergedPathsException;
57 import org.eclipse.jgit.dircache.DirCache;
58 import org.eclipse.jgit.dircache.DirCacheBuilder;
59 import org.eclipse.jgit.dircache.DirCacheEditor;
60 import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
61 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
62 import org.eclipse.jgit.dircache.DirCacheEntry;
63 import org.eclipse.jgit.dircache.DirCacheIterator;
64 import org.eclipse.jgit.errors.UnmergedPathException;
65 import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
66 import org.eclipse.jgit.internal.JGitText;
67 import org.eclipse.jgit.lib.CommitBuilder;
68 import org.eclipse.jgit.lib.Constants;
69 import org.eclipse.jgit.lib.MutableObjectId;
70 import org.eclipse.jgit.lib.ObjectId;
71 import org.eclipse.jgit.lib.ObjectInserter;
72 import org.eclipse.jgit.lib.ObjectReader;
73 import org.eclipse.jgit.lib.PersonIdent;
74 import org.eclipse.jgit.lib.Ref;
75 import org.eclipse.jgit.lib.RefUpdate;
76 import org.eclipse.jgit.lib.Repository;
77 import org.eclipse.jgit.revwalk.RevCommit;
78 import org.eclipse.jgit.revwalk.RevWalk;
79 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
80 import org.eclipse.jgit.treewalk.FileTreeIterator;
81 import org.eclipse.jgit.treewalk.TreeWalk;
82 import org.eclipse.jgit.treewalk.WorkingTreeIterator;
83 import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
84 import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
85 import org.eclipse.jgit.treewalk.filter.SkipWorkTreeFilter;
86 import org.eclipse.jgit.util.FileUtils;
87
88
89
90
91
92
93
94
95
96 public class StashCreateCommand extends GitCommand<RevCommit> {
97
98 private static final String MSG_INDEX = "index on {0}: {1} {2}";
99
100 private static final String MSG_UNTRACKED = "untracked files on {0}: {1} {2}";
101
102 private static final String MSG_WORKING_DIR = "WIP on {0}: {1} {2}";
103
104 private String indexMessage = MSG_INDEX;
105
106 private String workingDirectoryMessage = MSG_WORKING_DIR;
107
108 private String ref = Constants.R_STASH;
109
110 private PersonIdent person;
111
112 private boolean includeUntracked;
113
114
115
116
117
118
119
120 public StashCreateCommand(Repository repo) {
121 super(repo);
122 person = new PersonIdent(repo);
123 }
124
125
126
127
128
129
130
131
132
133
134
135 public StashCreateCommand setIndexMessage(String message) {
136 indexMessage = message;
137 return this;
138 }
139
140
141
142
143
144
145
146
147
148
149
150 public StashCreateCommand setWorkingDirectoryMessage(String message) {
151 workingDirectoryMessage = message;
152 return this;
153 }
154
155
156
157
158
159
160
161
162
163 public StashCreateCommand setPerson(PersonIdent person) {
164 this.person = person;
165 return this;
166 }
167
168
169
170
171
172
173
174
175
176
177
178 public StashCreateCommand setRef(String ref) {
179 this.ref = ref;
180 return this;
181 }
182
183
184
185
186
187
188
189
190
191 public StashCreateCommand setIncludeUntracked(boolean includeUntracked) {
192 this.includeUntracked = includeUntracked;
193 return this;
194 }
195
196 private RevCommit parseCommit(final ObjectReader reader,
197 final ObjectId headId) throws IOException {
198 try (RevWalkvWalk.html#RevWalk">RevWalk walk = new RevWalk(reader)) {
199 return walk.parseCommit(headId);
200 }
201 }
202
203 private CommitBuilder createBuilder() {
204 CommitBuilder builder = new CommitBuilder();
205 PersonIdent author = person;
206 if (author == null)
207 author = new PersonIdent(repo);
208 builder.setAuthor(author);
209 builder.setCommitter(author);
210 return builder;
211 }
212
213 private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent,
214 String refLogMessage) throws IOException {
215 if (ref == null)
216 return;
217 Ref currentRef = repo.findRef(ref);
218 RefUpdate refUpdate = repo.updateRef(ref);
219 refUpdate.setNewObjectId(commitId);
220 refUpdate.setRefLogIdent(refLogIdent);
221 refUpdate.setRefLogMessage(refLogMessage, false);
222 refUpdate.setForceRefLog(true);
223 if (currentRef != null)
224 refUpdate.setExpectedOldObjectId(currentRef.getObjectId());
225 else
226 refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
227 refUpdate.forceUpdate();
228 }
229
230 private Ref getHead() throws GitAPIException {
231 try {
232 Ref head = repo.exactRef(Constants.HEAD);
233 if (head == null || head.getObjectId() == null)
234 throw new NoHeadException(JGitText.get().headRequiredToStash);
235 return head;
236 } catch (IOException e) {
237 throw new JGitInternalException(JGitText.get().stashFailed, e);
238 }
239 }
240
241
242
243
244
245
246
247 @Override
248 public RevCommit call() throws GitAPIException {
249 checkCallable();
250
251 List<String> deletedFiles = new ArrayList<>();
252 Ref head = getHead();
253 try (ObjectReader reader = repo.newObjectReader()) {
254 RevCommit headCommit = parseCommit(reader, head.getObjectId());
255 DirCache cache = repo.lockDirCache();
256 ObjectId commitId;
257 try (ObjectInserter inserter = repo.newObjectInserter();
258 TreeWalk treeWalk = new TreeWalk(repo, reader)) {
259
260 treeWalk.setRecursive(true);
261 treeWalk.addTree(headCommit.getTree());
262 treeWalk.addTree(new DirCacheIterator(cache));
263 treeWalk.addTree(new FileTreeIterator(repo));
264 treeWalk.getTree(2, FileTreeIterator.class)
265 .setDirCacheIterator(treeWalk, 1);
266 treeWalk.setFilter(AndTreeFilter.create(new SkipWorkTreeFilter(
267 1), new IndexDiffFilter(1, 2)));
268
269
270 if (!treeWalk.next())
271 return null;
272
273 MutableObjectId id = new MutableObjectId();
274 List<PathEdit> wtEdits = new ArrayList<>();
275 List<String> wtDeletes = new ArrayList<>();
276 List<DirCacheEntry> untracked = new ArrayList<>();
277 boolean hasChanges = false;
278 do {
279 AbstractTreeIterator headIter = treeWalk.getTree(0,
280 AbstractTreeIterator.class);
281 DirCacheIterator indexIter = treeWalk.getTree(1,
282 DirCacheIterator.class);
283 WorkingTreeIterator wtIter = treeWalk.getTree(2,
284 WorkingTreeIterator.class);
285 if (indexIter != null
286 && !indexIter.getDirCacheEntry().isMerged())
287 throw new UnmergedPathsException(
288 new UnmergedPathException(
289 indexIter.getDirCacheEntry()));
290 if (wtIter != null) {
291 if (indexIter == null && headIter == null
292 && !includeUntracked)
293 continue;
294 hasChanges = true;
295 if (indexIter != null && wtIter.idEqual(indexIter))
296 continue;
297 if (headIter != null && wtIter.idEqual(headIter))
298 continue;
299 treeWalk.getObjectId(id, 0);
300 final DirCacheEntryEntry.html#DirCacheEntry">DirCacheEntry entry = new DirCacheEntry(
301 treeWalk.getRawPath());
302 entry.setLength(wtIter.getEntryLength());
303 entry.setLastModified(
304 wtIter.getEntryLastModifiedInstant());
305 entry.setFileMode(wtIter.getEntryFileMode());
306 long contentLength = wtIter.getEntryContentLength();
307 try (InputStream in = wtIter.openEntryStream()) {
308 entry.setObjectId(inserter.insert(
309 Constants.OBJ_BLOB, contentLength, in));
310 }
311
312 if (indexIter == null && headIter == null)
313 untracked.add(entry);
314 else
315 wtEdits.add(new PathEdit(entry) {
316 @Override
317 public void apply(DirCacheEntry ent) {
318 ent.copyMetaData(entry);
319 }
320 });
321 }
322 hasChanges = true;
323 if (wtIter == null && headIter != null)
324 wtDeletes.add(treeWalk.getPathString());
325 } while (treeWalk.next());
326
327 if (!hasChanges)
328 return null;
329
330 String branch = Repository.shortenRefName(head.getTarget()
331 .getName());
332
333
334 CommitBuilder builder = createBuilder();
335 builder.setParentId(headCommit);
336 builder.setTreeId(cache.writeTree(inserter));
337 builder.setMessage(MessageFormat.format(indexMessage, branch,
338 headCommit.abbreviate(7).name(),
339 headCommit.getShortMessage()));
340 ObjectId indexCommit = inserter.insert(builder);
341
342
343 ObjectId untrackedCommit = null;
344 if (!untracked.isEmpty()) {
345 DirCache untrackedDirCache = DirCache.newInCore();
346 DirCacheBuilder untrackedBuilder = untrackedDirCache
347 .builder();
348 for (DirCacheEntry entry : untracked)
349 untrackedBuilder.add(entry);
350 untrackedBuilder.finish();
351
352 builder.setParentIds(new ObjectId[0]);
353 builder.setTreeId(untrackedDirCache.writeTree(inserter));
354 builder.setMessage(MessageFormat.format(MSG_UNTRACKED,
355 branch, headCommit.abbreviate(7).name(),
356 headCommit.getShortMessage()));
357 untrackedCommit = inserter.insert(builder);
358 }
359
360
361 if (!wtEdits.isEmpty() || !wtDeletes.isEmpty()) {
362 DirCacheEditor editor = cache.editor();
363 for (PathEdit edit : wtEdits)
364 editor.add(edit);
365 for (String path : wtDeletes)
366 editor.add(new DeletePath(path));
367 editor.finish();
368 }
369 builder.setParentId(headCommit);
370 builder.addParentId(indexCommit);
371 if (untrackedCommit != null)
372 builder.addParentId(untrackedCommit);
373 builder.setMessage(MessageFormat.format(
374 workingDirectoryMessage, branch,
375 headCommit.abbreviate(7).name(),
376 headCommit.getShortMessage()));
377 builder.setTreeId(cache.writeTree(inserter));
378 commitId = inserter.insert(builder);
379 inserter.flush();
380
381 updateStashRef(commitId, builder.getAuthor(),
382 builder.getMessage());
383
384
385 if (includeUntracked) {
386 for (DirCacheEntry entry : untracked) {
387 String repoRelativePath = entry.getPathString();
388 File file = new File(repo.getWorkTree(),
389 repoRelativePath);
390 FileUtils.delete(file);
391 deletedFiles.add(repoRelativePath);
392 }
393 }
394
395 } finally {
396 cache.unlock();
397 }
398
399
400 new ResetCommand(repo).setMode(ResetType.HARD).call();
401
402
403 return parseCommit(reader, commitId);
404 } catch (IOException e) {
405 throw new JGitInternalException(JGitText.get().stashFailed, e);
406 } finally {
407 if (!deletedFiles.isEmpty()) {
408 repo.fireEvent(
409 new WorkingTreeModifiedEvent(null, deletedFiles));
410 }
411 }
412 }
413 }