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