View Javadoc
1   /*
2    * Copyright (C) 2012, GitHub Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
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   * Command class to stash changes in the working directory and index in a
59   * commit.
60   *
61   * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-stash.html"
62   *      >Git documentation about Stash</a>
63   * @since 2.0
64   */
65  public class StashCreateCommand extends GitCommand<RevCommit> {
66  
67  	private static final String MSG_INDEX = "index on {0}: {1} {2}"; //$NON-NLS-1$
68  
69  	private static final String MSG_UNTRACKED = "untracked files on {0}: {1} {2}"; //$NON-NLS-1$
70  
71  	private static final String MSG_WORKING_DIR = "WIP on {0}: {1} {2}"; //$NON-NLS-1$
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  	 * Create a command to stash changes in the working directory and index
85  	 *
86  	 * @param repo
87  	 *            a {@link org.eclipse.jgit.lib.Repository} object.
88  	 */
89  	public StashCreateCommand(Repository repo) {
90  		super(repo);
91  		person = new PersonIdent(repo);
92  	}
93  
94  	/**
95  	 * Set the message used when committing index changes
96  	 * <p>
97  	 * The message will be formatted with the current branch, abbreviated commit
98  	 * id, and short commit message when used.
99  	 *
100 	 * @param message
101 	 *            the stash message
102 	 * @return {@code this}
103 	 */
104 	public StashCreateCommand setIndexMessage(String message) {
105 		indexMessage = message;
106 		return this;
107 	}
108 
109 	/**
110 	 * Set the message used when committing working directory changes
111 	 * <p>
112 	 * The message will be formatted with the current branch, abbreviated commit
113 	 * id, and short commit message when used.
114 	 *
115 	 * @param message
116 	 *            the working directory message
117 	 * @return {@code this}
118 	 */
119 	public StashCreateCommand setWorkingDirectoryMessage(String message) {
120 		workingDirectoryMessage = message;
121 		return this;
122 	}
123 
124 	/**
125 	 * Set the person to use as the author and committer in the commits made
126 	 *
127 	 * @param person
128 	 *            the {@link org.eclipse.jgit.lib.PersonIdent} of the person who
129 	 *            creates the stash.
130 	 * @return {@code this}
131 	 */
132 	public StashCreateCommand setPerson(PersonIdent person) {
133 		this.person = person;
134 		return this;
135 	}
136 
137 	/**
138 	 * Set the reference to update with the stashed commit id If null, no
139 	 * reference is updated
140 	 * <p>
141 	 * This value defaults to {@link org.eclipse.jgit.lib.Constants#R_STASH}
142 	 *
143 	 * @param ref
144 	 *            the name of the {@code Ref} to update
145 	 * @return {@code this}
146 	 */
147 	public StashCreateCommand setRef(String ref) {
148 		this.ref = ref;
149 		return this;
150 	}
151 
152 	/**
153 	 * Whether to include untracked files in the stash.
154 	 *
155 	 * @param includeUntracked
156 	 *            whether to include untracked files in the stash
157 	 * @return {@code this}
158 	 * @since 3.4
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 	 * {@inheritDoc}
212 	 * <p>
213 	 * Stash the contents on the working directory and index in separate commits
214 	 * and reset to the current HEAD commit.
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 				// Return null if no local changes to stash
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 				// Commit index changes
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 				// Commit untracked changes
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 				// Commit working tree changes
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 				// Remove untracked files
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 			// Hard reset to HEAD
374 			new ResetCommand(repo).setMode(ResetType.HARD).call();
375 
376 			// Return stashed commit
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 }