| /* | |
| * Copyright (C) 2008, 2017, Google Inc. and others | |
| * | |
| * This program and the accompanying materials are made available under the | |
| * terms of the Eclipse Distribution License v. 1.0 which is available at | |
| * https://www.eclipse.org/org/documents/edl-v10.php. | |
| * | |
| * SPDX-License-Identifier: BSD-3-Clause | |
| */ | |
| package org.eclipse.jgit.treewalk; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static org.junit.Assert.assertEquals; | |
| import static org.junit.Assert.assertFalse; | |
| import static org.junit.Assert.assertNotNull; | |
| import static org.junit.Assert.assertTrue; | |
| import static org.junit.Assume.assumeNoException; | |
| import static org.junit.Assume.assumeTrue; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.nio.file.InvalidPathException; | |
| import java.security.MessageDigest; | |
| import java.time.Instant; | |
| import org.eclipse.jgit.api.Git; | |
| import org.eclipse.jgit.api.ResetCommand.ResetType; | |
| import org.eclipse.jgit.dircache.Checkout; | |
| import org.eclipse.jgit.dircache.DirCache; | |
| import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; | |
| import org.eclipse.jgit.dircache.DirCacheEditor; | |
| import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; | |
| import org.eclipse.jgit.dircache.DirCacheEntry; | |
| import org.eclipse.jgit.dircache.DirCacheIterator; | |
| import org.eclipse.jgit.errors.CorruptObjectException; | |
| import org.eclipse.jgit.errors.IncorrectObjectTypeException; | |
| import org.eclipse.jgit.errors.MissingObjectException; | |
| import org.eclipse.jgit.junit.JGitTestUtil; | |
| import org.eclipse.jgit.junit.RepositoryTestCase; | |
| import org.eclipse.jgit.lib.ConfigConstants; | |
| import org.eclipse.jgit.lib.Constants; | |
| import org.eclipse.jgit.lib.CoreConfig.EolStreamType; | |
| import org.eclipse.jgit.lib.FileMode; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.ObjectInserter; | |
| import org.eclipse.jgit.lib.ObjectReader; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.revwalk.RevCommit; | |
| import org.eclipse.jgit.storage.file.FileRepositoryBuilder; | |
| import org.eclipse.jgit.treewalk.WorkingTreeIterator.MetadataDiff; | |
| import org.eclipse.jgit.treewalk.filter.PathFilter; | |
| import org.eclipse.jgit.util.FS; | |
| import org.eclipse.jgit.util.FileUtils; | |
| import org.eclipse.jgit.util.RawParseUtils; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| public class FileTreeIteratorTest extends RepositoryTestCase { | |
| private final String[] paths = { "a,", "a,b", "a/b", "a0b" }; | |
| private Instant[] mtime; | |
| @Override | |
| @Before | |
| public void setUp() throws Exception { | |
| super.setUp(); | |
| // We build the entries backwards so that on POSIX systems we | |
| // are likely to get the entries in the trash directory in the | |
| // opposite order of what they should be in for the iteration. | |
| // This should stress the sorting code better than doing it in | |
| // the correct order. | |
| // | |
| mtime = new Instant[paths.length]; | |
| for (int i = paths.length - 1; i >= 0; i--) { | |
| final String s = paths[i]; | |
| writeTrashFile(s, s); | |
| mtime[i] = db.getFS().lastModifiedInstant(new File(trash, s)); | |
| } | |
| } | |
| @Test | |
| public void testGetEntryContentLength() throws Exception { | |
| final FileTreeIterator fti = new FileTreeIterator(db); | |
| fti.next(1); | |
| assertEquals(3, fti.getEntryContentLength()); | |
| fti.back(1); | |
| assertEquals(2, fti.getEntryContentLength()); | |
| fti.next(1); | |
| assertEquals(3, fti.getEntryContentLength()); | |
| fti.reset(); | |
| assertEquals(2, fti.getEntryContentLength()); | |
| } | |
| @Test | |
| public void testEmptyIfRootIsFile() throws Exception { | |
| final File r = new File(trash, paths[0]); | |
| assertTrue(r.isFile()); | |
| final FileTreeIterator fti = new FileTreeIterator(r, db.getFS(), | |
| db.getConfig().get(WorkingTreeOptions.KEY)); | |
| assertTrue(fti.first()); | |
| assertTrue(fti.eof()); | |
| } | |
| @Test | |
| public void testEmptyIfRootDoesNotExist() throws Exception { | |
| final File r = new File(trash, "not-existing-file"); | |
| assertFalse(r.exists()); | |
| final FileTreeIterator fti = new FileTreeIterator(r, db.getFS(), | |
| db.getConfig().get(WorkingTreeOptions.KEY)); | |
| assertTrue(fti.first()); | |
| assertTrue(fti.eof()); | |
| } | |
| @Test | |
| public void testEmptyIfRootIsEmpty() throws Exception { | |
| final File r = new File(trash, "not-existing-file"); | |
| assertFalse(r.exists()); | |
| FileUtils.mkdir(r); | |
| final FileTreeIterator fti = new FileTreeIterator(r, db.getFS(), | |
| db.getConfig().get(WorkingTreeOptions.KEY)); | |
| assertTrue(fti.first()); | |
| assertTrue(fti.eof()); | |
| } | |
| @Test | |
| public void testEmptyIteratorOnEmptyDirectory() throws Exception { | |
| String nonExistingFileName = "not-existing-file"; | |
| final File r = new File(trash, nonExistingFileName); | |
| assertFalse(r.exists()); | |
| FileUtils.mkdir(r); | |
| final FileTreeIterator parent = new FileTreeIterator(db); | |
| while (!parent.getEntryPathString().equals(nonExistingFileName)) | |
| parent.next(1); | |
| final FileTreeIterator childIter = new FileTreeIterator(parent, r, | |
| db.getFS()); | |
| assertTrue(childIter.first()); | |
| assertTrue(childIter.eof()); | |
| String parentPath = parent.getEntryPathString(); | |
| assertEquals(nonExistingFileName, parentPath); | |
| // must be "not-existing-file/", but getEntryPathString() was broken by | |
| // 445363 too | |
| String childPath = childIter.getEntryPathString(); | |
| // in bug 445363 the iterator wrote garbage to the parent "path" field | |
| EmptyTreeIterator e = childIter.createEmptyTreeIterator(); | |
| assertNotNull(e); | |
| // check if parent path is not overridden by empty iterator (bug 445363) | |
| // due bug 445363 this was "/ot-existing-file" instead of | |
| // "not-existing-file" | |
| assertEquals(parentPath, parent.getEntryPathString()); | |
| assertEquals(parentPath + "/", childPath); | |
| assertEquals(parentPath + "/", childIter.getEntryPathString()); | |
| assertEquals(childPath + "/", e.getEntryPathString()); | |
| } | |
| @Test | |
| public void testSimpleIterate() throws Exception { | |
| final FileTreeIterator top = new FileTreeIterator(trash, db.getFS(), | |
| db.getConfig().get(WorkingTreeOptions.KEY)); | |
| assertTrue(top.first()); | |
| assertFalse(top.eof()); | |
| assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode); | |
| assertEquals(paths[0], nameOf(top)); | |
| assertEquals(paths[0].length(), top.getEntryLength()); | |
| assertEquals(mtime[0], top.getEntryLastModifiedInstant()); | |
| top.next(1); | |
| assertFalse(top.first()); | |
| assertFalse(top.eof()); | |
| assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode); | |
| assertEquals(paths[1], nameOf(top)); | |
| assertEquals(paths[1].length(), top.getEntryLength()); | |
| assertEquals(mtime[1], top.getEntryLastModifiedInstant()); | |
| top.next(1); | |
| assertFalse(top.first()); | |
| assertFalse(top.eof()); | |
| assertEquals(FileMode.TREE.getBits(), top.mode); | |
| try (ObjectReader reader = db.newObjectReader()) { | |
| final AbstractTreeIterator sub = top.createSubtreeIterator(reader); | |
| assertTrue(sub instanceof FileTreeIterator); | |
| final FileTreeIterator subfti = (FileTreeIterator) sub; | |
| assertTrue(sub.first()); | |
| assertFalse(sub.eof()); | |
| assertEquals(paths[2], nameOf(sub)); | |
| assertEquals(paths[2].length(), subfti.getEntryLength()); | |
| assertEquals(mtime[2], subfti.getEntryLastModifiedInstant()); | |
| sub.next(1); | |
| assertTrue(sub.eof()); | |
| top.next(1); | |
| assertFalse(top.first()); | |
| assertFalse(top.eof()); | |
| assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode); | |
| assertEquals(paths[3], nameOf(top)); | |
| assertEquals(paths[3].length(), top.getEntryLength()); | |
| assertEquals(mtime[3], top.getEntryLastModifiedInstant()); | |
| top.next(1); | |
| assertTrue(top.eof()); | |
| } | |
| } | |
| @Test | |
| public void testComputeFileObjectId() throws Exception { | |
| final FileTreeIterator top = new FileTreeIterator(trash, db.getFS(), | |
| db.getConfig().get(WorkingTreeOptions.KEY)); | |
| final MessageDigest md = Constants.newMessageDigest(); | |
| md.update(Constants.encodeASCII(Constants.TYPE_BLOB)); | |
| md.update((byte) ' '); | |
| md.update(Constants.encodeASCII(paths[0].length())); | |
| md.update((byte) 0); | |
| md.update(Constants.encode(paths[0])); | |
| final ObjectId expect = ObjectId.fromRaw(md.digest()); | |
| assertEquals(expect, top.getEntryObjectId()); | |
| // Verify it was cached by removing the file and getting it again. | |
| // | |
| FileUtils.delete(new File(trash, paths[0])); | |
| assertEquals(expect, top.getEntryObjectId()); | |
| } | |
| @Test | |
| public void testDirCacheMatchingId() throws Exception { | |
| File f = writeTrashFile("file", "content"); | |
| try (Git git = new Git(db)) { | |
| writeTrashFile("file", "content"); | |
| fsTick(f); | |
| git.add().addFilepattern("file").call(); | |
| } | |
| DirCacheEntry dce = db.readDirCache().getEntry("file"); | |
| try (TreeWalk tw = new TreeWalk(db)) { | |
| FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(), | |
| db.getConfig().get(WorkingTreeOptions.KEY)); | |
| tw.addTree(fti); | |
| DirCacheIterator dci = new DirCacheIterator(db.readDirCache()); | |
| tw.addTree(dci); | |
| fti.setDirCacheIterator(tw, 1); | |
| while (tw.next() && !tw.getPathString().equals("file")) { | |
| // | |
| } | |
| assertEquals(MetadataDiff.EQUAL, fti.compareMetadata(dce)); | |
| ObjectId fromRaw = ObjectId.fromRaw(fti.idBuffer(), fti.idOffset()); | |
| assertEquals("6b584e8ece562ebffc15d38808cd6b98fc3d97ea", | |
| fromRaw.getName()); | |
| try (ObjectReader objectReader = db.newObjectReader()) { | |
| assertFalse(fti.isModified(dce, false, objectReader)); | |
| } | |
| } | |
| } | |
| @Test | |
| public void testTreewalkEnterSubtree() throws Exception { | |
| try (Git git = new Git(db); TreeWalk tw = new TreeWalk(db)) { | |
| writeTrashFile("b/c", "b/c"); | |
| writeTrashFile("z/.git", "gitdir: /tmp/somewhere"); | |
| git.add().addFilepattern(".").call(); | |
| git.rm().addFilepattern("a,").addFilepattern("a,b") | |
| .addFilepattern("a0b").call(); | |
| assertEquals("[a/b, mode:100644][b/c, mode:100644][z, mode:160000]", | |
| indexState(0)); | |
| FileUtils.delete(new File(db.getWorkTree(), "b"), | |
| FileUtils.RECURSIVE); | |
| tw.addTree(new DirCacheIterator(db.readDirCache())); | |
| tw.addTree(new FileTreeIterator(db)); | |
| assertTrue(tw.next()); | |
| assertEquals("a", tw.getPathString()); | |
| tw.enterSubtree(); | |
| tw.next(); | |
| assertEquals("a/b", tw.getPathString()); | |
| tw.next(); | |
| assertEquals("b", tw.getPathString()); | |
| tw.enterSubtree(); | |
| tw.next(); | |
| assertEquals("b/c", tw.getPathString()); | |
| assertNotNull(tw.getTree(0, AbstractTreeIterator.class)); | |
| assertNotNull(tw.getTree(EmptyTreeIterator.class)); | |
| } | |
| } | |
| @Test | |
| public void testIsModifiedSymlinkAsFile() throws Exception { | |
| writeTrashFile("symlink", "content"); | |
| try (Git git = new Git(db)) { | |
| db.getConfig().setString(ConfigConstants.CONFIG_CORE_SECTION, null, | |
| ConfigConstants.CONFIG_KEY_SYMLINKS, "false"); | |
| git.add().addFilepattern("symlink").call(); | |
| git.commit().setMessage("commit").call(); | |
| } | |
| // Modify previously committed DirCacheEntry and write it back to disk | |
| DirCacheEntry dce = db.readDirCache().getEntry("symlink"); | |
| dce.setFileMode(FileMode.SYMLINK); | |
| try (ObjectReader objectReader = db.newObjectReader()) { | |
| Checkout checkout = new Checkout(db).setRecursiveDeletion(false); | |
| checkout.checkout(dce, | |
| new CheckoutMetadata(EolStreamType.DIRECT, null), | |
| objectReader, null); | |
| WorkingTreeOptions options = db.getConfig() | |
| .get(WorkingTreeOptions.KEY); | |
| FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(), | |
| options); | |
| while (!fti.getEntryPathString().equals("symlink")) { | |
| fti.next(1); | |
| } | |
| assertFalse(fti.isModified(dce, false, objectReader)); | |
| } | |
| } | |
| @Test | |
| public void testIsModifiedFileSmudged() throws Exception { | |
| File f = writeTrashFile("file", "content"); | |
| FS fs = db.getFS(); | |
| try (Git git = new Git(db)) { | |
| // The idea of this test is to check the smudged handling | |
| // Hopefully fsTick will make sure our entry gets smudged | |
| fsTick(f); | |
| writeTrashFile("file", "content"); | |
| Instant lastModified = fs.lastModifiedInstant(f); | |
| git.add().addFilepattern("file").call(); | |
| writeTrashFile("file", "conten2"); | |
| fs.setLastModified(f.toPath(), lastModified); | |
| // We cannot trust this to go fast enough on | |
| // a system with less than one-second lastModified | |
| // resolution, so we force the index to have the | |
| // same timestamp as the file we look at. | |
| fs.setLastModified(db.getIndexFile().toPath(), lastModified); | |
| } | |
| DirCacheEntry dce = db.readDirCache().getEntry("file"); | |
| FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(), | |
| db.getConfig().get(WorkingTreeOptions.KEY)); | |
| while (!fti.getEntryPathString().equals("file")) | |
| fti.next(1); | |
| // If the rounding trick does not work we could skip the compareMetaData | |
| // test and hope that we are usually testing the intended code path. | |
| assertEquals(MetadataDiff.SMUDGED, fti.compareMetadata(dce)); | |
| try (ObjectReader objectReader = db.newObjectReader()) { | |
| assertTrue(fti.isModified(dce, false, objectReader)); | |
| } | |
| } | |
| @Test | |
| public void submoduleHeadMatchesIndex() throws Exception { | |
| try (Git git = new Git(db); TreeWalk walk = new TreeWalk(db)) { | |
| writeTrashFile("file.txt", "content"); | |
| git.add().addFilepattern("file.txt").call(); | |
| final RevCommit id = git.commit().setMessage("create file").call(); | |
| final String path = "sub"; | |
| DirCache cache = db.lockDirCache(); | |
| DirCacheEditor editor = cache.editor(); | |
| editor.add(new PathEdit(path) { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.GITLINK); | |
| ent.setObjectId(id); | |
| } | |
| }); | |
| editor.commit(); | |
| Git.cloneRepository().setURI(db.getDirectory().toURI().toString()) | |
| .setDirectory(new File(db.getWorkTree(), path)).call() | |
| .getRepository().close(); | |
| DirCacheIterator indexIter = new DirCacheIterator( | |
| db.readDirCache()); | |
| FileTreeIterator workTreeIter = new FileTreeIterator(db); | |
| walk.addTree(indexIter); | |
| walk.addTree(workTreeIter); | |
| walk.setFilter(PathFilter.create(path)); | |
| assertTrue(walk.next()); | |
| assertTrue(indexIter.idEqual(workTreeIter)); | |
| } | |
| } | |
| @Test | |
| public void submoduleWithNoGitDirectory() throws Exception { | |
| try (Git git = new Git(db); TreeWalk walk = new TreeWalk(db)) { | |
| writeTrashFile("file.txt", "content"); | |
| git.add().addFilepattern("file.txt").call(); | |
| final RevCommit id = git.commit().setMessage("create file").call(); | |
| final String path = "sub"; | |
| DirCache cache = db.lockDirCache(); | |
| DirCacheEditor editor = cache.editor(); | |
| editor.add(new PathEdit(path) { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.GITLINK); | |
| ent.setObjectId(id); | |
| } | |
| }); | |
| editor.commit(); | |
| File submoduleRoot = new File(db.getWorkTree(), path); | |
| assertTrue(submoduleRoot.mkdir()); | |
| assertTrue(new File(submoduleRoot, Constants.DOT_GIT).mkdir()); | |
| DirCacheIterator indexIter = new DirCacheIterator( | |
| db.readDirCache()); | |
| FileTreeIterator workTreeIter = new FileTreeIterator(db); | |
| walk.addTree(indexIter); | |
| walk.addTree(workTreeIter); | |
| walk.setFilter(PathFilter.create(path)); | |
| assertTrue(walk.next()); | |
| assertFalse(indexIter.idEqual(workTreeIter)); | |
| assertEquals(ObjectId.zeroId(), workTreeIter.getEntryObjectId()); | |
| } | |
| } | |
| @Test | |
| public void submoduleWithNoHead() throws Exception { | |
| try (Git git = new Git(db); TreeWalk walk = new TreeWalk(db)) { | |
| writeTrashFile("file.txt", "content"); | |
| git.add().addFilepattern("file.txt").call(); | |
| final RevCommit id = git.commit().setMessage("create file").call(); | |
| final String path = "sub"; | |
| DirCache cache = db.lockDirCache(); | |
| DirCacheEditor editor = cache.editor(); | |
| editor.add(new PathEdit(path) { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.GITLINK); | |
| ent.setObjectId(id); | |
| } | |
| }); | |
| editor.commit(); | |
| assertNotNull( | |
| Git.init().setDirectory(new File(db.getWorkTree(), path)) | |
| .call().getRepository()); | |
| DirCacheIterator indexIter = new DirCacheIterator( | |
| db.readDirCache()); | |
| FileTreeIterator workTreeIter = new FileTreeIterator(db); | |
| walk.addTree(indexIter); | |
| walk.addTree(workTreeIter); | |
| walk.setFilter(PathFilter.create(path)); | |
| assertTrue(walk.next()); | |
| assertFalse(indexIter.idEqual(workTreeIter)); | |
| assertEquals(ObjectId.zeroId(), workTreeIter.getEntryObjectId()); | |
| } | |
| } | |
| @Test | |
| public void submoduleDirectoryIterator() throws Exception { | |
| try (Git git = new Git(db); TreeWalk walk = new TreeWalk(db)) { | |
| writeTrashFile("file.txt", "content"); | |
| git.add().addFilepattern("file.txt").call(); | |
| final RevCommit id = git.commit().setMessage("create file").call(); | |
| final String path = "sub"; | |
| DirCache cache = db.lockDirCache(); | |
| DirCacheEditor editor = cache.editor(); | |
| editor.add(new PathEdit(path) { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.GITLINK); | |
| ent.setObjectId(id); | |
| } | |
| }); | |
| editor.commit(); | |
| Git.cloneRepository().setURI(db.getDirectory().toURI().toString()) | |
| .setDirectory(new File(db.getWorkTree(), path)).call() | |
| .getRepository().close(); | |
| DirCacheIterator indexIter = new DirCacheIterator( | |
| db.readDirCache()); | |
| FileTreeIterator workTreeIter = new FileTreeIterator( | |
| db.getWorkTree(), db.getFS(), | |
| db.getConfig().get(WorkingTreeOptions.KEY)); | |
| walk.addTree(indexIter); | |
| walk.addTree(workTreeIter); | |
| walk.setFilter(PathFilter.create(path)); | |
| assertTrue(walk.next()); | |
| assertTrue(indexIter.idEqual(workTreeIter)); | |
| } | |
| } | |
| @Test | |
| public void submoduleNestedWithHeadMatchingIndex() throws Exception { | |
| try (Git git = new Git(db); TreeWalk walk = new TreeWalk(db)) { | |
| writeTrashFile("file.txt", "content"); | |
| git.add().addFilepattern("file.txt").call(); | |
| final RevCommit id = git.commit().setMessage("create file").call(); | |
| final String path = "sub/dir1/dir2"; | |
| DirCache cache = db.lockDirCache(); | |
| DirCacheEditor editor = cache.editor(); | |
| editor.add(new PathEdit(path) { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.GITLINK); | |
| ent.setObjectId(id); | |
| } | |
| }); | |
| editor.commit(); | |
| Git.cloneRepository().setURI(db.getDirectory().toURI().toString()) | |
| .setDirectory(new File(db.getWorkTree(), path)).call() | |
| .getRepository().close(); | |
| DirCacheIterator indexIter = new DirCacheIterator( | |
| db.readDirCache()); | |
| FileTreeIterator workTreeIter = new FileTreeIterator(db); | |
| walk.addTree(indexIter); | |
| walk.addTree(workTreeIter); | |
| walk.setFilter(PathFilter.create(path)); | |
| assertTrue(walk.next()); | |
| assertTrue(indexIter.idEqual(workTreeIter)); | |
| } | |
| } | |
| @Test | |
| public void idOffset() throws Exception { | |
| try (Git git = new Git(db); TreeWalk tw = new TreeWalk(db)) { | |
| writeTrashFile("fileAinfsonly", "A"); | |
| File fileBinindex = writeTrashFile("fileBinindex", "B"); | |
| fsTick(fileBinindex); | |
| git.add().addFilepattern("fileBinindex").call(); | |
| writeTrashFile("fileCinfsonly", "C"); | |
| DirCacheIterator indexIter = new DirCacheIterator( | |
| db.readDirCache()); | |
| FileTreeIterator workTreeIter = new FileTreeIterator(db); | |
| tw.addTree(indexIter); | |
| tw.addTree(workTreeIter); | |
| workTreeIter.setDirCacheIterator(tw, 0); | |
| assertEntry("d46c305e85b630558ee19cc47e73d2e5c8c64cdc", "a,", tw); | |
| assertEntry("58ee403f98538ec02409538b3f80adf610accdec", "a,b", tw); | |
| assertEntry("0000000000000000000000000000000000000000", "a", tw); | |
| assertEntry("b8d30ff397626f0f1d3538d66067edf865e201d6", "a0b", tw); | |
| // The reason for adding this test. Check that the id is correct for | |
| // mixed | |
| assertEntry("8c7e5a667f1b771847fe88c01c3de34413a1b220", | |
| "fileAinfsonly", tw); | |
| assertEntry("7371f47a6f8bd23a8fa1a8b2a9479cdd76380e54", | |
| "fileBinindex", tw); | |
| assertEntry("96d80cd6c4e7158dbebd0849f4fb7ce513e5828c", | |
| "fileCinfsonly", tw); | |
| assertFalse(tw.next()); | |
| } | |
| } | |
| private final FileTreeIterator.FileModeStrategy NO_GITLINKS_STRATEGY = ( | |
| File f, FS.Attributes attributes) -> { | |
| if (attributes.isSymbolicLink()) { | |
| return FileMode.SYMLINK; | |
| } else if (attributes.isDirectory()) { | |
| // NOTE: in the production DefaultFileModeStrategy, there is | |
| // a check here for a subdirectory called '.git', and if it | |
| // exists, we create a GITLINK instead of recursing into the | |
| // tree. In this custom strategy, we ignore nested git dirs | |
| // and treat all directories the same. | |
| return FileMode.TREE; | |
| } else if (attributes.isExecutable()) { | |
| return FileMode.EXECUTABLE_FILE; | |
| } else { | |
| return FileMode.REGULAR_FILE; | |
| } | |
| }; | |
| private Repository createNestedRepo() throws IOException { | |
| File gitdir = createUniqueTestGitDir(false); | |
| FileRepositoryBuilder builder = new FileRepositoryBuilder(); | |
| builder.setGitDir(gitdir); | |
| Repository nestedRepo = builder.build(); | |
| nestedRepo.create(); | |
| JGitTestUtil.writeTrashFile(nestedRepo, "sub", "a.txt", "content"); | |
| File nestedRepoPath = new File(nestedRepo.getWorkTree(), "sub/nested"); | |
| FileRepositoryBuilder nestedBuilder = new FileRepositoryBuilder(); | |
| nestedBuilder.setWorkTree(nestedRepoPath); | |
| nestedBuilder.build().create(); | |
| JGitTestUtil.writeTrashFile(nestedRepo, "sub/nested", "b.txt", | |
| "content b"); | |
| return nestedRepo; | |
| } | |
| @Test | |
| public void testCustomFileModeStrategy() throws Exception { | |
| try (Repository nestedRepo = createNestedRepo(); | |
| Git git = new Git(nestedRepo)) { | |
| // validate that our custom strategy is honored | |
| WorkingTreeIterator customIterator = new FileTreeIterator( | |
| nestedRepo, NO_GITLINKS_STRATEGY); | |
| git.add().setWorkingTreeIterator(customIterator).addFilepattern(".") | |
| .call(); | |
| assertEquals("[sub/a.txt, mode:100644, content:content]" | |
| + "[sub/nested/b.txt, mode:100644, content:content b]", | |
| indexState(nestedRepo, CONTENT)); | |
| } | |
| } | |
| @Test | |
| public void testCustomFileModeStrategyFromParentIterator() | |
| throws Exception { | |
| try (Repository nestedRepo = createNestedRepo(); | |
| Git git = new Git(nestedRepo)) { | |
| FileTreeIterator customIterator = new FileTreeIterator(nestedRepo, | |
| NO_GITLINKS_STRATEGY); | |
| File r = new File(nestedRepo.getWorkTree(), "sub"); | |
| // here we want to validate that if we create a new iterator using | |
| // the constructor that accepts a parent iterator, that the child | |
| // iterator correctly inherits the FileModeStrategy from the parent | |
| // iterator. | |
| FileTreeIterator childIterator = new FileTreeIterator( | |
| customIterator, r, nestedRepo.getFS()); | |
| git.add().setWorkingTreeIterator(childIterator).addFilepattern(".") | |
| .call(); | |
| assertEquals("[sub/a.txt, mode:100644, content:content]" | |
| + "[sub/nested/b.txt, mode:100644, content:content b]", | |
| indexState(nestedRepo, CONTENT)); | |
| } | |
| } | |
| @Test | |
| public void testFileModeSymLinkIsNotATree() throws IOException { | |
| org.junit.Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); | |
| FS fs = db.getFS(); | |
| // mål = target in swedish, just to get some unicode in here | |
| writeTrashFile("mål/data", "targetdata"); | |
| File file = new File(trash, "länk"); | |
| try { | |
| file.toPath(); | |
| } catch (InvalidPathException e) { | |
| // When executing a test with LANG environment variable set to non | |
| // UTF-8 encoding, it seems that JRE cannot handle Unicode file | |
| // paths. This happens when this test is executed in Bazel as it | |
| // unsets LANG | |
| // (https://docs.bazel.build/versions/master/test-encyclopedia.html#initial-conditions). | |
| // Skip the test if the runtime cannot handle Unicode characters. | |
| assumeNoException(e); | |
| } | |
| fs.createSymLink(file, "mål"); | |
| FileTreeIterator fti = new FileTreeIterator(db); | |
| assertFalse(fti.eof()); | |
| while (!fti.getEntryPathString().equals("länk")) { | |
| fti.next(1); | |
| } | |
| assertEquals("länk", fti.getEntryPathString()); | |
| assertEquals(FileMode.SYMLINK, fti.getEntryFileMode()); | |
| fti.next(1); | |
| assertFalse(fti.eof()); | |
| assertEquals("mål", fti.getEntryPathString()); | |
| assertEquals(FileMode.TREE, fti.getEntryFileMode()); | |
| fti.next(1); | |
| assertTrue(fti.eof()); | |
| } | |
| @Test | |
| public void testSymlinkNotModifiedThoughNormalized() throws Exception { | |
| DirCache dc = db.lockDirCache(); | |
| DirCacheEditor dce = dc.editor(); | |
| final String UNNORMALIZED = "target/"; | |
| final byte[] UNNORMALIZED_BYTES = Constants.encode(UNNORMALIZED); | |
| try (ObjectInserter oi = db.newObjectInserter()) { | |
| final ObjectId linkid = oi.insert(Constants.OBJ_BLOB, | |
| UNNORMALIZED_BYTES, 0, UNNORMALIZED_BYTES.length); | |
| dce.add(new DirCacheEditor.PathEdit("link") { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.SYMLINK); | |
| ent.setObjectId(linkid); | |
| ent.setLength(UNNORMALIZED_BYTES.length); | |
| } | |
| }); | |
| assertTrue(dce.commit()); | |
| } | |
| try (Git git = new Git(db)) { | |
| git.commit().setMessage("Adding link").call(); | |
| git.reset().setMode(ResetType.HARD).call(); | |
| DirCacheIterator dci = new DirCacheIterator(db.readDirCache()); | |
| FileTreeIterator fti = new FileTreeIterator(db); | |
| // self-check | |
| while (!fti.getEntryPathString().equals("link")) { | |
| fti.next(1); | |
| } | |
| assertEquals("link", fti.getEntryPathString()); | |
| assertEquals("link", dci.getEntryPathString()); | |
| // test | |
| assertFalse(fti.isModified(dci.getDirCacheEntry(), true, | |
| db.newObjectReader())); | |
| } | |
| } | |
| /** | |
| * Like #testSymlinkNotModifiedThoughNormalized but there is no | |
| * normalization being done. | |
| * | |
| * @throws Exception | |
| */ | |
| @Test | |
| public void testSymlinkModifiedNotNormalized() throws Exception { | |
| DirCache dc = db.lockDirCache(); | |
| DirCacheEditor dce = dc.editor(); | |
| final String NORMALIZED = "target"; | |
| final byte[] NORMALIZED_BYTES = Constants.encode(NORMALIZED); | |
| try (ObjectInserter oi = db.newObjectInserter()) { | |
| final ObjectId linkid = oi.insert(Constants.OBJ_BLOB, | |
| NORMALIZED_BYTES, 0, NORMALIZED_BYTES.length); | |
| dce.add(new DirCacheEditor.PathEdit("link") { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.SYMLINK); | |
| ent.setObjectId(linkid); | |
| ent.setLength(NORMALIZED_BYTES.length); | |
| } | |
| }); | |
| assertTrue(dce.commit()); | |
| } | |
| try (Git git = new Git(db)) { | |
| git.commit().setMessage("Adding link").call(); | |
| git.reset().setMode(ResetType.HARD).call(); | |
| DirCacheIterator dci = new DirCacheIterator(db.readDirCache()); | |
| FileTreeIterator fti = new FileTreeIterator(db); | |
| // self-check | |
| while (!fti.getEntryPathString().equals("link")) { | |
| fti.next(1); | |
| } | |
| assertEquals("link", fti.getEntryPathString()); | |
| assertEquals("link", dci.getEntryPathString()); | |
| // test | |
| assertFalse(fti.isModified(dci.getDirCacheEntry(), true, | |
| db.newObjectReader())); | |
| } | |
| } | |
| /** | |
| * Like #testSymlinkNotModifiedThoughNormalized but here the link is | |
| * modified. | |
| * | |
| * @throws Exception | |
| */ | |
| @Test | |
| public void testSymlinkActuallyModified() throws Exception { | |
| org.junit.Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); | |
| final String NORMALIZED = "target"; | |
| final byte[] NORMALIZED_BYTES = Constants.encode(NORMALIZED); | |
| try (ObjectInserter oi = db.newObjectInserter()) { | |
| final ObjectId linkid = oi.insert(Constants.OBJ_BLOB, | |
| NORMALIZED_BYTES, 0, NORMALIZED_BYTES.length); | |
| DirCache dc = db.lockDirCache(); | |
| DirCacheEditor dce = dc.editor(); | |
| dce.add(new DirCacheEditor.PathEdit("link") { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.SYMLINK); | |
| ent.setObjectId(linkid); | |
| ent.setLength(NORMALIZED_BYTES.length); | |
| } | |
| }); | |
| assertTrue(dce.commit()); | |
| } | |
| try (Git git = new Git(db)) { | |
| git.commit().setMessage("Adding link").call(); | |
| git.reset().setMode(ResetType.HARD).call(); | |
| FileUtils.delete(new File(trash, "link"), FileUtils.NONE); | |
| FS.DETECTED.createSymLink(new File(trash, "link"), "newtarget"); | |
| DirCacheIterator dci = new DirCacheIterator(db.readDirCache()); | |
| FileTreeIterator fti = new FileTreeIterator(db); | |
| // self-check | |
| while (!fti.getEntryPathString().equals("link")) { | |
| fti.next(1); | |
| } | |
| assertEquals("link", fti.getEntryPathString()); | |
| assertEquals("link", dci.getEntryPathString()); | |
| // test | |
| assertTrue(fti.isModified(dci.getDirCacheEntry(), true, | |
| db.newObjectReader())); | |
| } | |
| } | |
| @Test | |
| public void testGitignoreAndGitattributesAsDirectories() throws Exception { | |
| writeTrashFile(".gitignore/rules", "*.tmp\n"); | |
| writeTrashFile(".gitattributes/attrs", "* text=auto\n"); | |
| writeTrashFile("sub/file.tmp", "content"); | |
| // Those are regular files, nothing ignored | |
| try (TreeWalk tw = new TreeWalk(db)) { | |
| FileTreeIterator fti = new FileTreeIterator(db); | |
| tw.addTree(fti); | |
| tw.setRecursive(true); | |
| assertTrue(tw.next()); | |
| assertEquals(".gitattributes/attrs", tw.getPathString()); | |
| assertTrue(tw.next()); | |
| assertEquals(".gitignore/rules", tw.getPathString()); | |
| for (String p : paths) { | |
| assertTrue(tw.next()); | |
| assertEquals(p, tw.getPathString()); | |
| } | |
| assertTrue(tw.next()); | |
| assertEquals("sub/file.tmp", tw.getPathString()); | |
| assertFalse( | |
| tw.getTree(0, WorkingTreeIterator.class).isEntryIgnored()); | |
| assertFalse(tw.next()); | |
| } | |
| } | |
| @Test | |
| public void testGitignoreAndGitattributes() throws Exception { | |
| writeTrashFile(".gitignore", "*.tmp\n"); | |
| writeTrashFile(".gitattributes", "* text=auto\n"); | |
| writeTrashFile("sub/file.tmp", "content"); | |
| try (TreeWalk tw = new TreeWalk(db)) { | |
| FileTreeIterator fti = new FileTreeIterator(db); | |
| tw.addTree(fti); | |
| tw.setRecursive(true); | |
| assertTrue(tw.next()); | |
| assertEquals(".gitattributes", tw.getPathString()); | |
| assertNotNull(tw.getTree(0, WorkingTreeIterator.class) | |
| .getEntryAttributesNode()); | |
| assertTrue(tw.next()); | |
| assertEquals(".gitignore", tw.getPathString()); | |
| for (String p : paths) { | |
| assertTrue(tw.next()); | |
| assertEquals(p, tw.getPathString()); | |
| } | |
| assertTrue(tw.next()); | |
| assertEquals("sub/file.tmp", tw.getPathString()); | |
| assertTrue( | |
| tw.getTree(0, WorkingTreeIterator.class).isEntryIgnored()); | |
| assertFalse(tw.next()); | |
| } | |
| } | |
| @Test | |
| public void testExecutableGitignoreAndGitattributes() throws Exception { | |
| assumeTrue(db.getFS().supportsExecute()); | |
| File gitignore = writeTrashFile(".gitignore", "*.tmp\n"); | |
| File gitattributes = writeTrashFile(".gitattributes", "* text=auto\n"); | |
| assertTrue(db.getFS().setExecute(gitignore, true)); | |
| assertTrue(db.getFS().setExecute(gitattributes, true)); | |
| writeTrashFile("sub/file.tmp", "content"); | |
| try (TreeWalk tw = new TreeWalk(db)) { | |
| FileTreeIterator fti = new FileTreeIterator(db); | |
| tw.addTree(fti); | |
| tw.setRecursive(true); | |
| assertTrue(tw.next()); | |
| assertEquals(".gitattributes", tw.getPathString()); | |
| assertEquals(FileMode.EXECUTABLE_FILE, tw.getFileMode(0)); | |
| assertNotNull(tw.getTree(0, WorkingTreeIterator.class) | |
| .getEntryAttributesNode()); | |
| assertTrue(tw.next()); | |
| assertEquals(".gitignore", tw.getPathString()); | |
| assertEquals(FileMode.EXECUTABLE_FILE, tw.getFileMode(0)); | |
| for (String p : paths) { | |
| assertTrue(tw.next()); | |
| assertEquals(p, tw.getPathString()); | |
| } | |
| assertTrue(tw.next()); | |
| assertEquals("sub/file.tmp", tw.getPathString()); | |
| assertTrue( | |
| tw.getTree(0, WorkingTreeIterator.class).isEntryIgnored()); | |
| assertFalse(tw.next()); | |
| } | |
| } | |
| private static void assertEntry(String sha1string, String path, TreeWalk tw) | |
| throws MissingObjectException, IncorrectObjectTypeException, | |
| CorruptObjectException, IOException { | |
| assertTrue(tw.next()); | |
| assertEquals(path, tw.getPathString()); | |
| assertEquals(sha1string, | |
| tw.getObjectId(1).getName() /* 1=filetree here */); | |
| } | |
| private static String nameOf(AbstractTreeIterator i) { | |
| return RawParseUtils.decode(UTF_8, i.path, 0, i.pathLen); | |
| } | |
| } |