View Javadoc
1   /*
2    * Copyright (C) 2012, GitHub Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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   * Command class to stash changes in the working directory and index in a
90   * commit.
91   *
92   * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-stash.html"
93   *      >Git documentation about Stash</a>
94   * @since 2.0
95   */
96  public class StashCreateCommand extends GitCommand<RevCommit> {
97  
98  	private static final String MSG_INDEX = "index on {0}: {1} {2}"; //$NON-NLS-1$
99  
100 	private static final String MSG_UNTRACKED = "untracked files on {0}: {1} {2}"; //$NON-NLS-1$
101 
102 	private static final String MSG_WORKING_DIR = "WIP on {0}: {1} {2}"; //$NON-NLS-1$
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 	 * Create a command to stash changes in the working directory and index
116 	 *
117 	 * @param repo
118 	 *            a {@link org.eclipse.jgit.lib.Repository} object.
119 	 */
120 	public StashCreateCommand(Repository repo) {
121 		super(repo);
122 		person = new PersonIdent(repo);
123 	}
124 
125 	/**
126 	 * Set the message used when committing index changes
127 	 * <p>
128 	 * The message will be formatted with the current branch, abbreviated commit
129 	 * id, and short commit message when used.
130 	 *
131 	 * @param message
132 	 *            the stash message
133 	 * @return {@code this}
134 	 */
135 	public StashCreateCommand setIndexMessage(String message) {
136 		indexMessage = message;
137 		return this;
138 	}
139 
140 	/**
141 	 * Set the message used when committing working directory changes
142 	 * <p>
143 	 * The message will be formatted with the current branch, abbreviated commit
144 	 * id, and short commit message when used.
145 	 *
146 	 * @param message
147 	 *            the working directory message
148 	 * @return {@code this}
149 	 */
150 	public StashCreateCommand setWorkingDirectoryMessage(String message) {
151 		workingDirectoryMessage = message;
152 		return this;
153 	}
154 
155 	/**
156 	 * Set the person to use as the author and committer in the commits made
157 	 *
158 	 * @param person
159 	 *            the {@link org.eclipse.jgit.lib.PersonIdent} of the person who
160 	 *            creates the stash.
161 	 * @return {@code this}
162 	 */
163 	public StashCreateCommand setPerson(PersonIdent person) {
164 		this.person = person;
165 		return this;
166 	}
167 
168 	/**
169 	 * Set the reference to update with the stashed commit id If null, no
170 	 * reference is updated
171 	 * <p>
172 	 * This value defaults to {@link org.eclipse.jgit.lib.Constants#R_STASH}
173 	 *
174 	 * @param ref
175 	 *            the name of the {@code Ref} to update
176 	 * @return {@code this}
177 	 */
178 	public StashCreateCommand setRef(String ref) {
179 		this.ref = ref;
180 		return this;
181 	}
182 
183 	/**
184 	 * Whether to include untracked files in the stash.
185 	 *
186 	 * @param includeUntracked
187 	 *            whether to include untracked files in the stash
188 	 * @return {@code this}
189 	 * @since 3.4
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 	 * {@inheritDoc}
243 	 * <p>
244 	 * Stash the contents on the working directory and index in separate commits
245 	 * and reset to the current HEAD commit.
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 				// Return null if no local changes to stash
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 				// Commit index changes
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 				// Commit untracked changes
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 				// Commit working tree changes
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 				// Remove untracked files
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 			// Hard reset to HEAD
400 			new ResetCommand(repo).setMode(ResetType.HARD).call();
401 
402 			// Return stashed commit
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 }