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