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