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