View Javadoc
1   /*
2    * Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com>
3    * Copyright (C) 2010, Christian Halstrick <christian.halstrick@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.nio.charset.StandardCharsets.UTF_8;
14  import static org.eclipse.jgit.util.FileUtils.RECURSIVE;
15  import static org.junit.Assert.assertEquals;
16  import static org.junit.Assert.assertTrue;
17  import static org.junit.Assert.fail;
18  import static org.junit.Assume.assumeTrue;
19  
20  import java.io.File;
21  import java.io.FileInputStream;
22  import java.io.IOException;
23  import java.io.PrintWriter;
24  import java.nio.file.Files;
25  import java.util.Set;
26  
27  import org.eclipse.jgit.api.ResetCommand.ResetType;
28  import org.eclipse.jgit.api.errors.FilterFailedException;
29  import org.eclipse.jgit.api.errors.GitAPIException;
30  import org.eclipse.jgit.api.errors.NoFilepatternException;
31  import org.eclipse.jgit.attributes.FilterCommandRegistry;
32  import org.eclipse.jgit.dircache.DirCache;
33  import org.eclipse.jgit.dircache.DirCacheBuilder;
34  import org.eclipse.jgit.dircache.DirCacheEntry;
35  import org.eclipse.jgit.junit.JGitTestUtil;
36  import org.eclipse.jgit.junit.RepositoryTestCase;
37  import org.eclipse.jgit.lfs.BuiltinLFS;
38  import org.eclipse.jgit.lib.ConfigConstants;
39  import org.eclipse.jgit.lib.Constants;
40  import org.eclipse.jgit.lib.CoreConfig.SymLinks;
41  import org.eclipse.jgit.lib.FileMode;
42  import org.eclipse.jgit.lib.ObjectId;
43  import org.eclipse.jgit.lib.ObjectInserter;
44  import org.eclipse.jgit.lib.Repository;
45  import org.eclipse.jgit.lib.StoredConfig;
46  import org.eclipse.jgit.revwalk.RevCommit;
47  import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
48  import org.eclipse.jgit.treewalk.TreeWalk;
49  import org.eclipse.jgit.treewalk.WorkingTreeOptions;
50  import org.eclipse.jgit.util.FS;
51  import org.eclipse.jgit.util.FileUtils;
52  import org.junit.Test;
53  import org.junit.experimental.theories.DataPoints;
54  import org.junit.experimental.theories.Theories;
55  import org.junit.experimental.theories.Theory;
56  import org.junit.runner.RunWith;
57  
58  @RunWith(Theories.class)
59  public class AddCommandTest extends RepositoryTestCase {
60  	@DataPoints
61  	public static boolean[] sleepBeforeAddOptions = { true, false };
62  
63  
64  	@Override
65  	public void setUp() throws Exception {
66  		BuiltinLFS.register();
67  		super.setUp();
68  	}
69  
70  	@Test
71  	public void testAddNothing() throws GitAPIException {
72  		try (Git git = new Git(db)) {
73  			git.add().call();
74  			fail("Expected IllegalArgumentException");
75  		} catch (NoFilepatternException e) {
76  			// expected
77  		}
78  
79  	}
80  
81  	@Test
82  	public void testAddNonExistingSingleFile() throws GitAPIException {
83  		try (Git git = new Git(db)) {
84  			DirCache dc = git.add().addFilepattern("a.txt").call();
85  			assertEquals(0, dc.getEntryCount());
86  		}
87  	}
88  
89  	@Test
90  	public void testAddExistingSingleFile() throws IOException, GitAPIException {
91  		File file = new File(db.getWorkTree(), "a.txt");
92  		FileUtils.createNewFile(file);
93  		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
94  			writer.print("content");
95  		}
96  
97  		try (Git git = new Git(db)) {
98  			git.add().addFilepattern("a.txt").call();
99  
100 			assertEquals(
101 					"[a.txt, mode:100644, content:content]",
102 					indexState(CONTENT));
103 		}
104 	}
105 
106 	@Test
107 	public void testAddLink() throws IOException, GitAPIException {
108 		assumeTrue(db.getFS().supportsSymlinks());
109 		try (Git git = new Git(db)) {
110 			writeTrashFile("a.txt", "a");
111 			File link = new File(db.getWorkTree(), "link");
112 			db.getFS().createSymLink(link, "a.txt");
113 			git.add().addFilepattern(".").call();
114 			assertEquals(
115 					"[a.txt, mode:100644, content:a][link, mode:120000, content:a.txt]",
116 					indexState(CONTENT));
117 			git.commit().setMessage("link").call();
118 			StoredConfig config = db.getConfig();
119 			config.setEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
120 					ConfigConstants.CONFIG_KEY_SYMLINKS, SymLinks.FALSE);
121 			config.save();
122 			Files.delete(link.toPath());
123 			git.reset().setMode(ResetType.HARD).call();
124 			assertTrue(Files.isRegularFile(link.toPath()));
125 			assertEquals(
126 					"[a.txt, mode:100644, content:a][link, mode:120000, content:a.txt]",
127 					indexState(CONTENT));
128 			writeTrashFile("link", "b.txt");
129 			git.add().addFilepattern("link").call();
130 			assertEquals(
131 					"[a.txt, mode:100644, content:a][link, mode:120000, content:b.txt]",
132 					indexState(CONTENT));
133 			config.setEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
134 					ConfigConstants.CONFIG_KEY_SYMLINKS, SymLinks.TRUE);
135 			config.save();
136 			git.add().addFilepattern("link").call();
137 			assertEquals(
138 					"[a.txt, mode:100644, content:a][link, mode:100644, content:b.txt]",
139 					indexState(CONTENT));
140 		}
141 	}
142 
143 	@Test
144 	public void testCleanFilter() throws IOException, GitAPIException {
145 		writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
146 		writeTrashFile("src/a.tmp", "foo");
147 		// Caution: we need a trailing '\n' since sed on mac always appends
148 		// linefeeds if missing
149 		writeTrashFile("src/a.txt", "foo\n");
150 		File script = writeTempFile("sed s/o/e/g");
151 
152 		try (Git git = new Git(db)) {
153 			StoredConfig config = git.getRepository().getConfig();
154 			config.setString("filter", "tstFilter", "clean",
155 					"sh " + slashify(script.getPath()));
156 			config.save();
157 
158 			git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp")
159 					.call();
160 
161 			assertEquals(
162 					"[src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:fee\n]",
163 					indexState(CONTENT));
164 		}
165 	}
166 
167 	@Theory
168 	public void testBuiltinFilters(boolean sleepBeforeAdd)
169 			throws IOException,
170 			GitAPIException, InterruptedException {
171 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
172 		writeTrashFile("src/a.tmp", "foo");
173 		// Caution: we need a trailing '\n' since sed on mac always appends
174 		// linefeeds if missing
175 		File script = writeTempFile("sed s/o/e/g");
176 		File f = writeTrashFile("src/a.txt", "foo\n");
177 
178 		try (Git git = new Git(db)) {
179 			if (!sleepBeforeAdd) {
180 				fsTick(f);
181 			}
182 			git.add().addFilepattern(".gitattributes").call();
183 			StoredConfig config = git.getRepository().getConfig();
184 			config.setString("filter", "lfs", "clean",
185 					"sh " + slashify(script.getPath()));
186 			config.setString("filter", "lfs", "smudge",
187 					"sh " + slashify(script.getPath()));
188 			config.setBoolean("filter", "lfs", "useJGitBuiltin", true);
189 			config.save();
190 
191 			if (!sleepBeforeAdd) {
192 				fsTick(f);
193 			}
194 			git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp")
195 					.addFilepattern(".gitattributes").call();
196 
197 			assertEquals(
198 					"[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\nsize 4\n]",
199 					indexState(CONTENT));
200 
201 			RevCommit c1 = git.commit().setMessage("c1").call();
202 			assertTrue(git.status().call().isClean());
203 			f = writeTrashFile("src/a.txt", "foobar\n");
204 			if (!sleepBeforeAdd) {
205 				fsTick(f);
206 			}
207 			git.add().addFilepattern("src/a.txt").call();
208 			git.commit().setMessage("c2").call();
209 			assertTrue(git.status().call().isClean());
210 			assertEquals(
211 					"[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f\nsize 7\n]",
212 					indexState(CONTENT));
213 			assertEquals("foobar\n", read("src/a.txt"));
214 			git.checkout().setName(c1.getName()).call();
215 			assertEquals(
216 					"[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\nsize 4\n]",
217 					indexState(CONTENT));
218 			assertEquals(
219 					"foo\n", read("src/a.txt"));
220 		}
221 	}
222 
223 	@Theory
224 	public void testBuiltinCleanFilter(boolean sleepBeforeAdd)
225 			throws IOException, GitAPIException, InterruptedException {
226 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
227 		writeTrashFile("src/a.tmp", "foo");
228 		// Caution: we need a trailing '\n' since sed on mac always appends
229 		// linefeeds if missing
230 		File script = writeTempFile("sed s/o/e/g");
231 		File f = writeTrashFile("src/a.txt", "foo\n");
232 
233 		// unregister the smudge filter. Only clean filter should be builtin
234 		FilterCommandRegistry.unregister(
235 				org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX
236 						+ "lfs/smudge");
237 
238 		try (Git git = new Git(db)) {
239 			if (!sleepBeforeAdd) {
240 				fsTick(f);
241 			}
242 			git.add().addFilepattern(".gitattributes").call();
243 			StoredConfig config = git.getRepository().getConfig();
244 			config.setString("filter", "lfs", "clean",
245 					"sh " + slashify(script.getPath()));
246 			config.setString("filter", "lfs", "smudge",
247 					"sh " + slashify(script.getPath()));
248 			config.setBoolean("filter", "lfs", "useJGitBuiltin", true);
249 			config.save();
250 
251 			if (!sleepBeforeAdd) {
252 				fsTick(f);
253 			}
254 			git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp")
255 					.addFilepattern(".gitattributes").call();
256 
257 			assertEquals(
258 					"[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\nsize 4\n]",
259 					indexState(CONTENT));
260 
261 			RevCommit c1 = git.commit().setMessage("c1").call();
262 			assertTrue(git.status().call().isClean());
263 			f = writeTrashFile("src/a.txt", "foobar\n");
264 			if (!sleepBeforeAdd) {
265 				fsTick(f);
266 			}
267 			git.add().addFilepattern("src/a.txt").call();
268 			git.commit().setMessage("c2").call();
269 			assertTrue(git.status().call().isClean());
270 			assertEquals(
271 					"[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f\nsize 7\n]",
272 					indexState(CONTENT));
273 			assertEquals("foobar\n", read("src/a.txt"));
274 			git.checkout().setName(c1.getName()).call();
275 			assertEquals(
276 					"[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\nsize 4\n]",
277 					indexState(CONTENT));
278 			// due to lfs clean filter but dummy smudge filter we expect strange
279 			// content. The smudge filter converts from real content to pointer
280 			// file content (starting with "version ") but the smudge filter
281 			// replaces 'o' by 'e' which results in a text starting with
282 			// "versien "
283 			assertEquals(
284 					"versien https://git-lfs.github.cem/spec/v1\neid sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\nsize 4\n",
285 					read("src/a.txt"));
286 		}
287 	}
288 
289 	@Test
290 	public void testAttributesWithTreeWalkFilter()
291 			throws IOException, GitAPIException {
292 		writeTrashFile(".gitattributes", "*.txt filter=lfs");
293 		writeTrashFile("src/a.tmp", "foo");
294 		writeTrashFile("src/a.txt", "foo\n");
295 		File script = writeTempFile("sed s/o/e/g");
296 
297 		try (Git git = new Git(db)) {
298 			StoredConfig config = git.getRepository().getConfig();
299 			config.setString("filter", "lfs", "clean",
300 					"sh " + slashify(script.getPath()));
301 			config.save();
302 
303 			git.add().addFilepattern(".gitattributes").call();
304 			git.commit().setMessage("attr").call();
305 			git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp")
306 					.addFilepattern(".gitattributes").call();
307 			git.commit().setMessage("c1").call();
308 			assertTrue(git.status().call().isClean());
309 		}
310 	}
311 
312 	@Test
313 	public void testAttributesConflictingMatch() throws Exception {
314 		writeTrashFile(".gitattributes", "foo/** crlf=input\n*.jar binary");
315 		writeTrashFile("foo/bar.jar", "\r\n");
316 		// We end up with attributes [binary -diff -merge -text crlf=input].
317 		// crlf should have no effect when -text is present.
318 		try (Git git = new Git(db)) {
319 			git.add().addFilepattern(".").call();
320 			assertEquals(
321 					"[.gitattributes, mode:100644, content:foo/** crlf=input\n*.jar binary]"
322 							+ "[foo/bar.jar, mode:100644, content:\r\n]",
323 					indexState(CONTENT));
324 		}
325 	}
326 
327 	@Test
328 	public void testCleanFilterEnvironment()
329 			throws IOException, GitAPIException {
330 		writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
331 		writeTrashFile("src/a.txt", "foo");
332 		File script = writeTempFile("echo $GIT_DIR; echo 1 >xyz");
333 
334 		try (Git git = new Git(db)) {
335 			StoredConfig config = git.getRepository().getConfig();
336 			config.setString("filter", "tstFilter", "clean",
337 					"sh " + slashify(script.getPath()));
338 			config.save();
339 			git.add().addFilepattern("src/a.txt").call();
340 
341 			String gitDir = db.getDirectory().getAbsolutePath();
342 			assertEquals("[src/a.txt, mode:100644, content:" + gitDir
343 					+ "\n]", indexState(CONTENT));
344 			assertTrue(new File(db.getWorkTree(), "xyz").exists());
345 		}
346 	}
347 
348 	@Test
349 	public void testMultipleCleanFilter() throws IOException, GitAPIException {
350 		writeTrashFile(".gitattributes",
351 				"*.txt filter=tstFilter\n*.tmp filter=tstFilter2");
352 		// Caution: we need a trailing '\n' since sed on mac always appends
353 		// linefeeds if missing
354 		writeTrashFile("src/a.tmp", "foo\n");
355 		writeTrashFile("src/a.txt", "foo\n");
356 		File script = writeTempFile("sed s/o/e/g");
357 		File script2 = writeTempFile("sed s/f/x/g");
358 
359 		try (Git git = new Git(db)) {
360 			StoredConfig config = git.getRepository().getConfig();
361 			config.setString("filter", "tstFilter", "clean",
362 					"sh " + slashify(script.getPath()));
363 			config.setString("filter", "tstFilter2", "clean",
364 					"sh " + slashify(script2.getPath()));
365 			config.save();
366 
367 			git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp")
368 					.call();
369 
370 			assertEquals(
371 					"[src/a.tmp, mode:100644, content:xoo\n][src/a.txt, mode:100644, content:fee\n]",
372 					indexState(CONTENT));
373 
374 			// TODO: multiple clean filters for one file???
375 		}
376 	}
377 
378 	/**
379 	 * The path of an added file name contains ';' and afterwards malicious
380 	 * commands. Make sure when calling filter commands to properly escape the
381 	 * filenames
382 	 *
383 	 * @throws IOException
384 	 * @throws GitAPIException
385 	 */
386 	@Test
387 	public void testCommandInjection() throws IOException, GitAPIException {
388 		// Caution: we need a trailing '\n' since sed on mac always appends
389 		// linefeeds if missing
390 		writeTrashFile("; echo virus", "foo\n");
391 		File script = writeTempFile("sed s/o/e/g");
392 
393 		try (Git git = new Git(db)) {
394 			StoredConfig config = git.getRepository().getConfig();
395 			config.setString("filter", "tstFilter", "clean",
396 					"sh " + slashify(script.getPath()) + " %f");
397 			writeTrashFile(".gitattributes", "* filter=tstFilter");
398 
399 			git.add().addFilepattern("; echo virus").call();
400 			// Without proper escaping the content would be "feovirus". The sed
401 			// command and the "echo virus" would contribute to the content
402 			assertEquals("[; echo virus, mode:100644, content:fee\n]",
403 					indexState(CONTENT));
404 		}
405 	}
406 
407 	@Test
408 	public void testBadCleanFilter() throws IOException, GitAPIException {
409 		writeTrashFile("a.txt", "foo");
410 		File script = writeTempFile("sedfoo s/o/e/g");
411 
412 		try (Git git = new Git(db)) {
413 			StoredConfig config = git.getRepository().getConfig();
414 			config.setString("filter", "tstFilter", "clean",
415 					"sh " + script.getPath());
416 			config.save();
417 			writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
418 
419 			try {
420 				git.add().addFilepattern("a.txt").call();
421 				fail("Didn't received the expected exception");
422 			} catch (FilterFailedException e) {
423 				assertEquals(127, e.getReturnCode());
424 			}
425 		}
426 	}
427 
428 	@Test
429 	public void testBadCleanFilter2() throws IOException, GitAPIException {
430 		writeTrashFile("a.txt", "foo");
431 		File script = writeTempFile("sed s/o/e/g");
432 
433 		try (Git git = new Git(db)) {
434 			StoredConfig config = git.getRepository().getConfig();
435 			config.setString("filter", "tstFilter", "clean",
436 					"shfoo " + script.getPath());
437 			config.save();
438 			writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
439 
440 			try {
441 				git.add().addFilepattern("a.txt").call();
442 				fail("Didn't received the expected exception");
443 			} catch (FilterFailedException e) {
444 				assertEquals(127, e.getReturnCode());
445 			}
446 		}
447 	}
448 
449 	@Test
450 	public void testCleanFilterReturning12() throws IOException,
451 			GitAPIException {
452 		writeTrashFile("a.txt", "foo");
453 		File script = writeTempFile("exit 12");
454 
455 		try (Git git = new Git(db)) {
456 			StoredConfig config = git.getRepository().getConfig();
457 			config.setString("filter", "tstFilter", "clean",
458 					"sh " + slashify(script.getPath()));
459 			config.save();
460 			writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
461 
462 			try {
463 				git.add().addFilepattern("a.txt").call();
464 				fail("Didn't received the expected exception");
465 			} catch (FilterFailedException e) {
466 				assertEquals(12, e.getReturnCode());
467 			}
468 		}
469 	}
470 
471 	@Test
472 	public void testNotApplicableFilter() throws IOException, GitAPIException {
473 		writeTrashFile("a.txt", "foo");
474 		File script = writeTempFile("sed s/o/e/g");
475 
476 		try (Git git = new Git(db)) {
477 			StoredConfig config = git.getRepository().getConfig();
478 			config.setString("filter", "tstFilter", "something",
479 					"sh " + script.getPath());
480 			config.save();
481 			writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
482 
483 			git.add().addFilepattern("a.txt").call();
484 
485 			assertEquals("[a.txt, mode:100644, content:foo]",
486 					indexState(CONTENT));
487 		}
488 	}
489 
490 	private File writeTempFile(String body) throws IOException {
491 		File f = File.createTempFile("AddCommandTest_", "");
492 		JGitTestUtil.write(f, body);
493 		return f;
494 	}
495 
496 	@Test
497 	public void testAddExistingSingleSmallFileWithNewLine() throws IOException,
498 			GitAPIException {
499 		File file = new File(db.getWorkTree(), "a.txt");
500 		FileUtils.createNewFile(file);
501 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
502 			writer.print("row1\r\nrow2");
503 		}
504 
505 		try (Git git = new Git(db)) {
506 			db.getConfig().setString("core", null, "autocrlf", "false");
507 			git.add().addFilepattern("a.txt").call();
508 			assertEquals("[a.txt, mode:100644, content:row1\r\nrow2]",
509 					indexState(CONTENT));
510 			db.getConfig().setString("core", null, "autocrlf", "true");
511 			git.add().addFilepattern("a.txt").call();
512 			assertEquals("[a.txt, mode:100644, content:row1\r\nrow2]",
513 					indexState(CONTENT));
514 			db.getConfig().setString("core", null, "autocrlf", "input");
515 			git.add().addFilepattern("a.txt").call();
516 			assertEquals("[a.txt, mode:100644, content:row1\r\nrow2]",
517 					indexState(CONTENT));
518 		}
519 	}
520 
521 	@Test
522 	public void testAddExistingSingleMediumSizeFileWithNewLine()
523 			throws IOException, GitAPIException {
524 		File file = new File(db.getWorkTree(), "a.txt");
525 		FileUtils.createNewFile(file);
526 		StringBuilder data = new StringBuilder();
527 		for (int i = 0; i < 1000; ++i) {
528 			data.append("row1\r\nrow2");
529 		}
530 		String crData = data.toString();
531 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
532 			writer.print(crData);
533 		}
534 		try (Git git = new Git(db)) {
535 			db.getConfig().setString("core", null, "autocrlf", "false");
536 			git.add().addFilepattern("a.txt").call();
537 			assertEquals("[a.txt, mode:100644, content:" + crData + "]",
538 					indexState(CONTENT));
539 			db.getConfig().setString("core", null, "autocrlf", "true");
540 			git.add().addFilepattern("a.txt").call();
541 			assertEquals("[a.txt, mode:100644, content:" + crData + "]",
542 					indexState(CONTENT));
543 			db.getConfig().setString("core", null, "autocrlf", "input");
544 			git.add().addFilepattern("a.txt").call();
545 			assertEquals("[a.txt, mode:100644, content:" + crData + "]",
546 					indexState(CONTENT));
547 		}
548 	}
549 
550 	@Test
551 	public void testAddExistingSingleBinaryFile() throws IOException,
552 			GitAPIException {
553 		File file = new File(db.getWorkTree(), "a.txt");
554 		FileUtils.createNewFile(file);
555 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
556 			writer.print("row1\r\nrow2\u0000");
557 		}
558 
559 		try (Git git = new Git(db)) {
560 			db.getConfig().setString("core", null, "autocrlf", "false");
561 			git.add().addFilepattern("a.txt").call();
562 			assertEquals("[a.txt, mode:100644, content:row1\r\nrow2\u0000]",
563 					indexState(CONTENT));
564 			db.getConfig().setString("core", null, "autocrlf", "true");
565 			git.add().addFilepattern("a.txt").call();
566 			assertEquals("[a.txt, mode:100644, content:row1\r\nrow2\u0000]",
567 					indexState(CONTENT));
568 			db.getConfig().setString("core", null, "autocrlf", "input");
569 			git.add().addFilepattern("a.txt").call();
570 			assertEquals("[a.txt, mode:100644, content:row1\r\nrow2\u0000]",
571 					indexState(CONTENT));
572 		}
573 	}
574 
575 	@Test
576 	public void testAddExistingSingleFileInSubDir() throws IOException,
577 			GitAPIException {
578 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
579 		File file = new File(db.getWorkTree(), "sub/a.txt");
580 		FileUtils.createNewFile(file);
581 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
582 			writer.print("content");
583 		}
584 
585 		try (Git git = new Git(db)) {
586 			git.add().addFilepattern("sub/a.txt").call();
587 
588 			assertEquals(
589 					"[sub/a.txt, mode:100644, content:content]",
590 					indexState(CONTENT));
591 		}
592 	}
593 
594 	@Test
595 	public void testAddExistingSingleFileTwice() throws IOException,
596 			GitAPIException {
597 		File file = new File(db.getWorkTree(), "a.txt");
598 		FileUtils.createNewFile(file);
599 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
600 			writer.print("content");
601 		}
602 
603 		try (Git git = new Git(db)) {
604 			DirCache dc = git.add().addFilepattern("a.txt").call();
605 
606 			dc.getEntry(0).getObjectId();
607 
608 			try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
609 				writer.print("other content");
610 			}
611 
612 			dc = git.add().addFilepattern("a.txt").call();
613 
614 			assertEquals(
615 					"[a.txt, mode:100644, content:other content]",
616 					indexState(CONTENT));
617 		}
618 	}
619 
620 	@Test
621 	public void testAddExistingSingleFileTwiceWithCommit() throws Exception {
622 		File file = new File(db.getWorkTree(), "a.txt");
623 		FileUtils.createNewFile(file);
624 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
625 			writer.print("content");
626 		}
627 
628 		try (Git git = new Git(db)) {
629 			DirCache dc = git.add().addFilepattern("a.txt").call();
630 
631 			dc.getEntry(0).getObjectId();
632 
633 			git.commit().setMessage("commit a.txt").call();
634 
635 			try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
636 				writer.print("other content");
637 			}
638 
639 			dc = git.add().addFilepattern("a.txt").call();
640 
641 			assertEquals(
642 					"[a.txt, mode:100644, content:other content]",
643 					indexState(CONTENT));
644 		}
645 	}
646 
647 	@Test
648 	public void testAddRemovedFile() throws Exception {
649 		File file = new File(db.getWorkTree(), "a.txt");
650 		FileUtils.createNewFile(file);
651 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
652 			writer.print("content");
653 		}
654 
655 		try (Git git = new Git(db)) {
656 			DirCache dc = git.add().addFilepattern("a.txt").call();
657 
658 			dc.getEntry(0).getObjectId();
659 			FileUtils.delete(file);
660 
661 			// is supposed to do nothing
662 			dc = git.add().addFilepattern("a.txt").call();
663 
664 			assertEquals(
665 					"[a.txt, mode:100644, content:content]",
666 					indexState(CONTENT));
667 		}
668 	}
669 
670 	@Test
671 	public void testAddRemovedCommittedFile() throws Exception {
672 		File file = new File(db.getWorkTree(), "a.txt");
673 		FileUtils.createNewFile(file);
674 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
675 			writer.print("content");
676 		}
677 
678 		try (Git git = new Git(db)) {
679 			DirCache dc = git.add().addFilepattern("a.txt").call();
680 
681 			git.commit().setMessage("commit a.txt").call();
682 
683 			dc.getEntry(0).getObjectId();
684 			FileUtils.delete(file);
685 
686 			// is supposed to do nothing
687 			dc = git.add().addFilepattern("a.txt").call();
688 
689 			assertEquals(
690 					"[a.txt, mode:100644, content:content]",
691 					indexState(CONTENT));
692 		}
693 	}
694 
695 	@Test
696 	public void testAddWithConflicts() throws Exception {
697 		// prepare conflict
698 
699 		File file = new File(db.getWorkTree(), "a.txt");
700 		FileUtils.createNewFile(file);
701 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
702 			writer.print("content");
703 		}
704 
705 		File file2 = new File(db.getWorkTree(), "b.txt");
706 		FileUtils.createNewFile(file2);
707 		try (PrintWriter writer = new PrintWriter(file2, UTF_8.name())) {
708 			writer.print("content b");
709 		}
710 
711 		DirCache dc = db.lockDirCache();
712 		try (ObjectInserter newObjectInserter = db.newObjectInserter()) {
713 			DirCacheBuilder builder = dc.builder();
714 
715 			addEntryToBuilder("b.txt", file2, newObjectInserter, builder, 0);
716 			addEntryToBuilder("a.txt", file, newObjectInserter, builder, 1);
717 
718 			try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
719 				writer.print("other content");
720 			}
721 			addEntryToBuilder("a.txt", file, newObjectInserter, builder, 3);
722 
723 			try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
724 				writer.print("our content");
725 			}
726 			addEntryToBuilder("a.txt", file, newObjectInserter, builder, 2)
727 					.getObjectId();
728 
729 			builder.commit();
730 		}
731 		assertEquals(
732 				"[a.txt, mode:100644, stage:1, content:content]" +
733 				"[a.txt, mode:100644, stage:2, content:our content]" +
734 				"[a.txt, mode:100644, stage:3, content:other content]" +
735 				"[b.txt, mode:100644, content:content b]",
736 				indexState(CONTENT));
737 
738 		// now the test begins
739 
740 		try (Git git = new Git(db)) {
741 			dc = git.add().addFilepattern("a.txt").call();
742 
743 			assertEquals(
744 					"[a.txt, mode:100644, content:our content]" +
745 					"[b.txt, mode:100644, content:content b]",
746 					indexState(CONTENT));
747 		}
748 	}
749 
750 	@Test
751 	public void testAddTwoFiles() throws Exception  {
752 		File file = new File(db.getWorkTree(), "a.txt");
753 		FileUtils.createNewFile(file);
754 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
755 			writer.print("content");
756 		}
757 
758 		File file2 = new File(db.getWorkTree(), "b.txt");
759 		FileUtils.createNewFile(file2);
760 		try (PrintWriter writer = new PrintWriter(file2, UTF_8.name())) {
761 			writer.print("content b");
762 		}
763 
764 		try (Git git = new Git(db)) {
765 			git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
766 			assertEquals(
767 					"[a.txt, mode:100644, content:content]" +
768 					"[b.txt, mode:100644, content:content b]",
769 					indexState(CONTENT));
770 		}
771 	}
772 
773 	@Test
774 	public void testAddFolder() throws Exception  {
775 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
776 		File file = new File(db.getWorkTree(), "sub/a.txt");
777 		FileUtils.createNewFile(file);
778 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
779 			writer.print("content");
780 		}
781 
782 		File file2 = new File(db.getWorkTree(), "sub/b.txt");
783 		FileUtils.createNewFile(file2);
784 		try (PrintWriter writer = new PrintWriter(file2, UTF_8.name())) {
785 			writer.print("content b");
786 		}
787 
788 		try (Git git = new Git(db)) {
789 			git.add().addFilepattern("sub").call();
790 			assertEquals(
791 					"[sub/a.txt, mode:100644, content:content]" +
792 					"[sub/b.txt, mode:100644, content:content b]",
793 					indexState(CONTENT));
794 		}
795 	}
796 
797 	@Test
798 	public void testAddIgnoredFile() throws Exception  {
799 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
800 		File file = new File(db.getWorkTree(), "sub/a.txt");
801 		FileUtils.createNewFile(file);
802 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
803 			writer.print("content");
804 		}
805 
806 		File ignoreFile = new File(db.getWorkTree(), ".gitignore");
807 		FileUtils.createNewFile(ignoreFile);
808 		try (PrintWriter writer = new PrintWriter(ignoreFile, UTF_8.name())) {
809 			writer.print("sub/b.txt");
810 		}
811 
812 		File file2 = new File(db.getWorkTree(), "sub/b.txt");
813 		FileUtils.createNewFile(file2);
814 		try (PrintWriter writer = new PrintWriter(file2, UTF_8.name())) {
815 			writer.print("content b");
816 		}
817 
818 		try (Git git = new Git(db)) {
819 			git.add().addFilepattern("sub").call();
820 
821 			assertEquals(
822 					"[sub/a.txt, mode:100644, content:content]",
823 					indexState(CONTENT));
824 		}
825 	}
826 
827 	@Test
828 	public void testAddWholeRepo() throws Exception  {
829 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
830 		File file = new File(db.getWorkTree(), "sub/a.txt");
831 		FileUtils.createNewFile(file);
832 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
833 			writer.print("content");
834 		}
835 
836 		File file2 = new File(db.getWorkTree(), "sub/b.txt");
837 		FileUtils.createNewFile(file2);
838 		try (PrintWriter writer = new PrintWriter(file2, UTF_8.name())) {
839 			writer.print("content b");
840 		}
841 
842 		try (Git git = new Git(db)) {
843 			git.add().addFilepattern(".").call();
844 			assertEquals(
845 					"[sub/a.txt, mode:100644, content:content]" +
846 					"[sub/b.txt, mode:100644, content:content b]",
847 					indexState(CONTENT));
848 		}
849 	}
850 
851 	// the same three cases as in testAddWithParameterUpdate
852 	// file a exists in workdir and in index -> added
853 	// file b exists not in workdir but in index -> unchanged
854 	// file c exists in workdir but not in index -> added
855 	@Test
856 	public void testAddWithoutParameterUpdate() throws Exception {
857 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
858 		File file = new File(db.getWorkTree(), "sub/a.txt");
859 		FileUtils.createNewFile(file);
860 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
861 			writer.print("content");
862 		}
863 
864 		File file2 = new File(db.getWorkTree(), "sub/b.txt");
865 		FileUtils.createNewFile(file2);
866 		try (PrintWriter writer = new PrintWriter(file2, UTF_8.name())) {
867 			writer.print("content b");
868 		}
869 
870 		try (Git git = new Git(db)) {
871 			git.add().addFilepattern("sub").call();
872 
873 			assertEquals(
874 					"[sub/a.txt, mode:100644, content:content]" +
875 					"[sub/b.txt, mode:100644, content:content b]",
876 					indexState(CONTENT));
877 
878 			git.commit().setMessage("commit").call();
879 
880 			// new unstaged file sub/c.txt
881 			File file3 = new File(db.getWorkTree(), "sub/c.txt");
882 			FileUtils.createNewFile(file3);
883 			try (PrintWriter writer = new PrintWriter(file3, UTF_8.name())) {
884 				writer.print("content c");
885 			}
886 
887 			// file sub/a.txt is modified
888 			try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
889 				writer.print("modified content");
890 			}
891 
892 			// file sub/b.txt is deleted
893 			FileUtils.delete(file2);
894 
895 			git.add().addFilepattern("sub").call();
896 			// change in sub/a.txt is staged
897 			// deletion of sub/b.txt is not staged
898 			// sub/c.txt is staged
899 			assertEquals(
900 					"[sub/a.txt, mode:100644, content:modified content]" +
901 					"[sub/b.txt, mode:100644, content:content b]" +
902 					"[sub/c.txt, mode:100644, content:content c]",
903 					indexState(CONTENT));
904 		}
905 	}
906 
907 	// file a exists in workdir and in index -> added
908 	// file b exists not in workdir but in index -> deleted
909 	// file c exists in workdir but not in index -> unchanged
910 	@Test
911 	public void testAddWithParameterUpdate() throws Exception {
912 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
913 		File file = new File(db.getWorkTree(), "sub/a.txt");
914 		FileUtils.createNewFile(file);
915 		try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
916 			writer.print("content");
917 		}
918 
919 		File file2 = new File(db.getWorkTree(), "sub/b.txt");
920 		FileUtils.createNewFile(file2);
921 		try (PrintWriter writer = new PrintWriter(file2, UTF_8.name())) {
922 			writer.print("content b");
923 		}
924 
925 		try (Git git = new Git(db)) {
926 			git.add().addFilepattern("sub").call();
927 
928 			assertEquals(
929 					"[sub/a.txt, mode:100644, content:content]" +
930 					"[sub/b.txt, mode:100644, content:content b]",
931 					indexState(CONTENT));
932 
933 			git.commit().setMessage("commit").call();
934 
935 			// new unstaged file sub/c.txt
936 			File file3 = new File(db.getWorkTree(), "sub/c.txt");
937 			FileUtils.createNewFile(file3);
938 			try (PrintWriter writer = new PrintWriter(file3, UTF_8.name())) {
939 				writer.print("content c");
940 			}
941 
942 			// file sub/a.txt is modified
943 			try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
944 				writer.print("modified content");
945 			}
946 
947 			FileUtils.delete(file2);
948 
949 			// change in sub/a.txt is staged
950 			// deletion of sub/b.txt is staged
951 			// sub/c.txt is not staged
952 			git.add().addFilepattern("sub").setUpdate(true).call();
953 			// change in sub/a.txt is staged
954 			assertEquals(
955 					"[sub/a.txt, mode:100644, content:modified content]",
956 					indexState(CONTENT));
957 		}
958 	}
959 
960 	@Test
961 	public void testAssumeUnchanged() throws Exception {
962 		try (Git git = new Git(db)) {
963 			String path = "a.txt";
964 			writeTrashFile(path, "content");
965 			git.add().addFilepattern(path).call();
966 			String path2 = "b.txt";
967 			writeTrashFile(path2, "content");
968 			git.add().addFilepattern(path2).call();
969 			git.commit().setMessage("commit").call();
970 			assertEquals("[a.txt, mode:100644, content:"
971 					+ "content, assume-unchanged:false]"
972 					+ "[b.txt, mode:100644, content:content, "
973 					+ "assume-unchanged:false]", indexState(CONTENT
974 					| ASSUME_UNCHANGED));
975 			assumeUnchanged(path2);
976 			assertEquals("[a.txt, mode:100644, content:content, "
977 					+ "assume-unchanged:false][b.txt, mode:100644, "
978 					+ "content:content, assume-unchanged:true]", indexState(CONTENT
979 					| ASSUME_UNCHANGED));
980 			writeTrashFile(path, "more content");
981 			writeTrashFile(path2, "more content");
982 
983 			git.add().addFilepattern(".").call();
984 
985 			assertEquals("[a.txt, mode:100644, content:more content,"
986 					+ " assume-unchanged:false][b.txt, mode:100644,"
987 					+ " content:content, assume-unchanged:true]",
988 					indexState(CONTENT
989 					| ASSUME_UNCHANGED));
990 		}
991 	}
992 
993 	@Test
994 	public void testReplaceFileWithDirectory()
995 			throws IOException, NoFilepatternException, GitAPIException {
996 		try (Git git = new Git(db)) {
997 			writeTrashFile("df", "before replacement");
998 			git.add().addFilepattern("df").call();
999 			assertEquals("[df, mode:100644, content:before replacement]",
1000 					indexState(CONTENT));
1001 			FileUtils.delete(new File(db.getWorkTree(), "df"));
1002 			writeTrashFile("df/f", "after replacement");
1003 			git.add().addFilepattern("df").call();
1004 			assertEquals("[df/f, mode:100644, content:after replacement]",
1005 					indexState(CONTENT));
1006 		}
1007 	}
1008 
1009 	@Test
1010 	public void testReplaceDirectoryWithFile()
1011 			throws IOException, NoFilepatternException, GitAPIException {
1012 		try (Git git = new Git(db)) {
1013 			writeTrashFile("df/f", "before replacement");
1014 			git.add().addFilepattern("df").call();
1015 			assertEquals("[df/f, mode:100644, content:before replacement]",
1016 					indexState(CONTENT));
1017 			FileUtils.delete(new File(db.getWorkTree(), "df"), RECURSIVE);
1018 			writeTrashFile("df", "after replacement");
1019 			git.add().addFilepattern("df").call();
1020 			assertEquals("[df, mode:100644, content:after replacement]",
1021 					indexState(CONTENT));
1022 		}
1023 	}
1024 
1025 	@Test
1026 	public void testReplaceFileByPartOfDirectory()
1027 			throws IOException, NoFilepatternException, GitAPIException {
1028 		try (Git git = new Git(db)) {
1029 			writeTrashFile("src/main", "df", "before replacement");
1030 			writeTrashFile("src/main", "z", "z");
1031 			writeTrashFile("z", "z2");
1032 			git.add().addFilepattern("src/main/df")
1033 				.addFilepattern("src/main/z")
1034 				.addFilepattern("z")
1035 				.call();
1036 			assertEquals(
1037 					"[src/main/df, mode:100644, content:before replacement]" +
1038 					"[src/main/z, mode:100644, content:z]" +
1039 					"[z, mode:100644, content:z2]",
1040 					indexState(CONTENT));
1041 			FileUtils.delete(new File(db.getWorkTree(), "src/main/df"));
1042 			writeTrashFile("src/main/df", "a", "after replacement");
1043 			writeTrashFile("src/main/df", "b", "unrelated file");
1044 			git.add().addFilepattern("src/main/df/a").call();
1045 			assertEquals(
1046 					"[src/main/df/a, mode:100644, content:after replacement]" +
1047 					"[src/main/z, mode:100644, content:z]" +
1048 					"[z, mode:100644, content:z2]",
1049 					indexState(CONTENT));
1050 		}
1051 	}
1052 
1053 	@Test
1054 	public void testReplaceDirectoryConflictsWithFile()
1055 			throws IOException, NoFilepatternException, GitAPIException {
1056 		DirCache dc = db.lockDirCache();
1057 		try (ObjectInserter oi = db.newObjectInserter()) {
1058 			DirCacheBuilder builder = dc.builder();
1059 			File f = writeTrashFile("a", "df", "content");
1060 			addEntryToBuilder("a", f, oi, builder, 1);
1061 
1062 			f = writeTrashFile("a", "df", "other content");
1063 			addEntryToBuilder("a/df", f, oi, builder, 3);
1064 
1065 			f = writeTrashFile("a", "df", "our content");
1066 			addEntryToBuilder("a/df", f, oi, builder, 2);
1067 
1068 			f = writeTrashFile("z", "z");
1069 			addEntryToBuilder("z", f, oi, builder, 0);
1070 			builder.commit();
1071 		}
1072 		assertEquals(
1073 				"[a, mode:100644, stage:1, content:content]" +
1074 				"[a/df, mode:100644, stage:2, content:our content]" +
1075 				"[a/df, mode:100644, stage:3, content:other content]" +
1076 				"[z, mode:100644, content:z]",
1077 				indexState(CONTENT));
1078 
1079 		try (Git git = new Git(db)) {
1080 			FileUtils.delete(new File(db.getWorkTree(), "a"), RECURSIVE);
1081 			writeTrashFile("a", "merged");
1082 			git.add().addFilepattern("a").call();
1083 			assertEquals("[a, mode:100644, content:merged]" +
1084 					"[z, mode:100644, content:z]",
1085 					indexState(CONTENT));
1086 		}
1087 	}
1088 
1089 	@Test
1090 	public void testExecutableRetention() throws Exception {
1091 		StoredConfig config = db.getConfig();
1092 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
1093 				ConfigConstants.CONFIG_KEY_FILEMODE, true);
1094 		config.save();
1095 
1096 		FS executableFs = new FS() {
1097 
1098 			@Override
1099 			public boolean supportsExecute() {
1100 				return true;
1101 			}
1102 
1103 			@Override
1104 			public boolean setExecute(File f, boolean canExec) {
1105 				return true;
1106 			}
1107 
1108 			@Override
1109 			public ProcessBuilder runInShell(String cmd, String[] args) {
1110 				return null;
1111 			}
1112 
1113 			@Override
1114 			public boolean retryFailedLockFileCommit() {
1115 				return false;
1116 			}
1117 
1118 			@Override
1119 			public FS newInstance() {
1120 				return this;
1121 			}
1122 
1123 			@Override
1124 			protected File discoverGitExe() {
1125 				return null;
1126 			}
1127 
1128 			@Override
1129 			public boolean canExecute(File f) {
1130 				try {
1131 					return read(f).startsWith("binary:");
1132 				} catch (IOException e) {
1133 					return false;
1134 				}
1135 			}
1136 
1137 			@Override
1138 			public boolean isCaseSensitive() {
1139 				return false;
1140 			}
1141 		};
1142 
1143 		String path = "a.txt";
1144 		String path2 = "a.sh";
1145 		writeTrashFile(path, "content");
1146 		writeTrashFile(path2, "binary: content");
1147 		try (Git git = Git.open(db.getDirectory(), executableFs)) {
1148 			git.add().addFilepattern(path).addFilepattern(path2).call();
1149 			RevCommit commit1 = git.commit().setMessage("commit").call();
1150 			try (TreeWalk walk = new TreeWalk(db)) {
1151 				walk.addTree(commit1.getTree());
1152 				walk.next();
1153 				assertEquals(path2, walk.getPathString());
1154 				assertEquals(FileMode.EXECUTABLE_FILE, walk.getFileMode(0));
1155 				walk.next();
1156 				assertEquals(path, walk.getPathString());
1157 				assertEquals(FileMode.REGULAR_FILE, walk.getFileMode(0));
1158 			}
1159 		}
1160 		config = db.getConfig();
1161 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
1162 				ConfigConstants.CONFIG_KEY_FILEMODE, false);
1163 		config.save();
1164 
1165 		writeTrashFile(path2, "content2");
1166 		writeTrashFile(path, "binary: content2");
1167 		try (Git git2 = Git.open(db.getDirectory(), executableFs)) {
1168 			git2.add().addFilepattern(path).addFilepattern(path2).call();
1169 			RevCommit commit2 = git2.commit().setMessage("commit2").call();
1170 			try (TreeWalk walk = new TreeWalk(db)) {
1171 				walk.addTree(commit2.getTree());
1172 				walk.next();
1173 				assertEquals(path2, walk.getPathString());
1174 				assertEquals(FileMode.EXECUTABLE_FILE, walk.getFileMode(0));
1175 				walk.next();
1176 				assertEquals(path, walk.getPathString());
1177 				assertEquals(FileMode.REGULAR_FILE, walk.getFileMode(0));
1178 			}
1179 		}
1180 	}
1181 
1182 	@Test
1183 	public void testAddGitlink() throws Exception {
1184 		createNestedRepo("git-link-dir");
1185 		try (Git git = new Git(db)) {
1186 			git.add().addFilepattern("git-link-dir").call();
1187 
1188 			assertEquals(
1189 					"[git-link-dir, mode:160000]",
1190 					indexState(0));
1191 			Set<String> untrackedFiles = git.status().call().getUntracked();
1192 			assert (untrackedFiles.isEmpty());
1193 		}
1194 
1195 	}
1196 
1197 	@Test
1198 	public void testAddSubrepoWithDirNoGitlinks() throws Exception {
1199 		createNestedRepo("nested-repo");
1200 
1201 		// Set DIR_NO_GITLINKS
1202 		StoredConfig config = db.getConfig();
1203 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
1204 				ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, true);
1205 		config.save();
1206 
1207 		assert (db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks());
1208 
1209 		try (Git git = new Git(db)) {
1210 			git.add().addFilepattern("nested-repo").call();
1211 
1212 			assertEquals(
1213 					"[nested-repo/README1.md, mode:100644]" +
1214 							"[nested-repo/README2.md, mode:100644]",
1215 					indexState(0));
1216 		}
1217 
1218 		// Turn off DIR_NO_GITLINKS, ensure nested-repo is still treated as
1219 		// a normal directory
1220 		// Set DIR_NO_GITLINKS
1221 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
1222 				ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, false);
1223 		config.save();
1224 
1225 		writeTrashFile("nested-repo", "README3.md", "content");
1226 
1227 		try (Git git = new Git(db)) {
1228 			git.add().addFilepattern("nested-repo").call();
1229 
1230 			assertEquals(
1231 					"[nested-repo/README1.md, mode:100644]" +
1232 							"[nested-repo/README2.md, mode:100644]" +
1233 							"[nested-repo/README3.md, mode:100644]",
1234 					indexState(0));
1235 		}
1236 	}
1237 
1238 	@Test
1239 	public void testAddGitlinkDoesNotChange() throws Exception {
1240 		createNestedRepo("nested-repo");
1241 
1242 		try (Git git = new Git(db)) {
1243 			git.add().addFilepattern("nested-repo").call();
1244 
1245 			assertEquals(
1246 					"[nested-repo, mode:160000]",
1247 					indexState(0));
1248 		}
1249 
1250 		// Set DIR_NO_GITLINKS
1251 		StoredConfig config = db.getConfig();
1252 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
1253 				ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, true);
1254 		config.save();
1255 
1256 		assertTrue(
1257 				db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks());
1258 
1259 		try (Git git = new Git(db)) {
1260 			git.add().addFilepattern("nested-repo").call();
1261 			// with gitlinks ignored, we treat this as a normal directory
1262 			assertEquals(
1263 					"[nested-repo/README1.md, mode:100644][nested-repo/README2.md, mode:100644]",
1264 					indexState(0));
1265 		}
1266 	}
1267 
1268 	private static DirCacheEntry addEntryToBuilder(String path, File file,
1269 			ObjectInserter newObjectInserter, DirCacheBuilder builder, int stage)
1270 			throws IOException {
1271 		ObjectId id;
1272 		try (FileInputStream inputStream = new FileInputStream(file)) {
1273 			id = newObjectInserter.insert(
1274 				Constants.OBJ_BLOB, file.length(), inputStream);
1275 		}
1276 		DirCacheEntry entry = new DirCacheEntry(path, stage);
1277 		entry.setObjectId(id);
1278 		entry.setFileMode(FileMode.REGULAR_FILE);
1279 		entry.setLastModified(FS.DETECTED.lastModifiedInstant(file));
1280 		entry.setLength((int) file.length());
1281 
1282 		builder.add(entry);
1283 		return entry;
1284 	}
1285 
1286 	private void assumeUnchanged(String path) throws IOException {
1287 		final DirCache dirc = db.lockDirCache();
1288 		final DirCacheEntry ent = dirc.getEntry(path);
1289 		if (ent != null)
1290 			ent.setAssumeValid(true);
1291 		dirc.write();
1292 		if (!dirc.commit())
1293 			throw new IOException("could not commit");
1294 	}
1295 
1296 	private void createNestedRepo(String path) throws IOException {
1297 		File gitLinkDir = new File(db.getWorkTree(), path);
1298 		FileUtils.mkdir(gitLinkDir);
1299 
1300 		FileRepositoryBuilder nestedBuilder = new FileRepositoryBuilder();
1301 		nestedBuilder.setWorkTree(gitLinkDir);
1302 
1303 		try (Repository nestedRepo = nestedBuilder.build()) {
1304 			nestedRepo.create();
1305 
1306 			writeTrashFile(path, "README1.md", "content");
1307 			writeTrashFile(path, "README2.md", "content");
1308 
1309 			// Commit these changes in the subrepo
1310 			try (Git git = new Git(nestedRepo)) {
1311 				git.add().addFilepattern(".").call();
1312 				git.commit().setMessage("subrepo commit").call();
1313 			} catch (GitAPIException e) {
1314 				throw new RuntimeException(e);
1315 			}
1316 		}
1317 	}
1318 }