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