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