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