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