View Javadoc
1   /*
2    * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com>
3    * Copyright (C) 2011, Matthias Sohn <matthias.sohn@sap.com> and others
4    *
5    * This program and the accompanying materials are made available under the
6    * terms of the Eclipse Distribution License v. 1.0 which is available at
7    * https://www.eclipse.org/org/documents/edl-v10.php.
8    *
9    * SPDX-License-Identifier: BSD-3-Clause
10   */
11  package org.eclipse.jgit.api;
12  
13  import static java.time.Instant.EPOCH;
14  import static org.eclipse.jgit.lib.Constants.MASTER;
15  import static org.eclipse.jgit.lib.Constants.R_HEADS;
16  import static org.hamcrest.CoreMatchers.is;
17  import static org.hamcrest.MatcherAssert.assertThat;
18  import static org.junit.Assert.assertEquals;
19  import static org.junit.Assert.assertFalse;
20  import static org.junit.Assert.assertNotNull;
21  import static org.junit.Assert.assertNull;
22  import static org.junit.Assert.assertSame;
23  import static org.junit.Assert.assertTrue;
24  import static org.junit.Assert.fail;
25  
26  import java.io.File;
27  import java.io.FileInputStream;
28  import java.io.IOException;
29  import java.net.MalformedURLException;
30  import java.net.URISyntaxException;
31  import java.nio.file.Files;
32  import java.nio.file.attribute.FileTime;
33  import java.time.Instant;
34  
35  import org.eclipse.jgit.api.CheckoutResult.Status;
36  import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
37  import org.eclipse.jgit.api.errors.CheckoutConflictException;
38  import org.eclipse.jgit.api.errors.GitAPIException;
39  import org.eclipse.jgit.api.errors.InvalidRefNameException;
40  import org.eclipse.jgit.api.errors.InvalidRemoteException;
41  import org.eclipse.jgit.api.errors.JGitInternalException;
42  import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
43  import org.eclipse.jgit.api.errors.RefNotFoundException;
44  import org.eclipse.jgit.api.errors.TransportException;
45  import org.eclipse.jgit.dircache.DirCache;
46  import org.eclipse.jgit.dircache.DirCacheEntry;
47  import org.eclipse.jgit.junit.JGitTestUtil;
48  import org.eclipse.jgit.junit.RepositoryTestCase;
49  import org.eclipse.jgit.junit.time.TimeUtil;
50  import org.eclipse.jgit.lfs.BuiltinLFS;
51  import org.eclipse.jgit.lib.ConfigConstants;
52  import org.eclipse.jgit.lib.Constants;
53  import org.eclipse.jgit.lib.Ref;
54  import org.eclipse.jgit.lib.RefUpdate;
55  import org.eclipse.jgit.lib.Repository;
56  import org.eclipse.jgit.lib.Sets;
57  import org.eclipse.jgit.lib.StoredConfig;
58  import org.eclipse.jgit.revwalk.RevCommit;
59  import org.eclipse.jgit.storage.file.FileBasedConfig;
60  import org.eclipse.jgit.transport.RemoteConfig;
61  import org.eclipse.jgit.transport.URIish;
62  import org.eclipse.jgit.util.FS;
63  import org.eclipse.jgit.util.FileUtils;
64  import org.eclipse.jgit.util.SystemReader;
65  import org.junit.Before;
66  import org.junit.Test;
67  
68  public class CheckoutCommandTest extends RepositoryTestCase {
69  	private Git git;
70  
71  	RevCommit initialCommit;
72  
73  	RevCommit secondCommit;
74  
75  	@Override
76  	@Before
77  	public void setUp() throws Exception {
78  		BuiltinLFS.register();
79  		super.setUp();
80  		git = new Git(db);
81  		// commit something
82  		writeTrashFile("Test.txt", "Hello world");
83  		git.add().addFilepattern("Test.txt").call();
84  		initialCommit = git.commit().setMessage("Initial commit").call();
85  
86  		// create a test branch and switch to it
87  		git.branchCreate().setName("test").call();
88  		RefUpdate rup = db.updateRef(Constants.HEAD);
89  		rup.link("refs/heads/test");
90  
91  		// commit something on the test branch
92  		writeTrashFile("Test.txt", "Some change");
93  		git.add().addFilepattern("Test.txt").call();
94  		secondCommit = git.commit().setMessage("Second commit").call();
95  	}
96  
97  	@Test
98  	public void testSimpleCheckout() throws Exception {
99  		git.checkout().setName("test").call();
100 	}
101 
102 	@Test
103 	public void testCheckout() throws Exception {
104 		git.checkout().setName("test").call();
105 		assertEquals("[Test.txt, mode:100644, content:Some change]",
106 				indexState(CONTENT));
107 		Ref result = git.checkout().setName("master").call();
108 		assertEquals("[Test.txt, mode:100644, content:Hello world]",
109 				indexState(CONTENT));
110 		assertEquals("refs/heads/master", result.getName());
111 		assertEquals("refs/heads/master", git.getRepository().getFullBranch());
112 	}
113 
114 	@Test
115 	public void testCheckoutForced() throws Exception {
116 		writeTrashFile("Test.txt", "Garbage");
117 		try {
118 			git.checkout().setName("master").call().getObjectId();
119 			fail("Expected CheckoutConflictException didn't occur");
120 		} catch (CheckoutConflictException e) {
121 			// Expected
122 		}
123 		assertEquals(initialCommit.getId(), git.checkout().setName("master")
124 				.setForced(true).call().getObjectId());
125 	}
126 
127 	@Test
128 	public void testCheckoutForced_deleteFileAndRestore() throws Exception {
129 		File testFile = new File(db.getWorkTree(), "Test.txt");
130 		assertTrue(testFile.exists());
131 
132 		assertEquals("test", git.getRepository().getBranch());
133 		FileUtils.delete(testFile);
134 		assertFalse(testFile.exists());
135 		// Switch from "test" to "master".
136 		assertEquals(initialCommit.getId(), git.checkout().setName("master")
137 				.setForced(true).call().getObjectId());
138 		assertTrue(testFile.exists());
139 
140 		assertEquals("master", git.getRepository().getBranch());
141 		FileUtils.delete(testFile);
142 		assertFalse(testFile.exists());
143 		// Stay in current branch.
144 		assertEquals(initialCommit.getId(), git.checkout().setName("master")
145 				.setForced(true).call().getObjectId());
146 		assertTrue(testFile.exists());
147 	}
148 
149 	@Test
150 	public void testCheckoutForcedNoChangeNotInIndex() throws Exception {
151 		git.checkout().setCreateBranch(true).setName("test2").call();
152 		File f = writeTrashFile("NewFile.txt", "New file");
153 		git.add().addFilepattern("NewFile.txt").call();
154 		git.commit().setMessage("New file created").call();
155 		git.checkout().setName("test").call();
156 		assertFalse("NewFile.txt should not exist", f.exists());
157 		writeTrashFile("NewFile.txt", "New file");
158 		git.add().addFilepattern("NewFile.txt").call();
159 		git.commit().setMessage("New file created again with same content")
160 				.call();
161 		// Now remove the file from the index only. So it exists in both
162 		// commits, and in the working tree, but not in the index.
163 		git.rm().addFilepattern("NewFile.txt").setCached(true).call();
164 		assertTrue("NewFile.txt should exist", f.isFile());
165 		git.checkout().setForced(true).setName("test2").call();
166 		assertTrue("NewFile.txt should exist", f.isFile());
167 		assertEquals(Constants.R_HEADS + "test2", git.getRepository()
168 				.exactRef(Constants.HEAD).getTarget().getName());
169 		assertTrue("Force checkout should have undone git rm --cached",
170 				git.status().call().isClean());
171 	}
172 
173 	@Test
174 	public void testCheckoutNoChangeNotInIndex() throws Exception {
175 		git.checkout().setCreateBranch(true).setName("test2").call();
176 		File f = writeTrashFile("NewFile.txt", "New file");
177 		git.add().addFilepattern("NewFile.txt").call();
178 		git.commit().setMessage("New file created").call();
179 		git.checkout().setName("test").call();
180 		assertFalse("NewFile.txt should not exist", f.exists());
181 		writeTrashFile("NewFile.txt", "New file");
182 		git.add().addFilepattern("NewFile.txt").call();
183 		git.commit().setMessage("New file created again with same content")
184 				.call();
185 		// Now remove the file from the index only. So it exists in both
186 		// commits, and in the working tree, but not in the index.
187 		git.rm().addFilepattern("NewFile.txt").setCached(true).call();
188 		assertTrue("NewFile.txt should exist", f.isFile());
189 		git.checkout().setName("test2").call();
190 		assertTrue("NewFile.txt should exist", f.isFile());
191 		assertEquals(Constants.R_HEADS + "test2", git.getRepository()
192 				.exactRef(Constants.HEAD).getTarget().getName());
193 		org.eclipse.jgit.api.Status status = git.status().call();
194 		assertEquals("[NewFile.txt]", status.getRemoved().toString());
195 		assertEquals("[NewFile.txt]", status.getUntracked().toString());
196 	}
197 
198 	@Test
199 	public void testCreateBranchOnCheckout() throws Exception {
200 		git.checkout().setCreateBranch(true).setName("test2").call();
201 		assertNotNull(db.exactRef("refs/heads/test2"));
202 	}
203 
204 	@Test
205 	public void testCheckoutToNonExistingBranch() throws GitAPIException {
206 		try {
207 			git.checkout().setName("badbranch").call();
208 			fail("Should have failed");
209 		} catch (RefNotFoundException e) {
210 			// except to hit here
211 		}
212 	}
213 
214 	@Test
215 	public void testCheckoutWithConflict() throws Exception {
216 		CheckoutCommand co = git.checkout();
217 		try {
218 			writeTrashFile("Test.txt", "Another change");
219 			assertEquals(Status.NOT_TRIED, co.getResult().getStatus());
220 			co.setName("master").call();
221 			fail("Should have failed");
222 		} catch (Exception e) {
223 			assertEquals(Status.CONFLICTS, co.getResult().getStatus());
224 			assertTrue(co.getResult().getConflictList().contains("Test.txt"));
225 		}
226 		git.checkout().setName("master").setForced(true).call();
227 		assertThat(read("Test.txt"), is("Hello world"));
228 	}
229 
230 	@Test
231 	public void testCheckoutWithNonDeletedFiles() throws Exception {
232 		File testFile = writeTrashFile("temp", "");
233 		try (FileInputStream fis = new FileInputStream(testFile)) {
234 			FileUtils.delete(testFile);
235 			return;
236 		} catch (IOException e) {
237 			// the test makes only sense if deletion of
238 			// a file with open stream fails
239 		}
240 		FileUtils.delete(testFile);
241 		CheckoutCommand co = git.checkout();
242 		// delete Test.txt in branch test
243 		testFile = new File(db.getWorkTree(), "Test.txt");
244 		assertTrue(testFile.exists());
245 		FileUtils.delete(testFile);
246 		assertFalse(testFile.exists());
247 		git.add().addFilepattern("Test.txt");
248 		git.commit().setMessage("Delete Test.txt").setAll(true).call();
249 		git.checkout().setName("master").call();
250 		assertTrue(testFile.exists());
251 		// lock the file so it can't be deleted (in Windows, that is)
252 		try (FileInputStream fis = new FileInputStream(testFile)) {
253 			assertEquals(Status.NOT_TRIED, co.getResult().getStatus());
254 			co.setName("test").call();
255 			assertTrue(testFile.exists());
256 			assertEquals(Status.NONDELETED, co.getResult().getStatus());
257 			assertTrue(co.getResult().getUndeletedList().contains("Test.txt"));
258 		}
259 	}
260 
261 	@Test
262 	public void testCheckoutCommit() throws Exception {
263 		Ref result = git.checkout().setName(initialCommit.name()).call();
264 		assertEquals("[Test.txt, mode:100644, content:Hello world]",
265 				indexState(CONTENT));
266 		assertNull(result);
267 		assertEquals(initialCommit.name(), git.getRepository().getFullBranch());
268 	}
269 
270 	@Test
271 	public void testCheckoutLightweightTag() throws Exception {
272 		git.tag().setAnnotated(false).setName("test-tag")
273 				.setObjectId(initialCommit).call();
274 		Ref result = git.checkout().setName("test-tag").call();
275 
276 		assertNull(result);
277 		assertEquals(initialCommit.getId(), db.resolve(Constants.HEAD));
278 		assertHeadDetached();
279 	}
280 
281 	@Test
282 	public void testCheckoutAnnotatedTag() throws Exception {
283 		git.tag().setAnnotated(true).setName("test-tag")
284 				.setObjectId(initialCommit).call();
285 		Ref result = git.checkout().setName("test-tag").call();
286 
287 		assertNull(result);
288 		assertEquals(initialCommit.getId(), db.resolve(Constants.HEAD));
289 		assertHeadDetached();
290 	}
291 
292 	@Test
293 	public void testCheckoutRemoteTrackingWithUpstream() throws Exception {
294 		Repository db2 = createRepositoryWithRemote();
295 
296 		Git.wrap(db2).checkout().setCreateBranch(true).setName("test")
297 				.setStartPoint("origin/test")
298 				.setUpstreamMode(SetupUpstreamMode.TRACK).call();
299 
300 		assertEquals("refs/heads/test",
301 				db2.exactRef(Constants.HEAD).getTarget().getName());
302 		StoredConfig config = db2.getConfig();
303 		assertEquals("origin", config.getString(
304 				ConfigConstants.CONFIG_BRANCH_SECTION, "test",
305 				ConfigConstants.CONFIG_KEY_REMOTE));
306 		assertEquals("refs/heads/test", config.getString(
307 				ConfigConstants.CONFIG_BRANCH_SECTION, "test",
308 				ConfigConstants.CONFIG_KEY_MERGE));
309 	}
310 
311 	@Test
312 	public void testCheckoutRemoteTrackingWithoutLocalBranch() throws Exception {
313 		Repository db2 = createRepositoryWithRemote();
314 
315 		// checkout remote tracking branch in second repository
316 		// (no local branches exist yet in second repository)
317 		Git.wrap(db2).checkout().setName("remotes/origin/test").call();
318 		assertEquals("[Test.txt, mode:100644, content:Some change]",
319 				indexState(db2, CONTENT));
320 	}
321 
322 
323 
324 	@Test
325 	public void testCheckoutOfFileWithInexistentParentDir() throws Exception {
326 		File a = writeTrashFile("dir/a.txt", "A");
327 		writeTrashFile("dir/b.txt", "A");
328 		git.add().addFilepattern("dir/a.txt").addFilepattern("dir/b.txt")
329 				.call();
330 		git.commit().setMessage("Added dir").call();
331 
332 		File dir = new File(db.getWorkTree(), "dir");
333 		FileUtils.delete(dir, FileUtils.RECURSIVE);
334 
335 		git.checkout().addPath("dir/a.txt").call();
336 		assertTrue(a.exists());
337 	}
338 
339 	@Test
340 	public void testCheckoutOfDirectoryShouldBeRecursive() throws Exception {
341 		File a = writeTrashFile("dir/a.txt", "A");
342 		File b = writeTrashFile("dir/sub/b.txt", "B");
343 		git.add().addFilepattern("dir").call();
344 		git.commit().setMessage("Added dir").call();
345 
346 		write(a, "modified");
347 		write(b, "modified");
348 		git.checkout().addPath("dir").call();
349 
350 		assertThat(read(a), is("A"));
351 		assertThat(read(b), is("B"));
352 	}
353 
354 	@Test
355 	public void testCheckoutAllPaths() throws Exception {
356 		File a = writeTrashFile("dir/a.txt", "A");
357 		File b = writeTrashFile("dir/sub/b.txt", "B");
358 		git.add().addFilepattern("dir").call();
359 		git.commit().setMessage("Added dir").call();
360 
361 		write(a, "modified");
362 		write(b, "modified");
363 		git.checkout().setAllPaths(true).call();
364 
365 		assertThat(read(a), is("A"));
366 		assertThat(read(b), is("B"));
367 	}
368 
369 	@Test
370 	public void testCheckoutWithStartPoint() throws Exception {
371 		File a = writeTrashFile("a.txt", "A");
372 		git.add().addFilepattern("a.txt").call();
373 		RevCommit first = git.commit().setMessage("Added a").call();
374 
375 		write(a, "other");
376 		git.commit().setAll(true).setMessage("Other").call();
377 
378 		git.checkout().setCreateBranch(true).setName("a")
379 				.setStartPoint(first.getId().getName()).call();
380 
381 		assertThat(read(a), is("A"));
382 	}
383 
384 	@Test
385 	public void testCheckoutWithStartPointOnlyCertainFiles() throws Exception {
386 		File a = writeTrashFile("a.txt", "A");
387 		File b = writeTrashFile("b.txt", "B");
388 		git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
389 		RevCommit first = git.commit().setMessage("First").call();
390 
391 		write(a, "other");
392 		write(b, "other");
393 		git.commit().setAll(true).setMessage("Other").call();
394 
395 		git.checkout().setCreateBranch(true).setName("a")
396 				.setStartPoint(first.getId().getName()).addPath("a.txt").call();
397 
398 		assertThat(read(a), is("A"));
399 		assertThat(read(b), is("other"));
400 	}
401 
402 	@Test
403 	public void testDetachedHeadOnCheckout() throws JGitInternalException,
404 			IOException, GitAPIException {
405 		CheckoutCommand co = git.checkout();
406 		co.setName("master").call();
407 
408 		String commitId = db.exactRef(R_HEADS + MASTER).getObjectId().name();
409 		co = git.checkout();
410 		co.setName(commitId).call();
411 
412 		assertHeadDetached();
413 	}
414 
415 	@Test
416 	public void testUpdateSmudgedEntries() throws Exception {
417 		git.branchCreate().setName("test2").call();
418 		RefUpdate rup = db.updateRef(Constants.HEAD);
419 		rup.link("refs/heads/test2");
420 
421 		File file = new File(db.getWorkTree(), "Test.txt");
422 		long size = file.length();
423 		Instant mTime = TimeUtil.setLastModifiedWithOffset(file.toPath(),
424 				-5000L);
425 
426 		DirCache cache = DirCache.lock(db.getIndexFile(), db.getFS());
427 		DirCacheEntry entry = cache.getEntry("Test.txt");
428 		assertNotNull(entry);
429 		entry.setLength(0);
430 		entry.setLastModified(EPOCH);
431 		cache.write();
432 		assertTrue(cache.commit());
433 
434 		cache = DirCache.read(db.getIndexFile(), db.getFS());
435 		entry = cache.getEntry("Test.txt");
436 		assertNotNull(entry);
437 		assertEquals(0, entry.getLength());
438 		assertEquals(EPOCH, entry.getLastModifiedInstant());
439 
440 		Files.setLastModifiedTime(db.getIndexFile().toPath(),
441 				FileTime.from(FS.DETECTED
442 						.lastModifiedInstant(db.getIndexFile())
443 						.minusMillis(5000L)));
444 
445 		assertNotNull(git.checkout().setName("test").call());
446 
447 		cache = DirCache.read(db.getIndexFile(), db.getFS());
448 		entry = cache.getEntry("Test.txt");
449 		assertNotNull(entry);
450 		assertEquals(size, entry.getLength());
451 		assertEquals(mTime, entry.getLastModifiedInstant());
452 	}
453 
454 	@Test
455 	public void testCheckoutOrphanBranch() throws Exception {
456 		CheckoutCommand co = newOrphanBranchCommand();
457 		assertCheckoutRef(co.call());
458 
459 		File HEAD = new File(trash, ".git/HEAD");
460 		String headRef = read(HEAD);
461 		assertEquals("ref: refs/heads/orphanbranch\n", headRef);
462 		assertEquals(2, trash.list().length);
463 
464 		File heads = new File(trash, ".git/refs/heads");
465 		assertEquals(2, heads.listFiles().length);
466 
467 		this.assertNoHead();
468 		this.assertRepositoryCondition(1);
469 		assertEquals(CheckoutResult.NOT_TRIED_RESULT, co.getResult());
470 	}
471 
472 	private Repository createRepositoryWithRemote() throws IOException,
473 			URISyntaxException, MalformedURLException, GitAPIException,
474 			InvalidRemoteException, TransportException {
475 		// create second repository
476 		Repository db2 = createWorkRepository();
477 		try (Git git2 = new Git(db2)) {
478 			// setup the second repository to fetch from the first repository
479 			final StoredConfig config = db2.getConfig();
480 			RemoteConfig remoteConfig = new RemoteConfig(config, "origin");
481 			URIish uri = new URIish(db.getDirectory().toURI().toURL());
482 			remoteConfig.addURI(uri);
483 			remoteConfig.update(config);
484 			config.save();
485 
486 			// fetch from first repository
487 			git2.fetch().setRemote("origin")
488 					.setRefSpecs("+refs/heads/*:refs/remotes/origin/*").call();
489 			return db2;
490 		}
491 	}
492 
493 	private CheckoutCommand newOrphanBranchCommand() {
494 		return git.checkout().setOrphan(true)
495 				.setName("orphanbranch");
496 	}
497 
498 	private static void assertCheckoutRef(Ref ref) {
499 		assertNotNull(ref);
500 		assertEquals("refs/heads/orphanbranch", ref.getTarget().getName());
501 	}
502 
503 	private void assertNoHead() throws IOException {
504 		assertNull(db.resolve("HEAD"));
505 	}
506 
507 	private void assertHeadDetached() throws IOException {
508 		Ref head = db.exactRef(Constants.HEAD);
509 		assertFalse(head.isSymbolic());
510 		assertSame(head, head.getTarget());
511 	}
512 
513 	private void assertRepositoryCondition(int files) throws GitAPIException {
514 		org.eclipse.jgit.api.Status status = this.git.status().call();
515 		assertFalse(status.isClean());
516 		assertEquals(files, status.getAdded().size());
517 	}
518 
519 	@Test
520 	public void testCreateOrphanBranchWithStartCommit() throws Exception {
521 		CheckoutCommand co = newOrphanBranchCommand();
522 		Ref ref = co.setStartPoint(initialCommit).call();
523 		assertCheckoutRef(ref);
524 		assertEquals(2, trash.list().length);
525 		this.assertNoHead();
526 		this.assertRepositoryCondition(1);
527 	}
528 
529 	@Test
530 	public void testCreateOrphanBranchWithStartPoint() throws Exception {
531 		CheckoutCommand co = newOrphanBranchCommand();
532 		Ref ref = co.setStartPoint("HEAD^").call();
533 		assertCheckoutRef(ref);
534 
535 		assertEquals(2, trash.list().length);
536 		this.assertNoHead();
537 		this.assertRepositoryCondition(1);
538 	}
539 
540 	@Test
541 	public void testInvalidRefName() throws Exception {
542 		try {
543 			git.checkout().setOrphan(true).setName("../invalidname").call();
544 			fail("Should have failed");
545 		} catch (InvalidRefNameException e) {
546 			// except to hit here
547 		}
548 	}
549 
550 	@Test
551 	public void testNullRefName() throws Exception {
552 		try {
553 			git.checkout().setOrphan(true).setName(null).call();
554 			fail("Should have failed");
555 		} catch (InvalidRefNameException e) {
556 			// except to hit here
557 		}
558 	}
559 
560 	@Test
561 	public void testAlreadyExists() throws Exception {
562 		this.git.checkout().setCreateBranch(true).setName("orphanbranch")
563 				.call();
564 		this.git.checkout().setName("master").call();
565 
566 		try {
567 			newOrphanBranchCommand().call();
568 			fail("Should have failed");
569 		} catch (RefAlreadyExistsException e) {
570 			// except to hit here
571 		}
572 	}
573 
574 	// TODO: write a faster test which depends less on characteristics of
575 	// underlying filesystem/OS.
576 	@Test
577 	public void testCheckoutAutoCrlfTrue() throws Exception {
578 		int nrOfAutoCrlfTestFiles = 200;
579 
580 		FileBasedConfig c = db.getConfig();
581 		c.setString("core", null, "autocrlf", "true");
582 		c.save();
583 
584 		AddCommand add = git.add();
585 		for (int i = 100; i < 100 + nrOfAutoCrlfTestFiles; i++) {
586 			writeTrashFile("Test_" + i + ".txt", "Hello " + i
587 					+ " world\nX\nYU\nJK\n");
588 			add.addFilepattern("Test_" + i + ".txt");
589 		}
590 		fsTick(null);
591 		add.call();
592 		RevCommit c1 = git.commit().setMessage("add some lines").call();
593 
594 		add = git.add();
595 		for (int i = 100; i < 100 + nrOfAutoCrlfTestFiles; i++) {
596 			writeTrashFile("Test_" + i + ".txt", "Hello " + i
597 					+ " world\nX\nY\n");
598 			add.addFilepattern("Test_" + i + ".txt");
599 		}
600 		fsTick(null);
601 		add.call();
602 		git.commit().setMessage("add more").call();
603 
604 		git.checkout().setName(c1.getName()).call();
605 
606 		boolean foundUnsmudged = false;
607 		DirCache dc = db.readDirCache();
608 		for (int i = 100; i < 100 + nrOfAutoCrlfTestFiles; i++) {
609 			DirCacheEntry entry = dc.getEntry(
610 					"Test_" + i + ".txt");
611 			if (!entry.isSmudged()) {
612 				foundUnsmudged = true;
613 				assertEquals("unexpected file length in git index", 28,
614 						entry.getLength());
615 			}
616 		}
617 		org.junit.Assume.assumeTrue(foundUnsmudged);
618 	}
619 
620 	@Test
621 	public void testSmudgeFilter_modifyExisting() throws IOException, GitAPIException {
622 		File script = writeTempFile("sed s/o/e/g");
623 		StoredConfig config = git.getRepository().getConfig();
624 		config.setString("filter", "lfs", "smudge",
625 				"sh " + slashify(script.getPath()));
626 		config.save();
627 
628 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
629 		git.add().addFilepattern(".gitattributes").call();
630 		git.commit().setMessage("add filter").call();
631 
632 		writeTrashFile("src/a.tmp", "x");
633 		// Caution: we need a trailing '\n' since sed on mac always appends
634 		// linefeeds if missing
635 		writeTrashFile("src/a.txt", "x\n");
636 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
637 				.call();
638 		RevCommit content1 = git.commit().setMessage("add content").call();
639 
640 		writeTrashFile("src/a.tmp", "foo");
641 		writeTrashFile("src/a.txt", "foo\n");
642 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
643 				.call();
644 		RevCommit content2 = git.commit().setMessage("changed content").call();
645 
646 		git.checkout().setName(content1.getName()).call();
647 		git.checkout().setName(content2.getName()).call();
648 
649 		assertEquals(
650 				"[.gitattributes, mode:100644, content:*.txt filter=lfs][Test.txt, mode:100644, content:Some change][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:foo\n]",
651 				indexState(CONTENT));
652 		assertEquals(Sets.of("src/a.txt"), git.status().call().getModified());
653 		assertEquals("foo", read("src/a.tmp"));
654 		assertEquals("fee\n", read("src/a.txt"));
655 	}
656 
657 	@Test
658 	public void testSmudgeFilter_createNew()
659 			throws IOException, GitAPIException {
660 		File script = writeTempFile("sed s/o/e/g");
661 		StoredConfig config = git.getRepository().getConfig();
662 		config.setString("filter", "lfs", "smudge",
663 				"sh " + slashify(script.getPath()));
664 		config.save();
665 
666 		writeTrashFile("foo", "foo");
667 		git.add().addFilepattern("foo").call();
668 		RevCommit initial = git.commit().setMessage("initial").call();
669 
670 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
671 		git.add().addFilepattern(".gitattributes").call();
672 		git.commit().setMessage("add filter").call();
673 
674 		writeTrashFile("src/a.tmp", "foo");
675 		// Caution: we need a trailing '\n' since sed on mac always appends
676 		// linefeeds if missing
677 		writeTrashFile("src/a.txt", "foo\n");
678 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
679 				.call();
680 		RevCommit content = git.commit().setMessage("added content").call();
681 
682 		git.checkout().setName(initial.getName()).call();
683 		git.checkout().setName(content.getName()).call();
684 
685 		assertEquals(
686 				"[.gitattributes, mode:100644, content:*.txt filter=lfs][Test.txt, mode:100644, content:Some change][foo, mode:100644, content:foo][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:foo\n]",
687 				indexState(CONTENT));
688 		assertEquals("foo", read("src/a.tmp"));
689 		assertEquals("fee\n", read("src/a.txt"));
690 	}
691 
692 	@Test
693 	public void testSmudgeFilter_deleteFileAndRestoreFromCommit()
694 			throws IOException, GitAPIException {
695 		File script = writeTempFile("sed s/o/e/g");
696 		StoredConfig config = git.getRepository().getConfig();
697 		config.setString("filter", "lfs", "smudge",
698 				"sh " + slashify(script.getPath()));
699 		config.save();
700 
701 		writeTrashFile("foo", "foo");
702 		git.add().addFilepattern("foo").call();
703 		git.commit().setMessage("initial").call();
704 
705 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
706 		git.add().addFilepattern(".gitattributes").call();
707 		git.commit().setMessage("add filter").call();
708 
709 		writeTrashFile("src/a.tmp", "foo");
710 		// Caution: we need a trailing '\n' since sed on mac always appends
711 		// linefeeds if missing
712 		writeTrashFile("src/a.txt", "foo\n");
713 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
714 				.call();
715 		RevCommit content = git.commit().setMessage("added content").call();
716 
717 		deleteTrashFile("src/a.txt");
718 		git.checkout().setStartPoint(content.getName()).addPath("src/a.txt")
719 				.call();
720 
721 		assertEquals(
722 				"[.gitattributes, mode:100644, content:*.txt filter=lfs][Test.txt, mode:100644, content:Some change][foo, mode:100644, content:foo][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:foo\n]",
723 				indexState(CONTENT));
724 		assertEquals("foo", read("src/a.tmp"));
725 		assertEquals("fee\n", read("src/a.txt"));
726 	}
727 
728 	@Test
729 	public void testSmudgeFilter_deleteFileAndRestoreFromIndex()
730 			throws IOException, GitAPIException {
731 		File script = writeTempFile("sed s/o/e/g");
732 		StoredConfig config = git.getRepository().getConfig();
733 		config.setString("filter", "lfs", "smudge",
734 				"sh " + slashify(script.getPath()));
735 		config.save();
736 
737 		writeTrashFile("foo", "foo");
738 		git.add().addFilepattern("foo").call();
739 		git.commit().setMessage("initial").call();
740 
741 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
742 		git.add().addFilepattern(".gitattributes").call();
743 		git.commit().setMessage("add filter").call();
744 
745 		writeTrashFile("src/a.tmp", "foo");
746 		// Caution: we need a trailing '\n' since sed on mac always appends
747 		// linefeeds if missing
748 		writeTrashFile("src/a.txt", "foo\n");
749 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
750 				.call();
751 		git.commit().setMessage("added content").call();
752 
753 		deleteTrashFile("src/a.txt");
754 		git.checkout().addPath("src/a.txt").call();
755 
756 		assertEquals(
757 				"[.gitattributes, mode:100644, content:*.txt filter=lfs][Test.txt, mode:100644, content:Some change][foo, mode:100644, content:foo][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:foo\n]",
758 				indexState(CONTENT));
759 		assertEquals("foo", read("src/a.tmp"));
760 		assertEquals("fee\n", read("src/a.txt"));
761 	}
762 
763 	@Test
764 	public void testSmudgeFilter_deleteFileAndCreateBranchAndRestoreFromCommit()
765 			throws IOException, GitAPIException {
766 		File script = writeTempFile("sed s/o/e/g");
767 		StoredConfig config = git.getRepository().getConfig();
768 		config.setString("filter", "lfs", "smudge",
769 				"sh " + slashify(script.getPath()));
770 		config.save();
771 
772 		writeTrashFile("foo", "foo");
773 		git.add().addFilepattern("foo").call();
774 		git.commit().setMessage("initial").call();
775 
776 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
777 		git.add().addFilepattern(".gitattributes").call();
778 		git.commit().setMessage("add filter").call();
779 
780 		writeTrashFile("src/a.tmp", "foo");
781 		// Caution: we need a trailing '\n' since sed on mac always appends
782 		// linefeeds if missing
783 		writeTrashFile("src/a.txt", "foo\n");
784 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
785 				.call();
786 		RevCommit content = git.commit().setMessage("added content").call();
787 
788 		deleteTrashFile("src/a.txt");
789 		git.checkout().setName("newBranch").setCreateBranch(true)
790 				.setStartPoint(content).addPath("src/a.txt").call();
791 
792 		assertEquals(
793 				"[.gitattributes, mode:100644, content:*.txt filter=lfs][Test.txt, mode:100644, content:Some change][foo, mode:100644, content:foo][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:foo\n]",
794 				indexState(CONTENT));
795 		assertEquals("foo", read("src/a.tmp"));
796 		assertEquals("fee\n", read("src/a.txt"));
797 	}
798 
799 	@Test
800 	public void testSmudgeAndClean() throws Exception {
801 		File clean_filter = writeTempFile("sed s/V1/@version/g");
802 		File smudge_filter = writeTempFile("sed s/@version/V1/g");
803 
804 		try (Git git2 = new Git(db)) {
805 			StoredConfig config = git.getRepository().getConfig();
806 			config.setString("filter", "lfs", "smudge",
807 					"sh " + slashify(smudge_filter.getPath()));
808 			config.setString("filter", "lfs", "clean",
809 					"sh " + slashify(clean_filter.getPath()));
810 			config.setBoolean("filter", "lfs", "useJGitBuiltin", true);
811 			config.save();
812 			writeTrashFile(".gitattributes", "filterTest.txt filter=lfs");
813 			git2.add().addFilepattern(".gitattributes").call();
814 			git2.commit().setMessage("add attributes").call();
815 
816 			fsTick(writeTrashFile("filterTest.txt", "hello world, V1\n"));
817 			git2.add().addFilepattern("filterTest.txt").call();
818 			RevCommit one = git2.commit().setMessage("add filterText.txt").call();
819 			assertEquals(
820 					"[.gitattributes, mode:100644, content:filterTest.txt filter=lfs][Test.txt, mode:100644, content:Some change][filterTest.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:7bd5d32e5c494354aa4c2473a1306d0ce7b52cc3bffeb342c03cd517ef8cf8da\nsize 16\n]",
821 					indexState(CONTENT));
822 
823 			fsTick(writeTrashFile("filterTest.txt", "bon giorno world, V1\n"));
824 			git2.add().addFilepattern("filterTest.txt").call();
825 			RevCommit two = git2.commit().setMessage("modified filterTest.txt").call();
826 
827 			assertTrue(git2.status().call().isClean());
828 			assertEquals(
829 					"[.gitattributes, mode:100644, content:filterTest.txt filter=lfs][Test.txt, mode:100644, content:Some change][filterTest.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:087148cccf53b0049c56475c1595113c9da4b638997c3489af8ac7108d51ef13\nsize 21\n]",
830 					indexState(CONTENT));
831 
832 			git2.checkout().setName(one.getName()).call();
833 			assertTrue(git2.status().call().isClean());
834 			assertEquals(
835 					"[.gitattributes, mode:100644, content:filterTest.txt filter=lfs][Test.txt, mode:100644, content:Some change][filterTest.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:7bd5d32e5c494354aa4c2473a1306d0ce7b52cc3bffeb342c03cd517ef8cf8da\nsize 16\n]",
836 					indexState(CONTENT));
837 			assertEquals("hello world, V1\n", read("filterTest.txt"));
838 
839 			git2.checkout().setName(two.getName()).call();
840 			assertTrue(git2.status().call().isClean());
841 			assertEquals(
842 					"[.gitattributes, mode:100644, content:filterTest.txt filter=lfs][Test.txt, mode:100644, content:Some change][filterTest.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:087148cccf53b0049c56475c1595113c9da4b638997c3489af8ac7108d51ef13\nsize 21\n]",
843 					indexState(CONTENT));
844 			assertEquals("bon giorno world, V1\n", read("filterTest.txt"));
845 		}
846 	}
847 
848 	@Test
849 	public void testNonDeletableFilesOnWindows()
850 			throws GitAPIException, IOException {
851 		// Only on windows a FileInputStream blocks us from deleting a file
852 		org.junit.Assume.assumeTrue(SystemReader.getInstance().isWindows());
853 		writeTrashFile("toBeModified.txt", "a");
854 		writeTrashFile("toBeDeleted.txt", "a");
855 		git.add().addFilepattern(".").call();
856 		RevCommit addFiles = git.commit().setMessage("add more files").call();
857 
858 		git.rm().setCached(false).addFilepattern("Test.txt")
859 				.addFilepattern("toBeDeleted.txt").call();
860 		writeTrashFile("toBeModified.txt", "b");
861 		writeTrashFile("toBeCreated.txt", "a");
862 		git.add().addFilepattern(".").call();
863 		RevCommit crudCommit = git.commit().setMessage("delete, modify, add")
864 				.call();
865 		git.checkout().setName(addFiles.getName()).call();
866 		try ( FileInputStream fis=new FileInputStream(new File(db.getWorkTree(), "Test.txt")) ) {
867 			CheckoutCommand coCommand = git.checkout();
868 			coCommand.setName(crudCommit.getName()).call();
869 			CheckoutResult result = coCommand.getResult();
870 			assertEquals(Status.NONDELETED, result.getStatus());
871 			assertEquals("[Test.txt, toBeDeleted.txt]",
872 					result.getRemovedList().toString());
873 			assertEquals("[toBeCreated.txt, toBeModified.txt]",
874 					result.getModifiedList().toString());
875 			assertEquals("[Test.txt]", result.getUndeletedList().toString());
876 			assertTrue(result.getConflictList().isEmpty());
877 		}
878 	}
879 
880 	private File writeTempFile(String body) throws IOException {
881 		File f = File.createTempFile("CheckoutCommandTest_", "");
882 		JGitTestUtil.write(f, body);
883 		return f;
884 	}
885 }