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 static org.junit.Assert.assertEquals;
46  import static org.junit.Assert.assertFalse;
47  import static org.junit.Assert.assertNotNull;
48  import static org.junit.Assert.assertNull;
49  import static org.junit.Assert.assertTrue;
50  
51  import java.io.File;
52  import java.io.IOException;
53  import java.util.List;
54  
55  import org.eclipse.jgit.api.errors.UnmergedPathsException;
56  import org.eclipse.jgit.diff.DiffEntry;
57  import org.eclipse.jgit.junit.RepositoryTestCase;
58  import org.eclipse.jgit.lib.Constants;
59  import org.eclipse.jgit.lib.ObjectId;
60  import org.eclipse.jgit.lib.PersonIdent;
61  import org.eclipse.jgit.lib.Ref;
62  import org.eclipse.jgit.lib.ReflogEntry;
63  import org.eclipse.jgit.lib.ReflogReader;
64  import org.eclipse.jgit.revwalk.RevCommit;
65  import org.eclipse.jgit.revwalk.RevWalk;
66  import org.eclipse.jgit.treewalk.TreeWalk;
67  import org.eclipse.jgit.treewalk.filter.TreeFilter;
68  import org.eclipse.jgit.util.FileUtils;
69  import org.junit.Before;
70  import org.junit.Test;
71  
72  /**
73   * Unit tests of {@link StashCreateCommand}
74   */
75  public class StashCreateCommandTest extends RepositoryTestCase {
76  
77  	private RevCommit head;
78  
79  	private Git git;
80  
81  	private File committedFile;
82  
83  	private File untrackedFile;
84  
85  	@Before
86  	public void setUp() throws Exception {
87  		super.setUp();
88  		git = Git.wrap(db);
89  		committedFile = writeTrashFile("file.txt", "content");
90  		git.add().addFilepattern("file.txt").call();
91  		head = git.commit().setMessage("add file").call();
92  		assertNotNull(head);
93  		untrackedFile = writeTrashFile("untracked.txt", "content");
94  	}
95  
96  	private void validateStashedCommit(final RevCommit commit)
97  			throws IOException {
98  		validateStashedCommit(commit, 2);
99  	}
100 
101 	/**
102 	 * Core validation to be performed on all stashed commits
103 	 *
104 	 * @param commit
105 	 * @param parentCount
106 	 *            number of parent commits required
107 	 * @throws IOException
108 	 */
109 	private void validateStashedCommit(final RevCommit commit,
110 			int parentCount)
111 			throws IOException {
112 		assertNotNull(commit);
113 		Ref stashRef = db.exactRef(Constants.R_STASH);
114 		assertNotNull(stashRef);
115 		assertEquals(commit, stashRef.getObjectId());
116 		assertNotNull(commit.getAuthorIdent());
117 		assertEquals(commit.getAuthorIdent(), commit.getCommitterIdent());
118 		assertEquals(parentCount, commit.getParentCount());
119 
120 		// Load parents
121 		try (RevWalk walk = new RevWalk(db)) {
122 			for (RevCommit parent : commit.getParents())
123 				walk.parseBody(parent);
124 		}
125 
126 		assertEquals(1, commit.getParent(1).getParentCount());
127 		assertEquals(head, commit.getParent(1).getParent(0));
128 		assertFalse("Head tree matches stashed commit tree", commit.getTree()
129 				.equals(head.getTree()));
130 		assertEquals(head, commit.getParent(0));
131 		assertFalse(commit.getFullMessage().equals(
132 				commit.getParent(1).getFullMessage()));
133 	}
134 
135 	private TreeWalk createTreeWalk() {
136 		TreeWalk walk = new TreeWalk(db);
137 		walk.setRecursive(true);
138 		walk.setFilter(TreeFilter.ANY_DIFF);
139 		return walk;
140 	}
141 
142 	private List<DiffEntry> diffWorkingAgainstHead(final RevCommit commit)
143 			throws IOException {
144 		try (TreeWalk walk = createTreeWalk()) {
145 			walk.addTree(commit.getParent(0).getTree());
146 			walk.addTree(commit.getTree());
147 			return DiffEntry.scan(walk);
148 		}
149 	}
150 
151 	private List<DiffEntry> diffIndexAgainstHead(final RevCommit commit)
152 			throws IOException {
153 		try (TreeWalk walk = createTreeWalk()) {
154 			walk.addTree(commit.getParent(0).getTree());
155 			walk.addTree(commit.getParent(1).getTree());
156 			return DiffEntry.scan(walk);
157 		}
158 	}
159 
160 	private List<DiffEntry> diffIndexAgainstWorking(final RevCommit commit)
161 			throws IOException {
162 		try (TreeWalk walk = createTreeWalk()) {
163 			walk.addTree(commit.getParent(1).getTree());
164 			walk.addTree(commit.getTree());
165 			return DiffEntry.scan(walk);
166 		}
167 	}
168 
169 	@Test
170 	public void noLocalChanges() throws Exception {
171 		assertNull(git.stashCreate().call());
172 	}
173 
174 	@Test
175 	public void workingDirectoryDelete() throws Exception {
176 		deleteTrashFile("file.txt");
177 		RevCommit stashed = git.stashCreate().call();
178 		assertNotNull(stashed);
179 		assertEquals("content", read(committedFile));
180 		validateStashedCommit(stashed);
181 
182 		assertEquals(head.getTree(), stashed.getParent(1).getTree());
183 
184 		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
185 		assertEquals(1, diffs.size());
186 		assertEquals(DiffEntry.ChangeType.DELETE, diffs.get(0).getChangeType());
187 		assertEquals("file.txt", diffs.get(0).getOldPath());
188 	}
189 
190 	@Test
191 	public void indexAdd() throws Exception {
192 		File addedFile = writeTrashFile("file2.txt", "content2");
193 		git.add().addFilepattern("file2.txt").call();
194 
195 		RevCommit stashed = Git.wrap(db).stashCreate().call();
196 		assertNotNull(stashed);
197 		assertFalse(addedFile.exists());
198 		validateStashedCommit(stashed);
199 
200 		assertEquals(stashed.getTree(), stashed.getParent(1).getTree());
201 
202 		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
203 		assertEquals(1, diffs.size());
204 		assertEquals(DiffEntry.ChangeType.ADD, diffs.get(0).getChangeType());
205 		assertEquals("file2.txt", diffs.get(0).getNewPath());
206 	}
207 
208 	@Test
209 	public void newFileInIndexThenModifiedInWorkTree() throws Exception {
210 		writeTrashFile("file", "content");
211 		git.add().addFilepattern("file").call();
212 		writeTrashFile("file", "content2");
213 		RevCommit stashedWorkTree = Git.wrap(db).stashCreate().call();
214 		validateStashedCommit(stashedWorkTree);
215 		try (RevWalk walk = new RevWalk(db)) {
216 			RevCommit stashedIndex = stashedWorkTree.getParent(1);
217 			walk.parseBody(stashedIndex);
218 			walk.parseBody(stashedIndex.getTree());
219 			walk.parseBody(stashedIndex.getParent(0));
220 		}
221 		List<DiffEntry> workTreeStashAgainstWorkTree = diffWorkingAgainstHead(stashedWorkTree);
222 		assertEquals(1, workTreeStashAgainstWorkTree.size());
223 		List<DiffEntry> workIndexAgainstWorkTree = diffIndexAgainstHead(stashedWorkTree);
224 		assertEquals(1, workIndexAgainstWorkTree.size());
225 		List<DiffEntry> indexStashAgainstWorkTree = diffIndexAgainstWorking(stashedWorkTree);
226 		assertEquals(1, indexStashAgainstWorkTree.size());
227 	}
228 
229 	@Test
230 	public void indexDelete() throws Exception {
231 		git.rm().addFilepattern("file.txt").call();
232 
233 		RevCommit stashed = Git.wrap(db).stashCreate().call();
234 		assertNotNull(stashed);
235 		assertEquals("content", read(committedFile));
236 		validateStashedCommit(stashed);
237 
238 		assertEquals(stashed.getTree(), stashed.getParent(1).getTree());
239 
240 		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
241 		assertEquals(1, diffs.size());
242 		assertEquals(DiffEntry.ChangeType.DELETE, diffs.get(0).getChangeType());
243 		assertEquals("file.txt", diffs.get(0).getOldPath());
244 	}
245 
246 	@Test
247 	public void workingDirectoryModify() throws Exception {
248 		writeTrashFile("file.txt", "content2");
249 
250 		RevCommit stashed = Git.wrap(db).stashCreate().call();
251 		assertNotNull(stashed);
252 		assertEquals("content", read(committedFile));
253 		validateStashedCommit(stashed);
254 
255 		assertEquals(head.getTree(), stashed.getParent(1).getTree());
256 
257 		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
258 		assertEquals(1, diffs.size());
259 		assertEquals(DiffEntry.ChangeType.MODIFY, diffs.get(0).getChangeType());
260 		assertEquals("file.txt", diffs.get(0).getNewPath());
261 	}
262 
263 	@Test
264 	public void workingDirectoryModifyInSubfolder() throws Exception {
265 		String path = "d1/d2/f.txt";
266 		File subfolderFile = writeTrashFile(path, "content");
267 		git.add().addFilepattern(path).call();
268 		head = git.commit().setMessage("add file").call();
269 
270 		writeTrashFile(path, "content2");
271 
272 		RevCommit stashed = Git.wrap(db).stashCreate().call();
273 		assertNotNull(stashed);
274 		assertEquals("content", read(subfolderFile));
275 		validateStashedCommit(stashed);
276 
277 		assertEquals(head.getTree(), stashed.getParent(1).getTree());
278 
279 		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
280 		assertEquals(1, diffs.size());
281 		assertEquals(DiffEntry.ChangeType.MODIFY, diffs.get(0).getChangeType());
282 		assertEquals(path, diffs.get(0).getNewPath());
283 	}
284 
285 	@Test
286 	public void workingDirectoryModifyIndexChanged() throws Exception {
287 		writeTrashFile("file.txt", "content2");
288 		git.add().addFilepattern("file.txt").call();
289 		writeTrashFile("file.txt", "content3");
290 
291 		RevCommit stashed = Git.wrap(db).stashCreate().call();
292 		assertNotNull(stashed);
293 		assertEquals("content", read(committedFile));
294 		validateStashedCommit(stashed);
295 
296 		assertFalse(stashed.getTree().equals(stashed.getParent(1).getTree()));
297 
298 		List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
299 		assertEquals(1, workingDiffs.size());
300 		assertEquals(DiffEntry.ChangeType.MODIFY, workingDiffs.get(0)
301 				.getChangeType());
302 		assertEquals("file.txt", workingDiffs.get(0).getNewPath());
303 
304 		List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
305 		assertEquals(1, indexDiffs.size());
306 		assertEquals(DiffEntry.ChangeType.MODIFY, indexDiffs.get(0)
307 				.getChangeType());
308 		assertEquals("file.txt", indexDiffs.get(0).getNewPath());
309 
310 		assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
311 				.getOldId());
312 		assertFalse(workingDiffs.get(0).getNewId()
313 				.equals(indexDiffs.get(0).getNewId()));
314 	}
315 
316 	@Test
317 	public void workingDirectoryCleanIndexModify() throws Exception {
318 		writeTrashFile("file.txt", "content2");
319 		git.add().addFilepattern("file.txt").call();
320 		writeTrashFile("file.txt", "content");
321 
322 		RevCommit stashed = Git.wrap(db).stashCreate().call();
323 		assertNotNull(stashed);
324 		assertEquals("content", read(committedFile));
325 		validateStashedCommit(stashed);
326 
327 		assertEquals(stashed.getParent(1).getTree(), stashed.getTree());
328 
329 		List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
330 		assertEquals(1, workingDiffs.size());
331 		assertEquals(DiffEntry.ChangeType.MODIFY, workingDiffs.get(0)
332 				.getChangeType());
333 		assertEquals("file.txt", workingDiffs.get(0).getNewPath());
334 
335 		List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
336 		assertEquals(1, indexDiffs.size());
337 		assertEquals(DiffEntry.ChangeType.MODIFY, indexDiffs.get(0)
338 				.getChangeType());
339 		assertEquals("file.txt", indexDiffs.get(0).getNewPath());
340 
341 		assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
342 				.getOldId());
343 		assertTrue(workingDiffs.get(0).getNewId()
344 				.equals(indexDiffs.get(0).getNewId()));
345 	}
346 
347 	@Test
348 	public void workingDirectoryDeleteIndexAdd() throws Exception {
349 		String path = "file2.txt";
350 		File added = writeTrashFile(path, "content2");
351 		assertTrue(added.exists());
352 		git.add().addFilepattern(path).call();
353 		FileUtils.delete(added);
354 		assertFalse(added.exists());
355 
356 		RevCommit stashed = Git.wrap(db).stashCreate().call();
357 		assertNotNull(stashed);
358 		assertFalse(added.exists());
359 
360 		validateStashedCommit(stashed);
361 
362 		assertEquals(stashed.getParent(1).getTree(), stashed.getTree());
363 
364 		List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
365 		assertEquals(1, workingDiffs.size());
366 		assertEquals(DiffEntry.ChangeType.ADD, workingDiffs.get(0)
367 				.getChangeType());
368 		assertEquals(path, workingDiffs.get(0).getNewPath());
369 
370 		List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
371 		assertEquals(1, indexDiffs.size());
372 		assertEquals(DiffEntry.ChangeType.ADD, indexDiffs.get(0)
373 				.getChangeType());
374 		assertEquals(path, indexDiffs.get(0).getNewPath());
375 
376 		assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
377 				.getOldId());
378 		assertTrue(workingDiffs.get(0).getNewId()
379 				.equals(indexDiffs.get(0).getNewId()));
380 	}
381 
382 	@Test
383 	public void workingDirectoryDeleteIndexEdit() throws Exception {
384 		File edited = writeTrashFile("file.txt", "content2");
385 		git.add().addFilepattern("file.txt").call();
386 		FileUtils.delete(edited);
387 		assertFalse(edited.exists());
388 
389 		RevCommit stashed = Git.wrap(db).stashCreate().call();
390 		assertNotNull(stashed);
391 		assertEquals("content", read(committedFile));
392 		validateStashedCommit(stashed);
393 
394 		assertFalse(stashed.getTree().equals(stashed.getParent(1).getTree()));
395 
396 		List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
397 		assertEquals(1, workingDiffs.size());
398 		assertEquals(DiffEntry.ChangeType.DELETE, workingDiffs.get(0)
399 				.getChangeType());
400 		assertEquals("file.txt", workingDiffs.get(0).getOldPath());
401 
402 		List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
403 		assertEquals(1, indexDiffs.size());
404 		assertEquals(DiffEntry.ChangeType.MODIFY, indexDiffs.get(0)
405 				.getChangeType());
406 		assertEquals("file.txt", indexDiffs.get(0).getNewPath());
407 
408 		assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
409 				.getOldId());
410 		assertFalse(workingDiffs.get(0).getNewId()
411 				.equals(indexDiffs.get(0).getNewId()));
412 	}
413 
414 	@Test
415 	public void multipleEdits() throws Exception {
416 		git.rm().addFilepattern("file.txt").call();
417 		File addedFile = writeTrashFile("file2.txt", "content2");
418 		git.add().addFilepattern("file2.txt").call();
419 
420 		RevCommit stashed = Git.wrap(db).stashCreate().call();
421 		assertNotNull(stashed);
422 		assertFalse(addedFile.exists());
423 		validateStashedCommit(stashed);
424 
425 		assertEquals(stashed.getTree(), stashed.getParent(1).getTree());
426 
427 		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
428 		assertEquals(2, diffs.size());
429 		assertEquals(DiffEntry.ChangeType.DELETE, diffs.get(0).getChangeType());
430 		assertEquals("file.txt", diffs.get(0).getOldPath());
431 		assertEquals(DiffEntry.ChangeType.ADD, diffs.get(1).getChangeType());
432 		assertEquals("file2.txt", diffs.get(1).getNewPath());
433 	}
434 
435 	@Test
436 	public void refLogIncludesCommitMessage() throws Exception {
437 		PersonIdent who = new PersonIdent("user", "user@email.com");
438 		deleteTrashFile("file.txt");
439 		RevCommit stashed = git.stashCreate().setPerson(who).call();
440 		assertNotNull(stashed);
441 		assertEquals("content", read(committedFile));
442 		validateStashedCommit(stashed);
443 
444 		ReflogReader reader = git.getRepository().getReflogReader(
445 				Constants.R_STASH);
446 		ReflogEntry entry = reader.getLastEntry();
447 		assertNotNull(entry);
448 		assertEquals(ObjectId.zeroId(), entry.getOldId());
449 		assertEquals(stashed, entry.getNewId());
450 		assertEquals(who, entry.getWho());
451 		assertEquals(stashed.getFullMessage(), entry.getComment());
452 	}
453 
454 	@Test(expected = UnmergedPathsException.class)
455 	public void unmergedPathsShouldCauseException() throws Exception {
456 		commitFile("file.txt", "master", "base");
457 		RevCommit side = commitFile("file.txt", "side", "side");
458 		commitFile("file.txt", "master", "master");
459 		git.merge().include(side).call();
460 
461 		git.stashCreate().call();
462 	}
463 
464 	@Test
465 	public void untrackedFileIncluded() throws Exception {
466 		String trackedPath = "tracked.txt";
467 		writeTrashFile(trackedPath, "content2");
468 		git.add().addFilepattern(trackedPath).call();
469 
470 		RevCommit stashed = git.stashCreate()
471 				.setIncludeUntracked(true).call();
472 		validateStashedCommit(stashed, 3);
473 
474 		assertEquals(
475 				"Expected commits for workingDir,stashedIndex and untrackedFiles.",
476 				3, stashed.getParentCount());
477 		assertFalse("untracked file should be deleted.", untrackedFile.exists());
478 	}
479 
480 	@Test
481 	public void untrackedFileNotIncluded() throws Exception {
482 		String trackedPath = "tracked.txt";
483 		// at least one modification needed
484 		writeTrashFile(trackedPath, "content2");
485 		git.add().addFilepattern(trackedPath).call();
486 
487 		RevCommit stashed = git.stashCreate().call();
488 		validateStashedCommit(stashed);
489 
490 		assertTrue("untracked file should be left untouched.",
491 				untrackedFile.exists());
492 		assertEquals("content", read(untrackedFile));
493 	}
494 }