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