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