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