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 		addRepoToClose(db2);
296 
297 		Git.wrap(db2).checkout().setCreateBranch(true).setName("test")
298 				.setStartPoint("origin/test")
299 				.setUpstreamMode(SetupUpstreamMode.TRACK).call();
300 
301 		assertEquals("refs/heads/test",
302 				db2.exactRef(Constants.HEAD).getTarget().getName());
303 		StoredConfig config = db2.getConfig();
304 		assertEquals("origin", config.getString(
305 				ConfigConstants.CONFIG_BRANCH_SECTION, "test",
306 				ConfigConstants.CONFIG_KEY_REMOTE));
307 		assertEquals("refs/heads/test", config.getString(
308 				ConfigConstants.CONFIG_BRANCH_SECTION, "test",
309 				ConfigConstants.CONFIG_KEY_MERGE));
310 	}
311 
312 	@Test
313 	public void testCheckoutRemoteTrackingWithoutLocalBranch() throws Exception {
314 		Repository db2 = createRepositoryWithRemote();
315 		addRepoToClose(db2);
316 
317 		// checkout remote tracking branch in second repository
318 		// (no local branches exist yet in second repository)
319 		Git.wrap(db2).checkout().setName("remotes/origin/test").call();
320 		assertEquals("[Test.txt, mode:100644, content:Some change]",
321 				indexState(db2, CONTENT));
322 	}
323 
324 
325 
326 	@Test
327 	public void testCheckoutOfFileWithInexistentParentDir() throws Exception {
328 		File a = writeTrashFile("dir/a.txt", "A");
329 		writeTrashFile("dir/b.txt", "A");
330 		git.add().addFilepattern("dir/a.txt").addFilepattern("dir/b.txt")
331 				.call();
332 		git.commit().setMessage("Added dir").call();
333 
334 		File dir = new File(db.getWorkTree(), "dir");
335 		FileUtils.delete(dir, FileUtils.RECURSIVE);
336 
337 		git.checkout().addPath("dir/a.txt").call();
338 		assertTrue(a.exists());
339 	}
340 
341 	@Test
342 	public void testCheckoutOfDirectoryShouldBeRecursive() throws Exception {
343 		File a = writeTrashFile("dir/a.txt", "A");
344 		File b = writeTrashFile("dir/sub/b.txt", "B");
345 		git.add().addFilepattern("dir").call();
346 		git.commit().setMessage("Added dir").call();
347 
348 		write(a, "modified");
349 		write(b, "modified");
350 		git.checkout().addPath("dir").call();
351 
352 		assertThat(read(a), is("A"));
353 		assertThat(read(b), is("B"));
354 	}
355 
356 	@Test
357 	public void testCheckoutAllPaths() throws Exception {
358 		File a = writeTrashFile("dir/a.txt", "A");
359 		File b = writeTrashFile("dir/sub/b.txt", "B");
360 		git.add().addFilepattern("dir").call();
361 		git.commit().setMessage("Added dir").call();
362 
363 		write(a, "modified");
364 		write(b, "modified");
365 		git.checkout().setAllPaths(true).call();
366 
367 		assertThat(read(a), is("A"));
368 		assertThat(read(b), is("B"));
369 	}
370 
371 	@Test
372 	public void testCheckoutWithStartPoint() throws Exception {
373 		File a = writeTrashFile("a.txt", "A");
374 		git.add().addFilepattern("a.txt").call();
375 		RevCommit first = git.commit().setMessage("Added a").call();
376 
377 		write(a, "other");
378 		git.commit().setAll(true).setMessage("Other").call();
379 
380 		git.checkout().setCreateBranch(true).setName("a")
381 				.setStartPoint(first.getId().getName()).call();
382 
383 		assertThat(read(a), is("A"));
384 	}
385 
386 	@Test
387 	public void testCheckoutWithStartPointOnlyCertainFiles() throws Exception {
388 		File a = writeTrashFile("a.txt", "A");
389 		File b = writeTrashFile("b.txt", "B");
390 		git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
391 		RevCommit first = git.commit().setMessage("First").call();
392 
393 		write(a, "other");
394 		write(b, "other");
395 		git.commit().setAll(true).setMessage("Other").call();
396 
397 		git.checkout().setCreateBranch(true).setName("a")
398 				.setStartPoint(first.getId().getName()).addPath("a.txt").call();
399 
400 		assertThat(read(a), is("A"));
401 		assertThat(read(b), is("other"));
402 	}
403 
404 	@Test
405 	public void testDetachedHeadOnCheckout() throws JGitInternalException,
406 			IOException, GitAPIException {
407 		CheckoutCommand co = git.checkout();
408 		co.setName("master").call();
409 
410 		String commitId = db.exactRef(R_HEADS + MASTER).getObjectId().name();
411 		co = git.checkout();
412 		co.setName(commitId).call();
413 
414 		assertHeadDetached();
415 	}
416 
417 	@Test
418 	public void testUpdateSmudgedEntries() throws Exception {
419 		git.branchCreate().setName("test2").call();
420 		RefUpdate rup = db.updateRef(Constants.HEAD);
421 		rup.link("refs/heads/test2");
422 
423 		File file = new File(db.getWorkTree(), "Test.txt");
424 		long size = file.length();
425 		Instant mTime = TimeUtil.setLastModifiedWithOffset(file.toPath(),
426 				-5000L);
427 
428 		DirCache cache = DirCache.lock(db.getIndexFile(), db.getFS());
429 		DirCacheEntry entry = cache.getEntry("Test.txt");
430 		assertNotNull(entry);
431 		entry.setLength(0);
432 		entry.setLastModified(EPOCH);
433 		cache.write();
434 		assertTrue(cache.commit());
435 
436 		cache = DirCache.read(db.getIndexFile(), db.getFS());
437 		entry = cache.getEntry("Test.txt");
438 		assertNotNull(entry);
439 		assertEquals(0, entry.getLength());
440 		assertEquals(EPOCH, entry.getLastModifiedInstant());
441 
442 		Files.setLastModifiedTime(db.getIndexFile().toPath(),
443 				FileTime.from(FS.DETECTED
444 						.lastModifiedInstant(db.getIndexFile())
445 						.minusMillis(5000L)));
446 
447 		assertNotNull(git.checkout().setName("test").call());
448 
449 		cache = DirCache.read(db.getIndexFile(), db.getFS());
450 		entry = cache.getEntry("Test.txt");
451 		assertNotNull(entry);
452 		assertEquals(size, entry.getLength());
453 		assertEquals(mTime, entry.getLastModifiedInstant());
454 	}
455 
456 	@Test
457 	public void testCheckoutOrphanBranch() throws Exception {
458 		CheckoutCommand co = newOrphanBranchCommand();
459 		assertCheckoutRef(co.call());
460 
461 		File HEAD = new File(trash, ".git/HEAD");
462 		String headRef = read(HEAD);
463 		assertEquals("ref: refs/heads/orphanbranch\n", headRef);
464 		assertEquals(2, trash.list().length);
465 
466 		File heads = new File(trash, ".git/refs/heads");
467 		assertEquals(2, heads.listFiles().length);
468 
469 		this.assertNoHead();
470 		this.assertRepositoryCondition(1);
471 		assertEquals(CheckoutResult.NOT_TRIED_RESULT, co.getResult());
472 	}
473 
474 	private Repository createRepositoryWithRemote() throws IOException,
475 			URISyntaxException, MalformedURLException, GitAPIException,
476 			InvalidRemoteException, TransportException {
477 		// create second repository
478 		Repository db2 = createWorkRepository();
479 		try (Git git2 = new Git(db2)) {
480 			// setup the second repository to fetch from the first repository
481 			final StoredConfig config = db2.getConfig();
482 			RemoteConfig remoteConfig = new RemoteConfig(config, "origin");
483 			URIish uri = new URIish(db.getDirectory().toURI().toURL());
484 			remoteConfig.addURI(uri);
485 			remoteConfig.update(config);
486 			config.save();
487 
488 			// fetch from first repository
489 			git2.fetch().setRemote("origin")
490 					.setRefSpecs("+refs/heads/*:refs/remotes/origin/*").call();
491 			return db2;
492 		}
493 	}
494 
495 	private CheckoutCommand newOrphanBranchCommand() {
496 		return git.checkout().setOrphan(true)
497 				.setName("orphanbranch");
498 	}
499 
500 	private static void assertCheckoutRef(Ref ref) {
501 		assertNotNull(ref);
502 		assertEquals("refs/heads/orphanbranch", ref.getTarget().getName());
503 	}
504 
505 	private void assertNoHead() throws IOException {
506 		assertNull(db.resolve("HEAD"));
507 	}
508 
509 	private void assertHeadDetached() throws IOException {
510 		Ref head = db.exactRef(Constants.HEAD);
511 		assertFalse(head.isSymbolic());
512 		assertSame(head, head.getTarget());
513 	}
514 
515 	private void assertRepositoryCondition(int files) throws GitAPIException {
516 		org.eclipse.jgit.api.Status status = this.git.status().call();
517 		assertFalse(status.isClean());
518 		assertEquals(files, status.getAdded().size());
519 	}
520 
521 	@Test
522 	public void testCreateOrphanBranchWithStartCommit() throws Exception {
523 		CheckoutCommand co = newOrphanBranchCommand();
524 		Ref ref = co.setStartPoint(initialCommit).call();
525 		assertCheckoutRef(ref);
526 		assertEquals(2, trash.list().length);
527 		this.assertNoHead();
528 		this.assertRepositoryCondition(1);
529 	}
530 
531 	@Test
532 	public void testCreateOrphanBranchWithStartPoint() throws Exception {
533 		CheckoutCommand co = newOrphanBranchCommand();
534 		Ref ref = co.setStartPoint("HEAD^").call();
535 		assertCheckoutRef(ref);
536 
537 		assertEquals(2, trash.list().length);
538 		this.assertNoHead();
539 		this.assertRepositoryCondition(1);
540 	}
541 
542 	@Test
543 	public void testInvalidRefName() throws Exception {
544 		try {
545 			git.checkout().setOrphan(true).setName("../invalidname").call();
546 			fail("Should have failed");
547 		} catch (InvalidRefNameException e) {
548 			// except to hit here
549 		}
550 	}
551 
552 	@Test
553 	public void testNullRefName() throws Exception {
554 		try {
555 			git.checkout().setOrphan(true).setName(null).call();
556 			fail("Should have failed");
557 		} catch (InvalidRefNameException e) {
558 			// except to hit here
559 		}
560 	}
561 
562 	@Test
563 	public void testAlreadyExists() throws Exception {
564 		this.git.checkout().setCreateBranch(true).setName("orphanbranch")
565 				.call();
566 		this.git.checkout().setName("master").call();
567 
568 		try {
569 			newOrphanBranchCommand().call();
570 			fail("Should have failed");
571 		} catch (RefAlreadyExistsException e) {
572 			// except to hit here
573 		}
574 	}
575 
576 	// TODO: write a faster test which depends less on characteristics of
577 	// underlying filesystem/OS.
578 	@Test
579 	public void testCheckoutAutoCrlfTrue() throws Exception {
580 		int nrOfAutoCrlfTestFiles = 200;
581 
582 		FileBasedConfig c = db.getConfig();
583 		c.setString("core", null, "autocrlf", "true");
584 		c.save();
585 
586 		AddCommand add = git.add();
587 		for (int i = 100; i < 100 + nrOfAutoCrlfTestFiles; i++) {
588 			writeTrashFile("Test_" + i + ".txt", "Hello " + i
589 					+ " world\nX\nYU\nJK\n");
590 			add.addFilepattern("Test_" + i + ".txt");
591 		}
592 		fsTick(null);
593 		add.call();
594 		RevCommit c1 = git.commit().setMessage("add some lines").call();
595 
596 		add = git.add();
597 		for (int i = 100; i < 100 + nrOfAutoCrlfTestFiles; i++) {
598 			writeTrashFile("Test_" + i + ".txt", "Hello " + i
599 					+ " world\nX\nY\n");
600 			add.addFilepattern("Test_" + i + ".txt");
601 		}
602 		fsTick(null);
603 		add.call();
604 		git.commit().setMessage("add more").call();
605 
606 		git.checkout().setName(c1.getName()).call();
607 
608 		boolean foundUnsmudged = false;
609 		DirCache dc = db.readDirCache();
610 		for (int i = 100; i < 100 + nrOfAutoCrlfTestFiles; i++) {
611 			DirCacheEntry entry = dc.getEntry(
612 					"Test_" + i + ".txt");
613 			if (!entry.isSmudged()) {
614 				foundUnsmudged = true;
615 				assertEquals("unexpected file length in git index", 28,
616 						entry.getLength());
617 			}
618 		}
619 		org.junit.Assume.assumeTrue(foundUnsmudged);
620 	}
621 
622 	@Test
623 	public void testSmudgeFilter_modifyExisting() throws IOException, GitAPIException {
624 		File script = writeTempFile("sed s/o/e/g");
625 		StoredConfig config = git.getRepository().getConfig();
626 		config.setString("filter", "lfs", "smudge",
627 				"sh " + slashify(script.getPath()));
628 		config.save();
629 
630 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
631 		git.add().addFilepattern(".gitattributes").call();
632 		git.commit().setMessage("add filter").call();
633 
634 		writeTrashFile("src/a.tmp", "x");
635 		// Caution: we need a trailing '\n' since sed on mac always appends
636 		// linefeeds if missing
637 		writeTrashFile("src/a.txt", "x\n");
638 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
639 				.call();
640 		RevCommit content1 = git.commit().setMessage("add content").call();
641 
642 		writeTrashFile("src/a.tmp", "foo");
643 		writeTrashFile("src/a.txt", "foo\n");
644 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
645 				.call();
646 		RevCommit content2 = git.commit().setMessage("changed content").call();
647 
648 		git.checkout().setName(content1.getName()).call();
649 		git.checkout().setName(content2.getName()).call();
650 
651 		assertEquals(
652 				"[.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]",
653 				indexState(CONTENT));
654 		assertEquals(Sets.of("src/a.txt"), git.status().call().getModified());
655 		assertEquals("foo", read("src/a.tmp"));
656 		assertEquals("fee\n", read("src/a.txt"));
657 	}
658 
659 	@Test
660 	public void testSmudgeFilter_createNew()
661 			throws IOException, GitAPIException {
662 		File script = writeTempFile("sed s/o/e/g");
663 		StoredConfig config = git.getRepository().getConfig();
664 		config.setString("filter", "lfs", "smudge",
665 				"sh " + slashify(script.getPath()));
666 		config.save();
667 
668 		writeTrashFile("foo", "foo");
669 		git.add().addFilepattern("foo").call();
670 		RevCommit initial = git.commit().setMessage("initial").call();
671 
672 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
673 		git.add().addFilepattern(".gitattributes").call();
674 		git.commit().setMessage("add filter").call();
675 
676 		writeTrashFile("src/a.tmp", "foo");
677 		// Caution: we need a trailing '\n' since sed on mac always appends
678 		// linefeeds if missing
679 		writeTrashFile("src/a.txt", "foo\n");
680 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
681 				.call();
682 		RevCommit content = git.commit().setMessage("added content").call();
683 
684 		git.checkout().setName(initial.getName()).call();
685 		git.checkout().setName(content.getName()).call();
686 
687 		assertEquals(
688 				"[.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]",
689 				indexState(CONTENT));
690 		assertEquals("foo", read("src/a.tmp"));
691 		assertEquals("fee\n", read("src/a.txt"));
692 	}
693 
694 	@Test
695 	public void testSmudgeFilter_deleteFileAndRestoreFromCommit()
696 			throws IOException, GitAPIException {
697 		File script = writeTempFile("sed s/o/e/g");
698 		StoredConfig config = git.getRepository().getConfig();
699 		config.setString("filter", "lfs", "smudge",
700 				"sh " + slashify(script.getPath()));
701 		config.save();
702 
703 		writeTrashFile("foo", "foo");
704 		git.add().addFilepattern("foo").call();
705 		git.commit().setMessage("initial").call();
706 
707 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
708 		git.add().addFilepattern(".gitattributes").call();
709 		git.commit().setMessage("add filter").call();
710 
711 		writeTrashFile("src/a.tmp", "foo");
712 		// Caution: we need a trailing '\n' since sed on mac always appends
713 		// linefeeds if missing
714 		writeTrashFile("src/a.txt", "foo\n");
715 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
716 				.call();
717 		RevCommit content = git.commit().setMessage("added content").call();
718 
719 		deleteTrashFile("src/a.txt");
720 		git.checkout().setStartPoint(content.getName()).addPath("src/a.txt")
721 				.call();
722 
723 		assertEquals(
724 				"[.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]",
725 				indexState(CONTENT));
726 		assertEquals("foo", read("src/a.tmp"));
727 		assertEquals("fee\n", read("src/a.txt"));
728 	}
729 
730 	@Test
731 	public void testSmudgeFilter_deleteFileAndRestoreFromIndex()
732 			throws IOException, GitAPIException {
733 		File script = writeTempFile("sed s/o/e/g");
734 		StoredConfig config = git.getRepository().getConfig();
735 		config.setString("filter", "lfs", "smudge",
736 				"sh " + slashify(script.getPath()));
737 		config.save();
738 
739 		writeTrashFile("foo", "foo");
740 		git.add().addFilepattern("foo").call();
741 		git.commit().setMessage("initial").call();
742 
743 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
744 		git.add().addFilepattern(".gitattributes").call();
745 		git.commit().setMessage("add filter").call();
746 
747 		writeTrashFile("src/a.tmp", "foo");
748 		// Caution: we need a trailing '\n' since sed on mac always appends
749 		// linefeeds if missing
750 		writeTrashFile("src/a.txt", "foo\n");
751 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
752 				.call();
753 		git.commit().setMessage("added content").call();
754 
755 		deleteTrashFile("src/a.txt");
756 		git.checkout().addPath("src/a.txt").call();
757 
758 		assertEquals(
759 				"[.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]",
760 				indexState(CONTENT));
761 		assertEquals("foo", read("src/a.tmp"));
762 		assertEquals("fee\n", read("src/a.txt"));
763 	}
764 
765 	@Test
766 	public void testSmudgeFilter_deleteFileAndCreateBranchAndRestoreFromCommit()
767 			throws IOException, GitAPIException {
768 		File script = writeTempFile("sed s/o/e/g");
769 		StoredConfig config = git.getRepository().getConfig();
770 		config.setString("filter", "lfs", "smudge",
771 				"sh " + slashify(script.getPath()));
772 		config.save();
773 
774 		writeTrashFile("foo", "foo");
775 		git.add().addFilepattern("foo").call();
776 		git.commit().setMessage("initial").call();
777 
778 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
779 		git.add().addFilepattern(".gitattributes").call();
780 		git.commit().setMessage("add filter").call();
781 
782 		writeTrashFile("src/a.tmp", "foo");
783 		// Caution: we need a trailing '\n' since sed on mac always appends
784 		// linefeeds if missing
785 		writeTrashFile("src/a.txt", "foo\n");
786 		git.add().addFilepattern("src/a.tmp").addFilepattern("src/a.txt")
787 				.call();
788 		RevCommit content = git.commit().setMessage("added content").call();
789 
790 		deleteTrashFile("src/a.txt");
791 		git.checkout().setName("newBranch").setCreateBranch(true)
792 				.setStartPoint(content).addPath("src/a.txt").call();
793 
794 		assertEquals(
795 				"[.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]",
796 				indexState(CONTENT));
797 		assertEquals("foo", read("src/a.tmp"));
798 		assertEquals("fee\n", read("src/a.txt"));
799 	}
800 
801 	@Test
802 	public void testSmudgeAndClean() throws Exception {
803 		File clean_filter = writeTempFile("sed s/V1/@version/g");
804 		File smudge_filter = writeTempFile("sed s/@version/V1/g");
805 
806 		try (Git git2 = new Git(db)) {
807 			StoredConfig config = git.getRepository().getConfig();
808 			config.setString("filter", "lfs", "smudge",
809 					"sh " + slashify(smudge_filter.getPath()));
810 			config.setString("filter", "lfs", "clean",
811 					"sh " + slashify(clean_filter.getPath()));
812 			config.setBoolean("filter", "lfs", "useJGitBuiltin", true);
813 			config.save();
814 			writeTrashFile(".gitattributes", "filterTest.txt filter=lfs");
815 			git2.add().addFilepattern(".gitattributes").call();
816 			git2.commit().setMessage("add attributes").call();
817 
818 			fsTick(writeTrashFile("filterTest.txt", "hello world, V1\n"));
819 			git2.add().addFilepattern("filterTest.txt").call();
820 			RevCommit one = git2.commit().setMessage("add filterText.txt").call();
821 			assertEquals(
822 					"[.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]",
823 					indexState(CONTENT));
824 
825 			fsTick(writeTrashFile("filterTest.txt", "bon giorno world, V1\n"));
826 			git2.add().addFilepattern("filterTest.txt").call();
827 			RevCommit two = git2.commit().setMessage("modified filterTest.txt").call();
828 
829 			assertTrue(git2.status().call().isClean());
830 			assertEquals(
831 					"[.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]",
832 					indexState(CONTENT));
833 
834 			git2.checkout().setName(one.getName()).call();
835 			assertTrue(git2.status().call().isClean());
836 			assertEquals(
837 					"[.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]",
838 					indexState(CONTENT));
839 			assertEquals("hello world, V1\n", read("filterTest.txt"));
840 
841 			git2.checkout().setName(two.getName()).call();
842 			assertTrue(git2.status().call().isClean());
843 			assertEquals(
844 					"[.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]",
845 					indexState(CONTENT));
846 			assertEquals("bon giorno world, V1\n", read("filterTest.txt"));
847 		}
848 	}
849 
850 	@Test
851 	public void testNonDeletableFilesOnWindows()
852 			throws GitAPIException, IOException {
853 		// Only on windows a FileInputStream blocks us from deleting a file
854 		org.junit.Assume.assumeTrue(SystemReader.getInstance().isWindows());
855 		writeTrashFile("toBeModified.txt", "a");
856 		writeTrashFile("toBeDeleted.txt", "a");
857 		git.add().addFilepattern(".").call();
858 		RevCommit addFiles = git.commit().setMessage("add more files").call();
859 
860 		git.rm().setCached(false).addFilepattern("Test.txt")
861 				.addFilepattern("toBeDeleted.txt").call();
862 		writeTrashFile("toBeModified.txt", "b");
863 		writeTrashFile("toBeCreated.txt", "a");
864 		git.add().addFilepattern(".").call();
865 		RevCommit crudCommit = git.commit().setMessage("delete, modify, add")
866 				.call();
867 		git.checkout().setName(addFiles.getName()).call();
868 		try ( FileInputStream fis=new FileInputStream(new File(db.getWorkTree(), "Test.txt")) ) {
869 			CheckoutCommand coCommand = git.checkout();
870 			coCommand.setName(crudCommit.getName()).call();
871 			CheckoutResult result = coCommand.getResult();
872 			assertEquals(Status.NONDELETED, result.getStatus());
873 			assertEquals("[toBeDeleted.txt]",
874 					result.getRemovedList().toString());
875 			assertEquals("[toBeCreated.txt, toBeModified.txt]",
876 					result.getModifiedList().toString());
877 			assertEquals("[Test.txt]", result.getUndeletedList().toString());
878 			assertTrue(result.getConflictList().isEmpty());
879 		}
880 	}
881 
882 	private File writeTempFile(String body) throws IOException {
883 		File f = File.createTempFile("CheckoutCommandTest_", "");
884 		JGitTestUtil.write(f, body);
885 		return f;
886 	}
887 }