View Javadoc
1   /*
2    * Copyright (C) 2008, 2017, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.treewalk;
12  
13  import static java.nio.charset.StandardCharsets.UTF_8;
14  import static org.junit.Assert.assertEquals;
15  import static org.junit.Assert.assertFalse;
16  import static org.junit.Assert.assertNotNull;
17  import static org.junit.Assert.assertTrue;
18  import static org.junit.Assume.assumeNoException;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.nio.file.InvalidPathException;
23  import java.security.MessageDigest;
24  import java.time.Instant;
25  
26  import org.eclipse.jgit.api.Git;
27  import org.eclipse.jgit.api.ResetCommand.ResetType;
28  import org.eclipse.jgit.dircache.DirCache;
29  import org.eclipse.jgit.dircache.DirCacheCheckout;
30  import org.eclipse.jgit.dircache.DirCacheEditor;
31  import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
32  import org.eclipse.jgit.dircache.DirCacheEntry;
33  import org.eclipse.jgit.dircache.DirCacheIterator;
34  import org.eclipse.jgit.errors.CorruptObjectException;
35  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
36  import org.eclipse.jgit.errors.MissingObjectException;
37  import org.eclipse.jgit.junit.JGitTestUtil;
38  import org.eclipse.jgit.junit.RepositoryTestCase;
39  import org.eclipse.jgit.lib.ConfigConstants;
40  import org.eclipse.jgit.lib.Constants;
41  import org.eclipse.jgit.lib.FileMode;
42  import org.eclipse.jgit.lib.ObjectId;
43  import org.eclipse.jgit.lib.ObjectInserter;
44  import org.eclipse.jgit.lib.ObjectReader;
45  import org.eclipse.jgit.lib.Repository;
46  import org.eclipse.jgit.revwalk.RevCommit;
47  import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
48  import org.eclipse.jgit.treewalk.WorkingTreeIterator.MetadataDiff;
49  import org.eclipse.jgit.treewalk.filter.PathFilter;
50  import org.eclipse.jgit.util.FS;
51  import org.eclipse.jgit.util.FileUtils;
52  import org.eclipse.jgit.util.RawParseUtils;
53  import org.junit.Before;
54  import org.junit.Test;
55  
56  public class FileTreeIteratorTest extends RepositoryTestCase {
57  	private final String[] paths = { "a,", "a,b", "a/b", "a0b" };
58  
59  	private Instant[] mtime;
60  
61  	@Override
62  	@Before
63  	public void setUp() throws Exception {
64  		super.setUp();
65  
66  		// We build the entries backwards so that on POSIX systems we
67  		// are likely to get the entries in the trash directory in the
68  		// opposite order of what they should be in for the iteration.
69  		// This should stress the sorting code better than doing it in
70  		// the correct order.
71  		//
72  		mtime = new Instant[paths.length];
73  		for (int i = paths.length - 1; i >= 0; i--) {
74  			final String s = paths[i];
75  			writeTrashFile(s, s);
76  			mtime[i] = db.getFS().lastModifiedInstant(new File(trash, s));
77  		}
78  	}
79  
80  	@Test
81  	public void testGetEntryContentLength() throws Exception {
82  		final FileTreeIterator fti = new FileTreeIterator(db);
83  		fti.next(1);
84  		assertEquals(3, fti.getEntryContentLength());
85  		fti.back(1);
86  		assertEquals(2, fti.getEntryContentLength());
87  		fti.next(1);
88  		assertEquals(3, fti.getEntryContentLength());
89  		fti.reset();
90  		assertEquals(2, fti.getEntryContentLength());
91  	}
92  
93  	@Test
94  	public void testEmptyIfRootIsFile() throws Exception {
95  		final File r = new File(trash, paths[0]);
96  		assertTrue(r.isFile());
97  		final FileTreeIterator fti = new FileTreeIterator(r, db.getFS(),
98  				db.getConfig().get(WorkingTreeOptions.KEY));
99  		assertTrue(fti.first());
100 		assertTrue(fti.eof());
101 	}
102 
103 	@Test
104 	public void testEmptyIfRootDoesNotExist() throws Exception {
105 		final File r = new File(trash, "not-existing-file");
106 		assertFalse(r.exists());
107 		final FileTreeIterator fti = new FileTreeIterator(r, db.getFS(),
108 				db.getConfig().get(WorkingTreeOptions.KEY));
109 		assertTrue(fti.first());
110 		assertTrue(fti.eof());
111 	}
112 
113 	@Test
114 	public void testEmptyIfRootIsEmpty() throws Exception {
115 		final File r = new File(trash, "not-existing-file");
116 		assertFalse(r.exists());
117 		FileUtils.mkdir(r);
118 
119 		final FileTreeIterator fti = new FileTreeIterator(r, db.getFS(),
120 				db.getConfig().get(WorkingTreeOptions.KEY));
121 		assertTrue(fti.first());
122 		assertTrue(fti.eof());
123 	}
124 
125 	@Test
126 	public void testEmptyIteratorOnEmptyDirectory() throws Exception {
127 		String nonExistingFileName = "not-existing-file";
128 		final File r = new File(trash, nonExistingFileName);
129 		assertFalse(r.exists());
130 		FileUtils.mkdir(r);
131 
132 		final FileTreeIterator parent = new FileTreeIterator(db);
133 
134 		while (!parent.getEntryPathString().equals(nonExistingFileName))
135 			parent.next(1);
136 
137 		final FileTreeIterator childIter = new FileTreeIterator(parent, r,
138 				db.getFS());
139 		assertTrue(childIter.first());
140 		assertTrue(childIter.eof());
141 
142 		String parentPath = parent.getEntryPathString();
143 		assertEquals(nonExistingFileName, parentPath);
144 
145 		// must be "not-existing-file/", but getEntryPathString() was broken by
146 		// 445363 too
147 		String childPath = childIter.getEntryPathString();
148 
149 		// in bug 445363 the iterator wrote garbage to the parent "path" field
150 		EmptyTreeIterator e = childIter.createEmptyTreeIterator();
151 		assertNotNull(e);
152 
153 		// check if parent path is not overridden by empty iterator (bug 445363)
154 		// due bug 445363 this was "/ot-existing-file" instead of
155 		// "not-existing-file"
156 		assertEquals(parentPath, parent.getEntryPathString());
157 		assertEquals(parentPath + "/", childPath);
158 		assertEquals(parentPath + "/", childIter.getEntryPathString());
159 		assertEquals(childPath + "/", e.getEntryPathString());
160 	}
161 
162 	@Test
163 	public void testSimpleIterate() throws Exception {
164 		final FileTreeIterator top = new FileTreeIterator(trash, db.getFS(),
165 				db.getConfig().get(WorkingTreeOptions.KEY));
166 
167 		assertTrue(top.first());
168 		assertFalse(top.eof());
169 		assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode);
170 		assertEquals(paths[0], nameOf(top));
171 		assertEquals(paths[0].length(), top.getEntryLength());
172 		assertEquals(mtime[0], top.getEntryLastModifiedInstant());
173 
174 		top.next(1);
175 		assertFalse(top.first());
176 		assertFalse(top.eof());
177 		assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode);
178 		assertEquals(paths[1], nameOf(top));
179 		assertEquals(paths[1].length(), top.getEntryLength());
180 		assertEquals(mtime[1], top.getEntryLastModifiedInstant());
181 
182 		top.next(1);
183 		assertFalse(top.first());
184 		assertFalse(top.eof());
185 		assertEquals(FileMode.TREE.getBits(), top.mode);
186 
187 		try (ObjectReader reader = db.newObjectReader()) {
188 			final AbstractTreeIterator sub = top.createSubtreeIterator(reader);
189 			assertTrue(sub instanceof FileTreeIterator);
190 			final FileTreeIterator subfti = (FileTreeIterator) sub;
191 			assertTrue(sub.first());
192 			assertFalse(sub.eof());
193 			assertEquals(paths[2], nameOf(sub));
194 			assertEquals(paths[2].length(), subfti.getEntryLength());
195 			assertEquals(mtime[2], subfti.getEntryLastModifiedInstant());
196 
197 			sub.next(1);
198 			assertTrue(sub.eof());
199 			top.next(1);
200 			assertFalse(top.first());
201 			assertFalse(top.eof());
202 			assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode);
203 			assertEquals(paths[3], nameOf(top));
204 			assertEquals(paths[3].length(), top.getEntryLength());
205 			assertEquals(mtime[3], top.getEntryLastModifiedInstant());
206 
207 			top.next(1);
208 			assertTrue(top.eof());
209 		}
210 	}
211 
212 	@Test
213 	public void testComputeFileObjectId() throws Exception {
214 		final FileTreeIterator top = new FileTreeIterator(trash, db.getFS(),
215 				db.getConfig().get(WorkingTreeOptions.KEY));
216 
217 		final MessageDigest md = Constants.newMessageDigest();
218 		md.update(Constants.encodeASCII(Constants.TYPE_BLOB));
219 		md.update((byte) ' ');
220 		md.update(Constants.encodeASCII(paths[0].length()));
221 		md.update((byte) 0);
222 		md.update(Constants.encode(paths[0]));
223 		final ObjectId expect = ObjectId.fromRaw(md.digest());
224 
225 		assertEquals(expect, top.getEntryObjectId());
226 
227 		// Verify it was cached by removing the file and getting it again.
228 		//
229 		FileUtils.delete(new File(trash, paths[0]));
230 		assertEquals(expect, top.getEntryObjectId());
231 	}
232 
233 	@Test
234 	public void testDirCacheMatchingId() throws Exception {
235 		File f = writeTrashFile("file", "content");
236 		try (Git git = new Git(db)) {
237 			writeTrashFile("file", "content");
238 			fsTick(f);
239 			git.add().addFilepattern("file").call();
240 		}
241 		DirCacheEntry dce = db.readDirCache().getEntry("file");
242 		try (TreeWalk tw = new TreeWalk(db)) {
243 			FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(),
244 					db.getConfig().get(WorkingTreeOptions.KEY));
245 			tw.addTree(fti);
246 			DirCacheIterator dci = new DirCacheIterator(db.readDirCache());
247 			tw.addTree(dci);
248 			fti.setDirCacheIterator(tw, 1);
249 			while (tw.next() && !tw.getPathString().equals("file")) {
250 				//
251 			}
252 			assertEquals(MetadataDiff.EQUAL, fti.compareMetadata(dce));
253 			ObjectId fromRaw = ObjectId.fromRaw(fti.idBuffer(), fti.idOffset());
254 			assertEquals("6b584e8ece562ebffc15d38808cd6b98fc3d97ea",
255 					fromRaw.getName());
256 			try (ObjectReader objectReader = db.newObjectReader()) {
257 				assertFalse(fti.isModified(dce, false, objectReader));
258 			}
259 		}
260 	}
261 
262 	@Test
263 	public void testTreewalkEnterSubtree() throws Exception {
264 		try (Git git = new Git(db); TreeWalk tw = new TreeWalk(db)) {
265 			writeTrashFile("b/c", "b/c");
266 			writeTrashFile("z/.git", "gitdir: /tmp/somewhere");
267 			git.add().addFilepattern(".").call();
268 			git.rm().addFilepattern("a,").addFilepattern("a,b")
269 					.addFilepattern("a0b").call();
270 			assertEquals("[a/b, mode:100644][b/c, mode:100644][z, mode:160000]",
271 					indexState(0));
272 			FileUtils.delete(new File(db.getWorkTree(), "b"),
273 					FileUtils.RECURSIVE);
274 
275 			tw.addTree(new DirCacheIterator(db.readDirCache()));
276 			tw.addTree(new FileTreeIterator(db));
277 			assertTrue(tw.next());
278 			assertEquals("a", tw.getPathString());
279 			tw.enterSubtree();
280 			tw.next();
281 			assertEquals("a/b", tw.getPathString());
282 			tw.next();
283 			assertEquals("b", tw.getPathString());
284 			tw.enterSubtree();
285 			tw.next();
286 			assertEquals("b/c", tw.getPathString());
287 			assertNotNull(tw.getTree(0, AbstractTreeIterator.class));
288 			assertNotNull(tw.getTree(EmptyTreeIterator.class));
289 		}
290 	}
291 
292 	@Test
293 	public void testIsModifiedSymlinkAsFile() throws Exception {
294 		writeTrashFile("symlink", "content");
295 		try (Git git = new Git(db)) {
296 			db.getConfig().setString(ConfigConstants.CONFIG_CORE_SECTION, null,
297 					ConfigConstants.CONFIG_KEY_SYMLINKS, "false");
298 			git.add().addFilepattern("symlink").call();
299 			git.commit().setMessage("commit").call();
300 		}
301 
302 		// Modify previously committed DirCacheEntry and write it back to disk
303 		DirCacheEntry dce = db.readDirCache().getEntry("symlink");
304 		dce.setFileMode(FileMode.SYMLINK);
305 		try (ObjectReader objectReader = db.newObjectReader()) {
306 			WorkingTreeOptions options = db.getConfig()
307 					.get(WorkingTreeOptions.KEY);
308 			DirCacheCheckout.checkoutEntry(db, dce, objectReader, false, null,
309 					options);
310 
311 			FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(),
312 					options);
313 			while (!fti.getEntryPathString().equals("symlink")) {
314 				fti.next(1);
315 			}
316 			assertFalse(fti.isModified(dce, false, objectReader));
317 		}
318 	}
319 
320 	@Test
321 	public void testIsModifiedFileSmudged() throws Exception {
322 		File f = writeTrashFile("file", "content");
323 		FS fs = db.getFS();
324 		try (Git git = new Git(db)) {
325 			// The idea of this test is to check the smudged handling
326 			// Hopefully fsTick will make sure our entry gets smudged
327 			fsTick(f);
328 			writeTrashFile("file", "content");
329 			Instant lastModified = fs.lastModifiedInstant(f);
330 			git.add().addFilepattern("file").call();
331 			writeTrashFile("file", "conten2");
332 			fs.setLastModified(f.toPath(), lastModified);
333 			// We cannot trust this to go fast enough on
334 			// a system with less than one-second lastModified
335 			// resolution, so we force the index to have the
336 			// same timestamp as the file we look at.
337 			fs.setLastModified(db.getIndexFile().toPath(), lastModified);
338 		}
339 		DirCacheEntry dce = db.readDirCache().getEntry("file");
340 		FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(), db
341 				.getConfig().get(WorkingTreeOptions.KEY));
342 		while (!fti.getEntryPathString().equals("file"))
343 			fti.next(1);
344 		// If the rounding trick does not work we could skip the compareMetaData
345 		// test and hope that we are usually testing the intended code path.
346 		assertEquals(MetadataDiff.SMUDGED, fti.compareMetadata(dce));
347 		try (ObjectReader objectReader = db.newObjectReader()) {
348 			assertTrue(fti.isModified(dce, false, objectReader));
349 		}
350 	}
351 
352 	@Test
353 	public void submoduleHeadMatchesIndex() throws Exception {
354 		try (Git git = new Git(db);
355 				TreeWalk walk = new TreeWalk(db)) {
356 			writeTrashFile("file.txt", "content");
357 			git.add().addFilepattern("file.txt").call();
358 			final RevCommit id = git.commit().setMessage("create file").call();
359 			final String path = "sub";
360 			DirCache cache = db.lockDirCache();
361 			DirCacheEditor editor = cache.editor();
362 			editor.add(new PathEdit(path) {
363 
364 				@Override
365 				public void apply(DirCacheEntry ent) {
366 					ent.setFileMode(FileMode.GITLINK);
367 					ent.setObjectId(id);
368 				}
369 			});
370 			editor.commit();
371 
372 			Git.cloneRepository().setURI(db.getDirectory().toURI().toString())
373 					.setDirectory(new File(db.getWorkTree(), path)).call()
374 					.getRepository().close();
375 
376 			DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
377 			FileTreeIterator workTreeIter = new FileTreeIterator(db);
378 			walk.addTree(indexIter);
379 			walk.addTree(workTreeIter);
380 			walk.setFilter(PathFilter.create(path));
381 
382 			assertTrue(walk.next());
383 			assertTrue(indexIter.idEqual(workTreeIter));
384 		}
385 	}
386 
387 	@Test
388 	public void submoduleWithNoGitDirectory() throws Exception {
389 		try (Git git = new Git(db);
390 				TreeWalk walk = new TreeWalk(db)) {
391 			writeTrashFile("file.txt", "content");
392 			git.add().addFilepattern("file.txt").call();
393 			final RevCommit id = git.commit().setMessage("create file").call();
394 			final String path = "sub";
395 			DirCache cache = db.lockDirCache();
396 			DirCacheEditor editor = cache.editor();
397 			editor.add(new PathEdit(path) {
398 
399 				@Override
400 				public void apply(DirCacheEntry ent) {
401 					ent.setFileMode(FileMode.GITLINK);
402 					ent.setObjectId(id);
403 				}
404 			});
405 			editor.commit();
406 
407 			File submoduleRoot = new File(db.getWorkTree(), path);
408 			assertTrue(submoduleRoot.mkdir());
409 			assertTrue(new File(submoduleRoot, Constants.DOT_GIT).mkdir());
410 
411 			DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
412 			FileTreeIterator workTreeIter = new FileTreeIterator(db);
413 			walk.addTree(indexIter);
414 			walk.addTree(workTreeIter);
415 			walk.setFilter(PathFilter.create(path));
416 
417 			assertTrue(walk.next());
418 			assertFalse(indexIter.idEqual(workTreeIter));
419 			assertEquals(ObjectId.zeroId(), workTreeIter.getEntryObjectId());
420 		}
421 	}
422 
423 	@Test
424 	public void submoduleWithNoHead() throws Exception {
425 		try (Git git = new Git(db);
426 				TreeWalk walk = new TreeWalk(db)) {
427 			writeTrashFile("file.txt", "content");
428 			git.add().addFilepattern("file.txt").call();
429 			final RevCommit id = git.commit().setMessage("create file").call();
430 			final String path = "sub";
431 			DirCache cache = db.lockDirCache();
432 			DirCacheEditor editor = cache.editor();
433 			editor.add(new PathEdit(path) {
434 
435 				@Override
436 				public void apply(DirCacheEntry ent) {
437 					ent.setFileMode(FileMode.GITLINK);
438 					ent.setObjectId(id);
439 				}
440 			});
441 			editor.commit();
442 
443 			assertNotNull(Git.init().setDirectory(new File(db.getWorkTree(), path))
444 					.call().getRepository());
445 
446 			DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
447 			FileTreeIterator workTreeIter = new FileTreeIterator(db);
448 			walk.addTree(indexIter);
449 			walk.addTree(workTreeIter);
450 			walk.setFilter(PathFilter.create(path));
451 
452 			assertTrue(walk.next());
453 			assertFalse(indexIter.idEqual(workTreeIter));
454 			assertEquals(ObjectId.zeroId(), workTreeIter.getEntryObjectId());
455 		}
456 	}
457 
458 	@Test
459 	public void submoduleDirectoryIterator() throws Exception {
460 		try (Git git = new Git(db);
461 				TreeWalk walk = new TreeWalk(db)) {
462 			writeTrashFile("file.txt", "content");
463 			git.add().addFilepattern("file.txt").call();
464 			final RevCommit id = git.commit().setMessage("create file").call();
465 			final String path = "sub";
466 			DirCache cache = db.lockDirCache();
467 			DirCacheEditor editor = cache.editor();
468 			editor.add(new PathEdit(path) {
469 
470 				@Override
471 				public void apply(DirCacheEntry ent) {
472 					ent.setFileMode(FileMode.GITLINK);
473 					ent.setObjectId(id);
474 				}
475 			});
476 			editor.commit();
477 
478 			Git.cloneRepository().setURI(db.getDirectory().toURI().toString())
479 					.setDirectory(new File(db.getWorkTree(), path)).call()
480 					.getRepository().close();
481 
482 			DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
483 			FileTreeIterator workTreeIter = new FileTreeIterator(db.getWorkTree(),
484 					db.getFS(), db.getConfig().get(WorkingTreeOptions.KEY));
485 			walk.addTree(indexIter);
486 			walk.addTree(workTreeIter);
487 			walk.setFilter(PathFilter.create(path));
488 
489 			assertTrue(walk.next());
490 			assertTrue(indexIter.idEqual(workTreeIter));
491 		}
492 	}
493 
494 	@Test
495 	public void submoduleNestedWithHeadMatchingIndex() throws Exception {
496 		try (Git git = new Git(db);
497 				TreeWalk walk = new TreeWalk(db)) {
498 			writeTrashFile("file.txt", "content");
499 			git.add().addFilepattern("file.txt").call();
500 			final RevCommit id = git.commit().setMessage("create file").call();
501 			final String path = "sub/dir1/dir2";
502 			DirCache cache = db.lockDirCache();
503 			DirCacheEditor editor = cache.editor();
504 			editor.add(new PathEdit(path) {
505 
506 				@Override
507 				public void apply(DirCacheEntry ent) {
508 					ent.setFileMode(FileMode.GITLINK);
509 					ent.setObjectId(id);
510 				}
511 			});
512 			editor.commit();
513 
514 			Git.cloneRepository().setURI(db.getDirectory().toURI().toString())
515 					.setDirectory(new File(db.getWorkTree(), path)).call()
516 					.getRepository().close();
517 
518 			DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
519 			FileTreeIterator workTreeIter = new FileTreeIterator(db);
520 			walk.addTree(indexIter);
521 			walk.addTree(workTreeIter);
522 			walk.setFilter(PathFilter.create(path));
523 
524 			assertTrue(walk.next());
525 			assertTrue(indexIter.idEqual(workTreeIter));
526 		}
527 	}
528 
529 	@Test
530 	public void idOffset() throws Exception {
531 		try (Git git = new Git(db);
532 				TreeWalk tw = new TreeWalk(db)) {
533 			writeTrashFile("fileAinfsonly", "A");
534 			File fileBinindex = writeTrashFile("fileBinindex", "B");
535 			fsTick(fileBinindex);
536 			git.add().addFilepattern("fileBinindex").call();
537 			writeTrashFile("fileCinfsonly", "C");
538 			DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
539 			FileTreeIterator workTreeIter = new FileTreeIterator(db);
540 			tw.addTree(indexIter);
541 			tw.addTree(workTreeIter);
542 			workTreeIter.setDirCacheIterator(tw, 0);
543 			assertEntry("d46c305e85b630558ee19cc47e73d2e5c8c64cdc", "a,", tw);
544 			assertEntry("58ee403f98538ec02409538b3f80adf610accdec", "a,b", tw);
545 			assertEntry("0000000000000000000000000000000000000000", "a", tw);
546 			assertEntry("b8d30ff397626f0f1d3538d66067edf865e201d6", "a0b", tw);
547 			// The reason for adding this test. Check that the id is correct for
548 			// mixed
549 			assertEntry("8c7e5a667f1b771847fe88c01c3de34413a1b220",
550 					"fileAinfsonly", tw);
551 			assertEntry("7371f47a6f8bd23a8fa1a8b2a9479cdd76380e54", "fileBinindex",
552 					tw);
553 			assertEntry("96d80cd6c4e7158dbebd0849f4fb7ce513e5828c",
554 					"fileCinfsonly", tw);
555 			assertFalse(tw.next());
556 		}
557 	}
558 
559 	private final FileTreeIterator.FileModeStrategy NO_GITLINKS_STRATEGY = (
560 			File f, FS.Attributes attributes) -> {
561 		if (attributes.isSymbolicLink()) {
562 			return FileMode.SYMLINK;
563 		} else if (attributes.isDirectory()) {
564 			// NOTE: in the production DefaultFileModeStrategy, there is
565 			// a check here for a subdirectory called '.git', and if it
566 			// exists, we create a GITLINK instead of recursing into the
567 			// tree. In this custom strategy, we ignore nested git dirs
568 			// and treat all directories the same.
569 			return FileMode.TREE;
570 		} else if (attributes.isExecutable()) {
571 			return FileMode.EXECUTABLE_FILE;
572 		} else {
573 			return FileMode.REGULAR_FILE;
574 		}
575 	};
576 
577 	private Repository createNestedRepo() throws IOException {
578 		File gitdir = createUniqueTestGitDir(false);
579 		FileRepositoryBuilder builder = new FileRepositoryBuilder();
580 		builder.setGitDir(gitdir);
581 		Repository nestedRepo = builder.build();
582 		nestedRepo.create();
583 
584 		JGitTestUtil.writeTrashFile(nestedRepo, "sub", "a.txt", "content");
585 
586 		File nestedRepoPath = new File(nestedRepo.getWorkTree(), "sub/nested");
587 		FileRepositoryBuilder nestedBuilder = new FileRepositoryBuilder();
588 		nestedBuilder.setWorkTree(nestedRepoPath);
589 		nestedBuilder.build().create();
590 
591 		JGitTestUtil.writeTrashFile(nestedRepo, "sub/nested", "b.txt",
592 				"content b");
593 
594 		return nestedRepo;
595 	}
596 
597 	@Test
598 	public void testCustomFileModeStrategy() throws Exception {
599 		try (Repository nestedRepo = createNestedRepo();
600 				Git git = new Git(nestedRepo)) {
601 			// validate that our custom strategy is honored
602 			WorkingTreeIterator customIterator = new FileTreeIterator(
603 					nestedRepo, NO_GITLINKS_STRATEGY);
604 			git.add().setWorkingTreeIterator(customIterator).addFilepattern(".")
605 					.call();
606 			assertEquals(
607 					"[sub/a.txt, mode:100644, content:content]"
608 							+ "[sub/nested/b.txt, mode:100644, content:content b]",
609 					indexState(nestedRepo, CONTENT));
610 		}
611 	}
612 
613 	@Test
614 	public void testCustomFileModeStrategyFromParentIterator() throws Exception {
615 		try (Repository nestedRepo = createNestedRepo();
616 				Git git = new Git(nestedRepo)) {
617 			FileTreeIterator customIterator = new FileTreeIterator(nestedRepo,
618 					NO_GITLINKS_STRATEGY);
619 			File r = new File(nestedRepo.getWorkTree(), "sub");
620 
621 			// here we want to validate that if we create a new iterator using
622 			// the constructor that accepts a parent iterator, that the child
623 			// iterator correctly inherits the FileModeStrategy from the parent
624 			// iterator.
625 			FileTreeIterator childIterator = new FileTreeIterator(
626 					customIterator, r, nestedRepo.getFS());
627 			git.add().setWorkingTreeIterator(childIterator).addFilepattern(".")
628 					.call();
629 			assertEquals(
630 					"[sub/a.txt, mode:100644, content:content]"
631 							+ "[sub/nested/b.txt, mode:100644, content:content b]",
632 					indexState(nestedRepo, CONTENT));
633 		}
634 	}
635 
636 	@Test
637 	public void testFileModeSymLinkIsNotATree() throws IOException {
638 		org.junit.Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
639 		FS fs = db.getFS();
640 		// mål = target in swedish, just to get some unicode in here
641 		writeTrashFile("mål/data", "targetdata");
642 		File file = new File(trash, "länk");
643 
644 		try {
645 			file.toPath();
646 		} catch (InvalidPathException e) {
647 			// When executing a test with LANG environment variable set to non
648 			// UTF-8 encoding, it seems that JRE cannot handle Unicode file
649 			// paths. This happens when this test is executed in Bazel as it
650 			// unsets LANG
651 			// (https://docs.bazel.build/versions/master/test-encyclopedia.html#initial-conditions).
652 			// Skip the test if the runtime cannot handle Unicode characters.
653 			assumeNoException(e);
654 		}
655 
656 		fs.createSymLink(file, "mål");
657 		FileTreeIterator fti = new FileTreeIterator(db);
658 		assertFalse(fti.eof());
659 		while (!fti.getEntryPathString().equals("länk")) {
660 			fti.next(1);
661 		}
662 		assertEquals("länk", fti.getEntryPathString());
663 		assertEquals(FileMode.SYMLINK, fti.getEntryFileMode());
664 		fti.next(1);
665 		assertFalse(fti.eof());
666 		assertEquals("mål", fti.getEntryPathString());
667 		assertEquals(FileMode.TREE, fti.getEntryFileMode());
668 		fti.next(1);
669 		assertTrue(fti.eof());
670 	}
671 
672 	@Test
673 	public void testSymlinkNotModifiedThoughNormalized() throws Exception {
674 		DirCache dc = db.lockDirCache();
675 		DirCacheEditor dce = dc.editor();
676 		final String UNNORMALIZED = "target/";
677 		final byte[] UNNORMALIZED_BYTES = Constants.encode(UNNORMALIZED);
678 		try (ObjectInserter oi = db.newObjectInserter()) {
679 			final ObjectId linkid = oi.insert(Constants.OBJ_BLOB,
680 					UNNORMALIZED_BYTES, 0, UNNORMALIZED_BYTES.length);
681 			dce.add(new DirCacheEditor.PathEdit("link") {
682 				@Override
683 				public void apply(DirCacheEntry ent) {
684 					ent.setFileMode(FileMode.SYMLINK);
685 					ent.setObjectId(linkid);
686 					ent.setLength(UNNORMALIZED_BYTES.length);
687 				}
688 			});
689 			assertTrue(dce.commit());
690 		}
691 		try (Git git = new Git(db)) {
692 			git.commit().setMessage("Adding link").call();
693 			git.reset().setMode(ResetType.HARD).call();
694 			DirCacheIterator dci = new DirCacheIterator(db.readDirCache());
695 			FileTreeIterator fti = new FileTreeIterator(db);
696 
697 			// self-check
698 			while (!fti.getEntryPathString().equals("link")) {
699 				fti.next(1);
700 			}
701 			assertEquals("link", fti.getEntryPathString());
702 			assertEquals("link", dci.getEntryPathString());
703 
704 			// test
705 			assertFalse(fti.isModified(dci.getDirCacheEntry(), true,
706 					db.newObjectReader()));
707 		}
708 	}
709 
710 	/**
711 	 * Like #testSymlinkNotModifiedThoughNormalized but there is no
712 	 * normalization being done.
713 	 *
714 	 * @throws Exception
715 	 */
716 	@Test
717 	public void testSymlinkModifiedNotNormalized() throws Exception {
718 		DirCache dc = db.lockDirCache();
719 		DirCacheEditor dce = dc.editor();
720 		final String NORMALIZED = "target";
721 		final byte[] NORMALIZED_BYTES = Constants.encode(NORMALIZED);
722 		try (ObjectInserter oi = db.newObjectInserter()) {
723 			final ObjectId linkid = oi.insert(Constants.OBJ_BLOB,
724 					NORMALIZED_BYTES, 0, NORMALIZED_BYTES.length);
725 			dce.add(new DirCacheEditor.PathEdit("link") {
726 				@Override
727 				public void apply(DirCacheEntry ent) {
728 					ent.setFileMode(FileMode.SYMLINK);
729 					ent.setObjectId(linkid);
730 					ent.setLength(NORMALIZED_BYTES.length);
731 				}
732 			});
733 			assertTrue(dce.commit());
734 		}
735 		try (Git git = new Git(db)) {
736 			git.commit().setMessage("Adding link").call();
737 			git.reset().setMode(ResetType.HARD).call();
738 			DirCacheIterator dci = new DirCacheIterator(db.readDirCache());
739 			FileTreeIterator fti = new FileTreeIterator(db);
740 
741 			// self-check
742 			while (!fti.getEntryPathString().equals("link")) {
743 				fti.next(1);
744 			}
745 			assertEquals("link", fti.getEntryPathString());
746 			assertEquals("link", dci.getEntryPathString());
747 
748 			// test
749 			assertFalse(fti.isModified(dci.getDirCacheEntry(), true,
750 					db.newObjectReader()));
751 		}
752 	}
753 
754 	/**
755 	 * Like #testSymlinkNotModifiedThoughNormalized but here the link is
756 	 * modified.
757 	 *
758 	 * @throws Exception
759 	 */
760 	@Test
761 	public void testSymlinkActuallyModified() throws Exception {
762 		org.junit.Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
763 		final String NORMALIZED = "target";
764 		final byte[] NORMALIZED_BYTES = Constants.encode(NORMALIZED);
765 		try (ObjectInserter oi = db.newObjectInserter()) {
766 			final ObjectId linkid = oi.insert(Constants.OBJ_BLOB,
767 					NORMALIZED_BYTES, 0, NORMALIZED_BYTES.length);
768 			DirCache dc = db.lockDirCache();
769 			DirCacheEditor dce = dc.editor();
770 			dce.add(new DirCacheEditor.PathEdit("link") {
771 				@Override
772 				public void apply(DirCacheEntry ent) {
773 					ent.setFileMode(FileMode.SYMLINK);
774 					ent.setObjectId(linkid);
775 					ent.setLength(NORMALIZED_BYTES.length);
776 				}
777 			});
778 			assertTrue(dce.commit());
779 		}
780 		try (Git git = new Git(db)) {
781 			git.commit().setMessage("Adding link").call();
782 			git.reset().setMode(ResetType.HARD).call();
783 
784 			FileUtils.delete(new File(trash, "link"), FileUtils.NONE);
785 			FS.DETECTED.createSymLink(new File(trash, "link"), "newtarget");
786 			DirCacheIterator dci = new DirCacheIterator(db.readDirCache());
787 			FileTreeIterator fti = new FileTreeIterator(db);
788 
789 			// self-check
790 			while (!fti.getEntryPathString().equals("link")) {
791 				fti.next(1);
792 			}
793 			assertEquals("link", fti.getEntryPathString());
794 			assertEquals("link", dci.getEntryPathString());
795 
796 			// test
797 			assertTrue(fti.isModified(dci.getDirCacheEntry(), true,
798 					db.newObjectReader()));
799 		}
800 	}
801 
802 	private static void assertEntry(String sha1string, String path, TreeWalk tw)
803 			throws MissingObjectException, IncorrectObjectTypeException,
804 			CorruptObjectException, IOException {
805 		assertTrue(tw.next());
806 		assertEquals(path, tw.getPathString());
807 		assertEquals(sha1string, tw.getObjectId(1).getName() /* 1=filetree here */);
808 	}
809 
810 	private static String nameOf(AbstractTreeIterator i) {
811 		return RawParseUtils.decode(UTF_8, i.path, 0, i.pathLen);
812 	}
813 }