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 testAttributesConflictingMatch() throws Exception {
307 		writeTrashFile(".gitattributes", "foo/** crlf=input\n*.jar binary");
308 		writeTrashFile("foo/bar.jar", "\r\n");
309 		// We end up with attributes [binary -diff -merge -text crlf=input].
310 		// crlf should have no effect when -text is present.
311 		try (Git git = new Git(db)) {
312 			git.add().addFilepattern(".").call();
313 			assertEquals(
314 					"[.gitattributes, mode:100644, content:foo/** crlf=input\n*.jar binary]"
315 							+ "[foo/bar.jar, mode:100644, content:\r\n]",
316 					indexState(CONTENT));
317 		}
318 	}
319 
320 	@Test
321 	public void testCleanFilterEnvironment()
322 			throws IOException, GitAPIException {
323 		writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
324 		writeTrashFile("src/a.txt", "foo");
325 		File script = writeTempFile("echo $GIT_DIR; echo 1 >xyz");
326 
327 		try (Git git = new Git(db)) {
328 			StoredConfig config = git.getRepository().getConfig();
329 			config.setString("filter", "tstFilter", "clean",
330 					"sh " + slashify(script.getPath()));
331 			config.save();
332 			git.add().addFilepattern("src/a.txt").call();
333 
334 			String gitDir = db.getDirectory().getAbsolutePath();
335 			assertEquals("[src/a.txt, mode:100644, content:" + gitDir
336 					+ "\n]", indexState(CONTENT));
337 			assertTrue(new File(db.getWorkTree(), "xyz").exists());
338 		}
339 	}
340 
341 	@Test
342 	public void testMultipleCleanFilter() throws IOException, GitAPIException {
343 		writeTrashFile(".gitattributes",
344 				"*.txt filter=tstFilter\n*.tmp filter=tstFilter2");
345 		// Caution: we need a trailing '\n' since sed on mac always appends
346 		// linefeeds if missing
347 		writeTrashFile("src/a.tmp", "foo\n");
348 		writeTrashFile("src/a.txt", "foo\n");
349 		File script = writeTempFile("sed s/o/e/g");
350 		File script2 = writeTempFile("sed s/f/x/g");
351 
352 		try (Git git = new Git(db)) {
353 			StoredConfig config = git.getRepository().getConfig();
354 			config.setString("filter", "tstFilter", "clean",
355 					"sh " + slashify(script.getPath()));
356 			config.setString("filter", "tstFilter2", "clean",
357 					"sh " + slashify(script2.getPath()));
358 			config.save();
359 
360 			git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp")
361 					.call();
362 
363 			assertEquals(
364 					"[src/a.tmp, mode:100644, content:xoo\n][src/a.txt, mode:100644, content:fee\n]",
365 					indexState(CONTENT));
366 
367 			// TODO: multiple clean filters for one file???
368 		}
369 	}
370 
371 	/**
372 	 * The path of an added file name contains ';' and afterwards malicious
373 	 * commands. Make sure when calling filter commands to properly escape the
374 	 * filenames
375 	 *
376 	 * @throws IOException
377 	 * @throws GitAPIException
378 	 */
379 	@Test
380 	public void testCommandInjection() throws IOException, GitAPIException {
381 		// Caution: we need a trailing '\n' since sed on mac always appends
382 		// linefeeds if missing
383 		writeTrashFile("; echo virus", "foo\n");
384 		File script = writeTempFile("sed s/o/e/g");
385 
386 		try (Git git = new Git(db)) {
387 			StoredConfig config = git.getRepository().getConfig();
388 			config.setString("filter", "tstFilter", "clean",
389 					"sh " + slashify(script.getPath()) + " %f");
390 			writeTrashFile(".gitattributes", "* filter=tstFilter");
391 
392 			git.add().addFilepattern("; echo virus").call();
393 			// Without proper escaping the content would be "feovirus". The sed
394 			// command and the "echo virus" would contribute to the content
395 			assertEquals("[; echo virus, mode:100644, content:fee\n]",
396 					indexState(CONTENT));
397 		}
398 	}
399 
400 	@Test
401 	public void testBadCleanFilter() throws IOException, GitAPIException {
402 		writeTrashFile("a.txt", "foo");
403 		File script = writeTempFile("sedfoo s/o/e/g");
404 
405 		try (Git git = new Git(db)) {
406 			StoredConfig config = git.getRepository().getConfig();
407 			config.setString("filter", "tstFilter", "clean",
408 					"sh " + script.getPath());
409 			config.save();
410 			writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
411 
412 			try {
413 				git.add().addFilepattern("a.txt").call();
414 				fail("Didn't received the expected exception");
415 			} catch (FilterFailedException e) {
416 				assertEquals(127, e.getReturnCode());
417 			}
418 		}
419 	}
420 
421 	@Test
422 	public void testBadCleanFilter2() throws IOException, GitAPIException {
423 		writeTrashFile("a.txt", "foo");
424 		File script = writeTempFile("sed s/o/e/g");
425 
426 		try (Git git = new Git(db)) {
427 			StoredConfig config = git.getRepository().getConfig();
428 			config.setString("filter", "tstFilter", "clean",
429 					"shfoo " + script.getPath());
430 			config.save();
431 			writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
432 
433 			try {
434 				git.add().addFilepattern("a.txt").call();
435 				fail("Didn't received the expected exception");
436 			} catch (FilterFailedException e) {
437 				assertEquals(127, e.getReturnCode());
438 			}
439 		}
440 	}
441 
442 	@Test
443 	public void testCleanFilterReturning12() throws IOException,
444 			GitAPIException {
445 		writeTrashFile("a.txt", "foo");
446 		File script = writeTempFile("exit 12");
447 
448 		try (Git git = new Git(db)) {
449 			StoredConfig config = git.getRepository().getConfig();
450 			config.setString("filter", "tstFilter", "clean",
451 					"sh " + slashify(script.getPath()));
452 			config.save();
453 			writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
454 
455 			try {
456 				git.add().addFilepattern("a.txt").call();
457 				fail("Didn't received the expected exception");
458 			} catch (FilterFailedException e) {
459 				assertEquals(12, e.getReturnCode());
460 			}
461 		}
462 	}
463 
464 	@Test
465 	public void testNotApplicableFilter() throws IOException, GitAPIException {
466 		writeTrashFile("a.txt", "foo");
467 		File script = writeTempFile("sed s/o/e/g");
468 
469 		try (Git git = new Git(db)) {
470 			StoredConfig config = git.getRepository().getConfig();
471 			config.setString("filter", "tstFilter", "something",
472 					"sh " + script.getPath());
473 			config.save();
474 			writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
475 
476 			git.add().addFilepattern("a.txt").call();
477 
478 			assertEquals("[a.txt, mode:100644, content:foo]",
479 					indexState(CONTENT));
480 		}
481 	}
482 
483 	private File writeTempFile(String body) throws IOException {
484 		File f = File.createTempFile("AddCommandTest_", "");
485 		JGitTestUtil.write(f, body);
486 		return f;
487 	}
488 
489 	@Test
490 	public void testAddExistingSingleSmallFileWithNewLine() throws IOException,
491 			GitAPIException {
492 		File file = new File(db.getWorkTree(), "a.txt");
493 		FileUtils.createNewFile(file);
494 		PrintWriter writer = new PrintWriter(file);
495 		writer.print("row1\r\nrow2");
496 		writer.close();
497 
498 		try (Git git = new Git(db)) {
499 			db.getConfig().setString("core", null, "autocrlf", "false");
500 			git.add().addFilepattern("a.txt").call();
501 			assertEquals("[a.txt, mode:100644, content:row1\r\nrow2]",
502 					indexState(CONTENT));
503 			db.getConfig().setString("core", null, "autocrlf", "true");
504 			git.add().addFilepattern("a.txt").call();
505 			assertEquals("[a.txt, mode:100644, content:row1\nrow2]",
506 					indexState(CONTENT));
507 			db.getConfig().setString("core", null, "autocrlf", "input");
508 			git.add().addFilepattern("a.txt").call();
509 			assertEquals("[a.txt, mode:100644, content:row1\nrow2]",
510 					indexState(CONTENT));
511 		}
512 	}
513 
514 	@Test
515 	public void testAddExistingSingleMediumSizeFileWithNewLine()
516 			throws IOException, GitAPIException {
517 		File file = new File(db.getWorkTree(), "a.txt");
518 		FileUtils.createNewFile(file);
519 		StringBuilder data = new StringBuilder();
520 		for (int i = 0; i < 1000; ++i) {
521 			data.append("row1\r\nrow2");
522 		}
523 		String crData = data.toString();
524 		PrintWriter writer = new PrintWriter(file);
525 		writer.print(crData);
526 		writer.close();
527 		String lfData = data.toString().replaceAll("\r", "");
528 		try (Git git = new Git(db)) {
529 			db.getConfig().setString("core", null, "autocrlf", "false");
530 			git.add().addFilepattern("a.txt").call();
531 			assertEquals("[a.txt, mode:100644, content:" + data + "]",
532 					indexState(CONTENT));
533 			db.getConfig().setString("core", null, "autocrlf", "true");
534 			git.add().addFilepattern("a.txt").call();
535 			assertEquals("[a.txt, mode:100644, content:" + lfData + "]",
536 					indexState(CONTENT));
537 			db.getConfig().setString("core", null, "autocrlf", "input");
538 			git.add().addFilepattern("a.txt").call();
539 			assertEquals("[a.txt, mode:100644, content:" + lfData + "]",
540 					indexState(CONTENT));
541 		}
542 	}
543 
544 	@Test
545 	public void testAddExistingSingleBinaryFile() throws IOException,
546 			GitAPIException {
547 		File file = new File(db.getWorkTree(), "a.txt");
548 		FileUtils.createNewFile(file);
549 		PrintWriter writer = new PrintWriter(file);
550 		writer.print("row1\r\nrow2\u0000");
551 		writer.close();
552 
553 		try (Git git = new Git(db)) {
554 			db.getConfig().setString("core", null, "autocrlf", "false");
555 			git.add().addFilepattern("a.txt").call();
556 			assertEquals("[a.txt, mode:100644, content:row1\r\nrow2\u0000]",
557 					indexState(CONTENT));
558 			db.getConfig().setString("core", null, "autocrlf", "true");
559 			git.add().addFilepattern("a.txt").call();
560 			assertEquals("[a.txt, mode:100644, content:row1\r\nrow2\u0000]",
561 					indexState(CONTENT));
562 			db.getConfig().setString("core", null, "autocrlf", "input");
563 			git.add().addFilepattern("a.txt").call();
564 			assertEquals("[a.txt, mode:100644, content:row1\r\nrow2\u0000]",
565 					indexState(CONTENT));
566 		}
567 	}
568 
569 	@Test
570 	public void testAddExistingSingleFileInSubDir() throws IOException,
571 			GitAPIException {
572 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
573 		File file = new File(db.getWorkTree(), "sub/a.txt");
574 		FileUtils.createNewFile(file);
575 		PrintWriter writer = new PrintWriter(file);
576 		writer.print("content");
577 		writer.close();
578 
579 		try (Git git = new Git(db)) {
580 			git.add().addFilepattern("sub/a.txt").call();
581 
582 			assertEquals(
583 					"[sub/a.txt, mode:100644, content:content]",
584 					indexState(CONTENT));
585 		}
586 	}
587 
588 	@Test
589 	public void testAddExistingSingleFileTwice() throws IOException,
590 			GitAPIException {
591 		File file = new File(db.getWorkTree(), "a.txt");
592 		FileUtils.createNewFile(file);
593 		PrintWriter writer = new PrintWriter(file);
594 		writer.print("content");
595 		writer.close();
596 
597 		try (Git git = new Git(db)) {
598 			DirCache dc = git.add().addFilepattern("a.txt").call();
599 
600 			dc.getEntry(0).getObjectId();
601 
602 			writer = new PrintWriter(file);
603 			writer.print("other content");
604 			writer.close();
605 
606 			dc = git.add().addFilepattern("a.txt").call();
607 
608 			assertEquals(
609 					"[a.txt, mode:100644, content:other content]",
610 					indexState(CONTENT));
611 		}
612 	}
613 
614 	@Test
615 	public void testAddExistingSingleFileTwiceWithCommit() throws Exception {
616 		File file = new File(db.getWorkTree(), "a.txt");
617 		FileUtils.createNewFile(file);
618 		PrintWriter writer = new PrintWriter(file);
619 		writer.print("content");
620 		writer.close();
621 
622 		try (Git git = new Git(db)) {
623 			DirCache dc = git.add().addFilepattern("a.txt").call();
624 
625 			dc.getEntry(0).getObjectId();
626 
627 			git.commit().setMessage("commit a.txt").call();
628 
629 			writer = new PrintWriter(file);
630 			writer.print("other content");
631 			writer.close();
632 
633 			dc = git.add().addFilepattern("a.txt").call();
634 
635 			assertEquals(
636 					"[a.txt, mode:100644, content:other content]",
637 					indexState(CONTENT));
638 		}
639 	}
640 
641 	@Test
642 	public void testAddRemovedFile() throws Exception {
643 		File file = new File(db.getWorkTree(), "a.txt");
644 		FileUtils.createNewFile(file);
645 		PrintWriter writer = new PrintWriter(file);
646 		writer.print("content");
647 		writer.close();
648 
649 		try (Git git = new Git(db)) {
650 			DirCache dc = git.add().addFilepattern("a.txt").call();
651 
652 			dc.getEntry(0).getObjectId();
653 			FileUtils.delete(file);
654 
655 			// is supposed to do nothing
656 			dc = git.add().addFilepattern("a.txt").call();
657 
658 			assertEquals(
659 					"[a.txt, mode:100644, content:content]",
660 					indexState(CONTENT));
661 		}
662 	}
663 
664 	@Test
665 	public void testAddRemovedCommittedFile() throws Exception {
666 		File file = new File(db.getWorkTree(), "a.txt");
667 		FileUtils.createNewFile(file);
668 		PrintWriter writer = new PrintWriter(file);
669 		writer.print("content");
670 		writer.close();
671 
672 		try (Git git = new Git(db)) {
673 			DirCache dc = git.add().addFilepattern("a.txt").call();
674 
675 			git.commit().setMessage("commit a.txt").call();
676 
677 			dc.getEntry(0).getObjectId();
678 			FileUtils.delete(file);
679 
680 			// is supposed to do nothing
681 			dc = git.add().addFilepattern("a.txt").call();
682 
683 			assertEquals(
684 					"[a.txt, mode:100644, content:content]",
685 					indexState(CONTENT));
686 		}
687 	}
688 
689 	@Test
690 	public void testAddWithConflicts() throws Exception {
691 		// prepare conflict
692 
693 		File file = new File(db.getWorkTree(), "a.txt");
694 		FileUtils.createNewFile(file);
695 		PrintWriter writer = new PrintWriter(file);
696 		writer.print("content");
697 		writer.close();
698 
699 		File file2 = new File(db.getWorkTree(), "b.txt");
700 		FileUtils.createNewFile(file2);
701 		writer = new PrintWriter(file2);
702 		writer.print("content b");
703 		writer.close();
704 
705 		ObjectInserter newObjectInserter = db.newObjectInserter();
706 		DirCache dc = db.lockDirCache();
707 		DirCacheBuilder builder = dc.builder();
708 
709 		addEntryToBuilder("b.txt", file2, newObjectInserter, builder, 0);
710 		addEntryToBuilder("a.txt", file, newObjectInserter, builder, 1);
711 
712 		writer = new PrintWriter(file);
713 		writer.print("other content");
714 		writer.close();
715 		addEntryToBuilder("a.txt", file, newObjectInserter, builder, 3);
716 
717 		writer = new PrintWriter(file);
718 		writer.print("our content");
719 		writer.close();
720 		addEntryToBuilder("a.txt", file, newObjectInserter, builder, 2)
721 				.getObjectId();
722 
723 		builder.commit();
724 
725 		assertEquals(
726 				"[a.txt, mode:100644, stage:1, content:content]" +
727 				"[a.txt, mode:100644, stage:2, content:our content]" +
728 				"[a.txt, mode:100644, stage:3, content:other content]" +
729 				"[b.txt, mode:100644, content:content b]",
730 				indexState(CONTENT));
731 
732 		// now the test begins
733 
734 		try (Git git = new Git(db)) {
735 			dc = git.add().addFilepattern("a.txt").call();
736 
737 			assertEquals(
738 					"[a.txt, mode:100644, content:our content]" +
739 					"[b.txt, mode:100644, content:content b]",
740 					indexState(CONTENT));
741 		}
742 	}
743 
744 	@Test
745 	public void testAddTwoFiles() throws Exception  {
746 		File file = new File(db.getWorkTree(), "a.txt");
747 		FileUtils.createNewFile(file);
748 		PrintWriter writer = new PrintWriter(file);
749 		writer.print("content");
750 		writer.close();
751 
752 		File file2 = new File(db.getWorkTree(), "b.txt");
753 		FileUtils.createNewFile(file2);
754 		writer = new PrintWriter(file2);
755 		writer.print("content b");
756 		writer.close();
757 
758 		try (Git git = new Git(db)) {
759 			git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
760 			assertEquals(
761 					"[a.txt, mode:100644, content:content]" +
762 					"[b.txt, mode:100644, content:content b]",
763 					indexState(CONTENT));
764 		}
765 	}
766 
767 	@Test
768 	public void testAddFolder() throws Exception  {
769 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
770 		File file = new File(db.getWorkTree(), "sub/a.txt");
771 		FileUtils.createNewFile(file);
772 		PrintWriter writer = new PrintWriter(file);
773 		writer.print("content");
774 		writer.close();
775 
776 		File file2 = new File(db.getWorkTree(), "sub/b.txt");
777 		FileUtils.createNewFile(file2);
778 		writer = new PrintWriter(file2);
779 		writer.print("content b");
780 		writer.close();
781 
782 		try (Git git = new Git(db)) {
783 			git.add().addFilepattern("sub").call();
784 			assertEquals(
785 					"[sub/a.txt, mode:100644, content:content]" +
786 					"[sub/b.txt, mode:100644, content:content b]",
787 					indexState(CONTENT));
788 		}
789 	}
790 
791 	@Test
792 	public void testAddIgnoredFile() throws Exception  {
793 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
794 		File file = new File(db.getWorkTree(), "sub/a.txt");
795 		FileUtils.createNewFile(file);
796 		PrintWriter writer = new PrintWriter(file);
797 		writer.print("content");
798 		writer.close();
799 
800 		File ignoreFile = new File(db.getWorkTree(), ".gitignore");
801 		FileUtils.createNewFile(ignoreFile);
802 		writer = new PrintWriter(ignoreFile);
803 		writer.print("sub/b.txt");
804 		writer.close();
805 
806 		File file2 = new File(db.getWorkTree(), "sub/b.txt");
807 		FileUtils.createNewFile(file2);
808 		writer = new PrintWriter(file2);
809 		writer.print("content b");
810 		writer.close();
811 
812 		try (Git git = new Git(db)) {
813 			git.add().addFilepattern("sub").call();
814 
815 			assertEquals(
816 					"[sub/a.txt, mode:100644, content:content]",
817 					indexState(CONTENT));
818 		}
819 	}
820 
821 	@Test
822 	public void testAddWholeRepo() throws Exception  {
823 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
824 		File file = new File(db.getWorkTree(), "sub/a.txt");
825 		FileUtils.createNewFile(file);
826 		PrintWriter writer = new PrintWriter(file);
827 		writer.print("content");
828 		writer.close();
829 
830 		File file2 = new File(db.getWorkTree(), "sub/b.txt");
831 		FileUtils.createNewFile(file2);
832 		writer = new PrintWriter(file2);
833 		writer.print("content b");
834 		writer.close();
835 
836 		try (Git git = new Git(db)) {
837 			git.add().addFilepattern(".").call();
838 			assertEquals(
839 					"[sub/a.txt, mode:100644, content:content]" +
840 					"[sub/b.txt, mode:100644, content:content b]",
841 					indexState(CONTENT));
842 		}
843 	}
844 
845 	// the same three cases as in testAddWithParameterUpdate
846 	// file a exists in workdir and in index -> added
847 	// file b exists not in workdir but in index -> unchanged
848 	// file c exists in workdir but not in index -> added
849 	@Test
850 	public void testAddWithoutParameterUpdate() throws Exception {
851 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
852 		File file = new File(db.getWorkTree(), "sub/a.txt");
853 		FileUtils.createNewFile(file);
854 		PrintWriter writer = new PrintWriter(file);
855 		writer.print("content");
856 		writer.close();
857 
858 		File file2 = new File(db.getWorkTree(), "sub/b.txt");
859 		FileUtils.createNewFile(file2);
860 		writer = new PrintWriter(file2);
861 		writer.print("content b");
862 		writer.close();
863 
864 		try (Git git = new Git(db)) {
865 			git.add().addFilepattern("sub").call();
866 
867 			assertEquals(
868 					"[sub/a.txt, mode:100644, content:content]" +
869 					"[sub/b.txt, mode:100644, content:content b]",
870 					indexState(CONTENT));
871 
872 			git.commit().setMessage("commit").call();
873 
874 			// new unstaged file sub/c.txt
875 			File file3 = new File(db.getWorkTree(), "sub/c.txt");
876 			FileUtils.createNewFile(file3);
877 			writer = new PrintWriter(file3);
878 			writer.print("content c");
879 			writer.close();
880 
881 			// file sub/a.txt is modified
882 			writer = new PrintWriter(file);
883 			writer.print("modified content");
884 			writer.close();
885 
886 			// file sub/b.txt is deleted
887 			FileUtils.delete(file2);
888 
889 			git.add().addFilepattern("sub").call();
890 			// change in sub/a.txt is staged
891 			// deletion of sub/b.txt is not staged
892 			// sub/c.txt is staged
893 			assertEquals(
894 					"[sub/a.txt, mode:100644, content:modified content]" +
895 					"[sub/b.txt, mode:100644, content:content b]" +
896 					"[sub/c.txt, mode:100644, content:content c]",
897 					indexState(CONTENT));
898 		}
899 	}
900 
901 	// file a exists in workdir and in index -> added
902 	// file b exists not in workdir but in index -> deleted
903 	// file c exists in workdir but not in index -> unchanged
904 	@Test
905 	public void testAddWithParameterUpdate() throws Exception {
906 		FileUtils.mkdir(new File(db.getWorkTree(), "sub"));
907 		File file = new File(db.getWorkTree(), "sub/a.txt");
908 		FileUtils.createNewFile(file);
909 		PrintWriter writer = new PrintWriter(file);
910 		writer.print("content");
911 		writer.close();
912 
913 		File file2 = new File(db.getWorkTree(), "sub/b.txt");
914 		FileUtils.createNewFile(file2);
915 		writer = new PrintWriter(file2);
916 		writer.print("content b");
917 		writer.close();
918 
919 		try (Git git = new Git(db)) {
920 			git.add().addFilepattern("sub").call();
921 
922 			assertEquals(
923 					"[sub/a.txt, mode:100644, content:content]" +
924 					"[sub/b.txt, mode:100644, content:content b]",
925 					indexState(CONTENT));
926 
927 			git.commit().setMessage("commit").call();
928 
929 			// new unstaged file sub/c.txt
930 			File file3 = new File(db.getWorkTree(), "sub/c.txt");
931 			FileUtils.createNewFile(file3);
932 			writer = new PrintWriter(file3);
933 			writer.print("content c");
934 			writer.close();
935 
936 			// file sub/a.txt is modified
937 			writer = new PrintWriter(file);
938 			writer.print("modified content");
939 			writer.close();
940 
941 			FileUtils.delete(file2);
942 
943 			// change in sub/a.txt is staged
944 			// deletion of sub/b.txt is staged
945 			// sub/c.txt is not staged
946 			git.add().addFilepattern("sub").setUpdate(true).call();
947 			// change in sub/a.txt is staged
948 			assertEquals(
949 					"[sub/a.txt, mode:100644, content:modified content]",
950 					indexState(CONTENT));
951 		}
952 	}
953 
954 	@Test
955 	public void testAssumeUnchanged() throws Exception {
956 		try (Git git = new Git(db)) {
957 			String path = "a.txt";
958 			writeTrashFile(path, "content");
959 			git.add().addFilepattern(path).call();
960 			String path2 = "b.txt";
961 			writeTrashFile(path2, "content");
962 			git.add().addFilepattern(path2).call();
963 			git.commit().setMessage("commit").call();
964 			assertEquals("[a.txt, mode:100644, content:"
965 					+ "content, assume-unchanged:false]"
966 					+ "[b.txt, mode:100644, content:content, "
967 					+ "assume-unchanged:false]", indexState(CONTENT
968 					| ASSUME_UNCHANGED));
969 			assumeUnchanged(path2);
970 			assertEquals("[a.txt, mode:100644, content:content, "
971 					+ "assume-unchanged:false][b.txt, mode:100644, "
972 					+ "content:content, assume-unchanged:true]", indexState(CONTENT
973 					| ASSUME_UNCHANGED));
974 			writeTrashFile(path, "more content");
975 			writeTrashFile(path2, "more content");
976 
977 			git.add().addFilepattern(".").call();
978 
979 			assertEquals("[a.txt, mode:100644, content:more content,"
980 					+ " assume-unchanged:false][b.txt, mode:100644,"
981 					+ " content:content, assume-unchanged:true]",
982 					indexState(CONTENT
983 					| ASSUME_UNCHANGED));
984 		}
985 	}
986 
987 	@Test
988 	public void testReplaceFileWithDirectory()
989 			throws IOException, NoFilepatternException, GitAPIException {
990 		try (Git git = new Git(db)) {
991 			writeTrashFile("df", "before replacement");
992 			git.add().addFilepattern("df").call();
993 			assertEquals("[df, mode:100644, content:before replacement]",
994 					indexState(CONTENT));
995 			FileUtils.delete(new File(db.getWorkTree(), "df"));
996 			writeTrashFile("df/f", "after replacement");
997 			git.add().addFilepattern("df").call();
998 			assertEquals("[df/f, mode:100644, content:after replacement]",
999 					indexState(CONTENT));
1000 		}
1001 	}
1002 
1003 	@Test
1004 	public void testReplaceDirectoryWithFile()
1005 			throws IOException, NoFilepatternException, GitAPIException {
1006 		try (Git git = new Git(db)) {
1007 			writeTrashFile("df/f", "before replacement");
1008 			git.add().addFilepattern("df").call();
1009 			assertEquals("[df/f, mode:100644, content:before replacement]",
1010 					indexState(CONTENT));
1011 			FileUtils.delete(new File(db.getWorkTree(), "df"), RECURSIVE);
1012 			writeTrashFile("df", "after replacement");
1013 			git.add().addFilepattern("df").call();
1014 			assertEquals("[df, mode:100644, content:after replacement]",
1015 					indexState(CONTENT));
1016 		}
1017 	}
1018 
1019 	@Test
1020 	public void testReplaceFileByPartOfDirectory()
1021 			throws IOException, NoFilepatternException, GitAPIException {
1022 		try (Git git = new Git(db)) {
1023 			writeTrashFile("src/main", "df", "before replacement");
1024 			writeTrashFile("src/main", "z", "z");
1025 			writeTrashFile("z", "z2");
1026 			git.add().addFilepattern("src/main/df")
1027 				.addFilepattern("src/main/z")
1028 				.addFilepattern("z")
1029 				.call();
1030 			assertEquals(
1031 					"[src/main/df, mode:100644, content:before replacement]" +
1032 					"[src/main/z, mode:100644, content:z]" +
1033 					"[z, mode:100644, content:z2]",
1034 					indexState(CONTENT));
1035 			FileUtils.delete(new File(db.getWorkTree(), "src/main/df"));
1036 			writeTrashFile("src/main/df", "a", "after replacement");
1037 			writeTrashFile("src/main/df", "b", "unrelated file");
1038 			git.add().addFilepattern("src/main/df/a").call();
1039 			assertEquals(
1040 					"[src/main/df/a, mode:100644, content:after replacement]" +
1041 					"[src/main/z, mode:100644, content:z]" +
1042 					"[z, mode:100644, content:z2]",
1043 					indexState(CONTENT));
1044 		}
1045 	}
1046 
1047 	@Test
1048 	public void testReplaceDirectoryConflictsWithFile()
1049 			throws IOException, NoFilepatternException, GitAPIException {
1050 		DirCache dc = db.lockDirCache();
1051 		try (ObjectInserter oi = db.newObjectInserter()) {
1052 			DirCacheBuilder builder = dc.builder();
1053 			File f = writeTrashFile("a", "df", "content");
1054 			addEntryToBuilder("a", f, oi, builder, 1);
1055 
1056 			f = writeTrashFile("a", "df", "other content");
1057 			addEntryToBuilder("a/df", f, oi, builder, 3);
1058 
1059 			f = writeTrashFile("a", "df", "our content");
1060 			addEntryToBuilder("a/df", f, oi, builder, 2);
1061 
1062 			f = writeTrashFile("z", "z");
1063 			addEntryToBuilder("z", f, oi, builder, 0);
1064 			builder.commit();
1065 		}
1066 		assertEquals(
1067 				"[a, mode:100644, stage:1, content:content]" +
1068 				"[a/df, mode:100644, stage:2, content:our content]" +
1069 				"[a/df, mode:100644, stage:3, content:other content]" +
1070 				"[z, mode:100644, content:z]",
1071 				indexState(CONTENT));
1072 
1073 		try (Git git = new Git(db)) {
1074 			FileUtils.delete(new File(db.getWorkTree(), "a"), RECURSIVE);
1075 			writeTrashFile("a", "merged");
1076 			git.add().addFilepattern("a").call();
1077 			assertEquals("[a, mode:100644, content:merged]" +
1078 					"[z, mode:100644, content:z]",
1079 					indexState(CONTENT));
1080 		}
1081 	}
1082 
1083 	@Test
1084 	public void testExecutableRetention() throws Exception {
1085 		StoredConfig config = db.getConfig();
1086 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
1087 				ConfigConstants.CONFIG_KEY_FILEMODE, true);
1088 		config.save();
1089 
1090 		FS executableFs = new FS() {
1091 
1092 			@Override
1093 			public boolean supportsExecute() {
1094 				return true;
1095 			}
1096 
1097 			@Override
1098 			public boolean setExecute(File f, boolean canExec) {
1099 				return true;
1100 			}
1101 
1102 			@Override
1103 			public ProcessBuilder runInShell(String cmd, String[] args) {
1104 				return null;
1105 			}
1106 
1107 			@Override
1108 			public boolean retryFailedLockFileCommit() {
1109 				return false;
1110 			}
1111 
1112 			@Override
1113 			public FS newInstance() {
1114 				return this;
1115 			}
1116 
1117 			@Override
1118 			protected File discoverGitExe() {
1119 				return null;
1120 			}
1121 
1122 			@Override
1123 			public boolean canExecute(File f) {
1124 				try {
1125 					return read(f).startsWith("binary:");
1126 				} catch (IOException e) {
1127 					return false;
1128 				}
1129 			}
1130 
1131 			@Override
1132 			public boolean isCaseSensitive() {
1133 				return false;
1134 			}
1135 		};
1136 
1137 		Git git = Git.open(db.getDirectory(), executableFs);
1138 		String path = "a.txt";
1139 		String path2 = "a.sh";
1140 		writeTrashFile(path, "content");
1141 		writeTrashFile(path2, "binary: content");
1142 		git.add().addFilepattern(path).addFilepattern(path2).call();
1143 		RevCommit commit1 = git.commit().setMessage("commit").call();
1144 		try (TreeWalk walk = new TreeWalk(db)) {
1145 			walk.addTree(commit1.getTree());
1146 			walk.next();
1147 			assertEquals(path2, walk.getPathString());
1148 			assertEquals(FileMode.EXECUTABLE_FILE, walk.getFileMode(0));
1149 			walk.next();
1150 			assertEquals(path, walk.getPathString());
1151 			assertEquals(FileMode.REGULAR_FILE, walk.getFileMode(0));
1152 		}
1153 
1154 		config = db.getConfig();
1155 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
1156 				ConfigConstants.CONFIG_KEY_FILEMODE, false);
1157 		config.save();
1158 
1159 		Git git2 = Git.open(db.getDirectory(), executableFs);
1160 		writeTrashFile(path2, "content2");
1161 		writeTrashFile(path, "binary: content2");
1162 		git2.add().addFilepattern(path).addFilepattern(path2).call();
1163 		RevCommit commit2 = git2.commit().setMessage("commit2").call();
1164 		try (TreeWalk walk = new TreeWalk(db)) {
1165 			walk.addTree(commit2.getTree());
1166 			walk.next();
1167 			assertEquals(path2, walk.getPathString());
1168 			assertEquals(FileMode.EXECUTABLE_FILE, walk.getFileMode(0));
1169 			walk.next();
1170 			assertEquals(path, walk.getPathString());
1171 			assertEquals(FileMode.REGULAR_FILE, walk.getFileMode(0));
1172 		}
1173 	}
1174 
1175 	@Test
1176 	public void testAddGitlink() throws Exception {
1177 		createNestedRepo("git-link-dir");
1178 		try (Git git = new Git(db)) {
1179 			git.add().addFilepattern("git-link-dir").call();
1180 
1181 			assertEquals(
1182 					"[git-link-dir, mode:160000]",
1183 					indexState(0));
1184 			Set<String> untrackedFiles = git.status().call().getUntracked();
1185 			assert (untrackedFiles.isEmpty());
1186 		}
1187 
1188 	}
1189 
1190 	@Test
1191 	public void testAddSubrepoWithDirNoGitlinks() throws Exception {
1192 		createNestedRepo("nested-repo");
1193 
1194 		// Set DIR_NO_GITLINKS
1195 		StoredConfig config = db.getConfig();
1196 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
1197 				ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, true);
1198 		config.save();
1199 
1200 		assert (db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks());
1201 
1202 		try (Git git = new Git(db)) {
1203 			git.add().addFilepattern("nested-repo").call();
1204 
1205 			assertEquals(
1206 					"[nested-repo/README1.md, mode:100644]" +
1207 							"[nested-repo/README2.md, mode:100644]",
1208 					indexState(0));
1209 		}
1210 
1211 		// Turn off DIR_NO_GITLINKS, ensure nested-repo is still treated as
1212 		// a normal directory
1213 		// Set DIR_NO_GITLINKS
1214 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
1215 				ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, false);
1216 		config.save();
1217 
1218 		writeTrashFile("nested-repo", "README3.md", "content");
1219 
1220 		try (Git git = new Git(db)) {
1221 			git.add().addFilepattern("nested-repo").call();
1222 
1223 			assertEquals(
1224 					"[nested-repo/README1.md, mode:100644]" +
1225 							"[nested-repo/README2.md, mode:100644]" +
1226 							"[nested-repo/README3.md, mode:100644]",
1227 					indexState(0));
1228 		}
1229 	}
1230 
1231 	@Test
1232 	public void testAddGitlinkDoesNotChange() throws Exception {
1233 		createNestedRepo("nested-repo");
1234 
1235 		try (Git git = new Git(db)) {
1236 			git.add().addFilepattern("nested-repo").call();
1237 
1238 			assertEquals(
1239 					"[nested-repo, mode:160000]",
1240 					indexState(0));
1241 		}
1242 
1243 		// Set DIR_NO_GITLINKS
1244 		StoredConfig config = db.getConfig();
1245 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
1246 				ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, true);
1247 		config.save();
1248 
1249 		assert (db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks());
1250 
1251 		try (Git git = new Git(db)) {
1252 			git.add().addFilepattern("nested-repo").call();
1253 
1254 			assertEquals(
1255 					"[nested-repo, mode:160000]",
1256 					indexState(0));
1257 		}
1258 	}
1259 
1260 	private static DirCacheEntry addEntryToBuilder(String path, File file,
1261 			ObjectInserter newObjectInserter, DirCacheBuilder builder, int stage)
1262 			throws IOException {
1263 		FileInputStream inputStream = new FileInputStream(file);
1264 		ObjectId id = newObjectInserter.insert(
1265 				Constants.OBJ_BLOB, file.length(), inputStream);
1266 		inputStream.close();
1267 		DirCacheEntry entry = new DirCacheEntry(path, stage);
1268 		entry.setObjectId(id);
1269 		entry.setFileMode(FileMode.REGULAR_FILE);
1270 		entry.setLastModified(file.lastModified());
1271 		entry.setLength((int) file.length());
1272 
1273 		builder.add(entry);
1274 		return entry;
1275 	}
1276 
1277 	private void assumeUnchanged(String path) throws IOException {
1278 		final DirCache dirc = db.lockDirCache();
1279 		final DirCacheEntry ent = dirc.getEntry(path);
1280 		if (ent != null)
1281 			ent.setAssumeValid(true);
1282 		dirc.write();
1283 		if (!dirc.commit())
1284 			throw new IOException("could not commit");
1285 	}
1286 
1287 	private void createNestedRepo(String path) throws IOException {
1288 		File gitLinkDir = new File(db.getWorkTree(), path);
1289 		FileUtils.mkdir(gitLinkDir);
1290 
1291 		FileRepositoryBuilder nestedBuilder = new FileRepositoryBuilder();
1292 		nestedBuilder.setWorkTree(gitLinkDir);
1293 
1294 		Repository nestedRepo = nestedBuilder.build();
1295 		nestedRepo.create();
1296 
1297 		writeTrashFile(path, "README1.md", "content");
1298 		writeTrashFile(path, "README2.md", "content");
1299 
1300 		// Commit these changes in the subrepo
1301 		try (Git git = new Git(nestedRepo)) {
1302 			git.add().addFilepattern(".").call();
1303 			git.commit().setMessage("subrepo commit").call();
1304 		} catch (GitAPIException e) {
1305 			throw new RuntimeException(e);
1306 		}
1307 	}
1308 }