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.internal.JGitText;
66  import org.eclipse.jgit.lib.CommitBuilder;
67  import org.eclipse.jgit.lib.Constants;
68  import org.eclipse.jgit.lib.MutableObjectId;
69  import org.eclipse.jgit.lib.ObjectId;
70  import org.eclipse.jgit.lib.ObjectInserter;
71  import org.eclipse.jgit.lib.ObjectReader;
72  import org.eclipse.jgit.lib.PersonIdent;
73  import org.eclipse.jgit.lib.Ref;
74  import org.eclipse.jgit.lib.RefUpdate;
75  import org.eclipse.jgit.lib.Repository;
76  import org.eclipse.jgit.revwalk.RevCommit;
77  import org.eclipse.jgit.revwalk.RevWalk;
78  import org.eclipse.jgit.treewalk.AbstractTreeIterator;
79  import org.eclipse.jgit.treewalk.FileTreeIterator;
80  import org.eclipse.jgit.treewalk.TreeWalk;
81  import org.eclipse.jgit.treewalk.WorkingTreeIterator;
82  import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
83  import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
84  import org.eclipse.jgit.treewalk.filter.SkipWorkTreeFilter;
85  import org.eclipse.jgit.util.FileUtils;
86  
87  /**
88   * Command class to stash changes in the working directory and index in a
89   * commit.
90   *
91   * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-stash.html"
92   *      >Git documentation about Stash</a>
93   * @since 2.0
94   */
95  public class StashCreateCommand extends GitCommand<RevCommit> {
96  
97  	private static final String MSG_INDEX = "index on {0}: {1} {2}"; //$NON-NLS-1$
98  
99  	private static final String MSG_UNTRACKED = "untracked files on {0}: {1} {2}"; //$NON-NLS-1$
100 
101 	private static final String MSG_WORKING_DIR = "WIP on {0}: {1} {2}"; //$NON-NLS-1$
102 
103 	private String indexMessage = MSG_INDEX;
104 
105 	private String workingDirectoryMessage = MSG_WORKING_DIR;
106 
107 	private String ref = Constants.R_STASH;
108 
109 	private PersonIdent person;
110 
111 	private boolean includeUntracked;
112 
113 	/**
114 	 * Create a command to stash changes in the working directory and index
115 	 *
116 	 * @param repo
117 	 */
118 	public StashCreateCommand(Repository repo) {
119 		super(repo);
120 		person = new PersonIdent(repo);
121 	}
122 
123 	/**
124 	 * Set the message used when committing index changes
125 	 * <p>
126 	 * The message will be formatted with the current branch, abbreviated commit
127 	 * id, and short commit message when used.
128 	 *
129 	 * @param message
130 	 * @return {@code this}
131 	 */
132 	public StashCreateCommand setIndexMessage(String message) {
133 		indexMessage = message;
134 		return this;
135 	}
136 
137 	/**
138 	 * Set the message used when committing working directory changes
139 	 * <p>
140 	 * The message will be formatted with the current branch, abbreviated commit
141 	 * id, and short commit message when used.
142 	 *
143 	 * @param message
144 	 * @return {@code this}
145 	 */
146 	public StashCreateCommand setWorkingDirectoryMessage(String message) {
147 		workingDirectoryMessage = message;
148 		return this;
149 	}
150 
151 	/**
152 	 * Set the person to use as the author and committer in the commits made
153 	 *
154 	 * @param person
155 	 * @return {@code this}
156 	 */
157 	public StashCreateCommand setPerson(PersonIdent person) {
158 		this.person = person;
159 		return this;
160 	}
161 
162 	/**
163 	 * Set the reference to update with the stashed commit id
164 	 * If null, no reference is updated
165 	 * <p>
166 	 * This value defaults to {@link Constants#R_STASH}
167 	 *
168 	 * @param ref
169 	 * @return {@code this}
170 	 */
171 	public StashCreateCommand setRef(String ref) {
172 		this.ref = ref;
173 		return this;
174 	}
175 
176 	/**
177 	 * Whether to include untracked files in the stash.
178 	 *
179 	 * @param includeUntracked
180 	 * @return {@code this}
181 	 * @since 3.4
182 	 */
183 	public StashCreateCommand setIncludeUntracked(boolean includeUntracked) {
184 		this.includeUntracked = includeUntracked;
185 		return this;
186 	}
187 
188 	private RevCommit parseCommit(final ObjectReader reader,
189 			final ObjectId headId) throws IOException {
190 		try (final RevWalk walk = new RevWalk(reader)) {
191 			return walk.parseCommit(headId);
192 		}
193 	}
194 
195 	private CommitBuilder createBuilder() {
196 		CommitBuilder builder = new CommitBuilder();
197 		PersonIdent author = person;
198 		if (author == null)
199 			author = new PersonIdent(repo);
200 		builder.setAuthor(author);
201 		builder.setCommitter(author);
202 		return builder;
203 	}
204 
205 	private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent,
206 			String refLogMessage) throws IOException {
207 		if (ref == null)
208 			return;
209 		Ref currentRef = repo.getRef(ref);
210 		RefUpdate refUpdate = repo.updateRef(ref);
211 		refUpdate.setNewObjectId(commitId);
212 		refUpdate.setRefLogIdent(refLogIdent);
213 		refUpdate.setRefLogMessage(refLogMessage, false);
214 		if (currentRef != null)
215 			refUpdate.setExpectedOldObjectId(currentRef.getObjectId());
216 		else
217 			refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
218 		refUpdate.forceUpdate();
219 	}
220 
221 	private Ref getHead() throws GitAPIException {
222 		try {
223 			Ref head = repo.getRef(Constants.HEAD);
224 			if (head == null || head.getObjectId() == null)
225 				throw new NoHeadException(JGitText.get().headRequiredToStash);
226 			return head;
227 		} catch (IOException e) {
228 			throw new JGitInternalException(JGitText.get().stashFailed, e);
229 		}
230 	}
231 
232 	/**
233 	 * Stash the contents on the working directory and index in separate commits
234 	 * and reset to the current HEAD commit.
235 	 *
236 	 * @return stashed commit or null if no changes to stash
237 	 * @throws GitAPIException
238 	 */
239 	public RevCommit call() throws GitAPIException {
240 		checkCallable();
241 
242 		Ref head = getHead();
243 		try (ObjectReader reader = repo.newObjectReader()) {
244 			RevCommit headCommit = parseCommit(reader, head.getObjectId());
245 			DirCache cache = repo.lockDirCache();
246 			ObjectId commitId;
247 			try (ObjectInserter inserter = repo.newObjectInserter();
248 					TreeWalk treeWalk = new TreeWalk(reader)) {
249 
250 				treeWalk.setRecursive(true);
251 				treeWalk.addTree(headCommit.getTree());
252 				treeWalk.addTree(new DirCacheIterator(cache));
253 				treeWalk.addTree(new FileTreeIterator(repo));
254 				treeWalk.setFilter(AndTreeFilter.create(new SkipWorkTreeFilter(
255 						1), new IndexDiffFilter(1, 2)));
256 
257 				// Return null if no local changes to stash
258 				if (!treeWalk.next())
259 					return null;
260 
261 				MutableObjectId id = new MutableObjectId();
262 				List<PathEdit> wtEdits = new ArrayList<PathEdit>();
263 				List<String> wtDeletes = new ArrayList<String>();
264 				List<DirCacheEntry> untracked = new ArrayList<DirCacheEntry>();
265 				boolean hasChanges = false;
266 				do {
267 					AbstractTreeIterator headIter = treeWalk.getTree(0,
268 							AbstractTreeIterator.class);
269 					DirCacheIterator indexIter = treeWalk.getTree(1,
270 							DirCacheIterator.class);
271 					WorkingTreeIterator wtIter = treeWalk.getTree(2,
272 							WorkingTreeIterator.class);
273 					if (indexIter != null
274 							&& !indexIter.getDirCacheEntry().isMerged())
275 						throw new UnmergedPathsException(
276 								new UnmergedPathException(
277 										indexIter.getDirCacheEntry()));
278 					if (wtIter != null) {
279 						if (indexIter == null && headIter == null
280 								&& !includeUntracked)
281 							continue;
282 						hasChanges = true;
283 						if (indexIter != null && wtIter.idEqual(indexIter))
284 							continue;
285 						if (headIter != null && wtIter.idEqual(headIter))
286 							continue;
287 						treeWalk.getObjectId(id, 0);
288 						final DirCacheEntry entry = new DirCacheEntry(
289 								treeWalk.getRawPath());
290 						entry.setLength(wtIter.getEntryLength());
291 						entry.setLastModified(wtIter.getEntryLastModified());
292 						entry.setFileMode(wtIter.getEntryFileMode());
293 						long contentLength = wtIter.getEntryContentLength();
294 						InputStream in = wtIter.openEntryStream();
295 						try {
296 							entry.setObjectId(inserter.insert(
297 									Constants.OBJ_BLOB, contentLength, in));
298 						} finally {
299 							in.close();
300 						}
301 
302 						if (indexIter == null && headIter == null)
303 							untracked.add(entry);
304 						else
305 							wtEdits.add(new PathEdit(entry) {
306 								public void apply(DirCacheEntry ent) {
307 									ent.copyMetaData(entry);
308 								}
309 							});
310 					}
311 					hasChanges = true;
312 					if (wtIter == null && headIter != null)
313 						wtDeletes.add(treeWalk.getPathString());
314 				} while (treeWalk.next());
315 
316 				if (!hasChanges)
317 					return null;
318 
319 				String branch = Repository.shortenRefName(head.getTarget()
320 						.getName());
321 
322 				// Commit index changes
323 				CommitBuilder builder = createBuilder();
324 				builder.setParentId(headCommit);
325 				builder.setTreeId(cache.writeTree(inserter));
326 				builder.setMessage(MessageFormat.format(indexMessage, branch,
327 						headCommit.abbreviate(7).name(),
328 						headCommit.getShortMessage()));
329 				ObjectId indexCommit = inserter.insert(builder);
330 
331 				// Commit untracked changes
332 				ObjectId untrackedCommit = null;
333 				if (!untracked.isEmpty()) {
334 					DirCache untrackedDirCache = DirCache.newInCore();
335 					DirCacheBuilder untrackedBuilder = untrackedDirCache
336 							.builder();
337 					for (DirCacheEntry entry : untracked)
338 						untrackedBuilder.add(entry);
339 					untrackedBuilder.finish();
340 
341 					builder.setParentIds(new ObjectId[0]);
342 					builder.setTreeId(untrackedDirCache.writeTree(inserter));
343 					builder.setMessage(MessageFormat.format(MSG_UNTRACKED,
344 							branch, headCommit.abbreviate(7).name(),
345 							headCommit.getShortMessage()));
346 					untrackedCommit = inserter.insert(builder);
347 				}
348 
349 				// Commit working tree changes
350 				if (!wtEdits.isEmpty() || !wtDeletes.isEmpty()) {
351 					DirCacheEditor editor = cache.editor();
352 					for (PathEdit edit : wtEdits)
353 						editor.add(edit);
354 					for (String path : wtDeletes)
355 						editor.add(new DeletePath(path));
356 					editor.finish();
357 				}
358 				builder.setParentId(headCommit);
359 				builder.addParentId(indexCommit);
360 				if (untrackedCommit != null)
361 					builder.addParentId(untrackedCommit);
362 				builder.setMessage(MessageFormat.format(
363 						workingDirectoryMessage, branch,
364 						headCommit.abbreviate(7).name(),
365 						headCommit.getShortMessage()));
366 				builder.setTreeId(cache.writeTree(inserter));
367 				commitId = inserter.insert(builder);
368 				inserter.flush();
369 
370 				updateStashRef(commitId, builder.getAuthor(),
371 						builder.getMessage());
372 
373 				// Remove untracked files
374 				if (includeUntracked) {
375 					for (DirCacheEntry entry : untracked) {
376 						File file = new File(repo.getWorkTree(),
377 								entry.getPathString());
378 						FileUtils.delete(file);
379 					}
380 				}
381 
382 			} finally {
383 				cache.unlock();
384 			}
385 
386 			// Hard reset to HEAD
387 			new ResetCommand(repo).setMode(ResetType.HARD).call();
388 
389 			// Return stashed commit
390 			return parseCommit(reader, commitId);
391 		} catch (IOException e) {
392 			throw new JGitInternalException(JGitText.get().stashFailed, e);
393 		}
394 	}
395 }