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