diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java index df17a3e2f..f80e117cf 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java @@ -62,15 +62,14 @@ 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.FileMode; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.*; 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; @@ -537,6 +536,87 @@ public void idOffset() throws Exception { } } + private final FileTreeIterator.FileModeStrategy NO_GITLINKS_STRATEGY = + new FileTreeIterator.FileModeStrategy() { + @Override + public FileMode getMode(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 { + 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 { + 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)); + } + + private static void assertEntry(String sha1string, String path, TreeWalk tw) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java index accf4956f..6f21186c5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java @@ -78,6 +78,15 @@ public class FileTreeIterator extends WorkingTreeIterator { */ protected final FS fs; + /** + * the strategy used to compute the FileMode for a FileEntry. Can be used to + * control things such as whether to recurse into a directory or create a + * gitlink. + * + * @since 4.3 + */ + protected final FileModeStrategy fileModeStrategy; + /** * Create a new iterator to traverse the work tree and its children. * @@ -85,8 +94,24 @@ public class FileTreeIterator extends WorkingTreeIterator { * the repository whose working tree will be scanned. */ public FileTreeIterator(Repository repo) { + this(repo, DefaultFileModeStrategy.INSTANCE); + } + + /** + * Create a new iterator to traverse the work tree and its children. + * + * @param repo + * the repository whose working tree will be scanned. + * @param fileModeStrategy + * the strategy to use to determine the FileMode for a FileEntry; + * controls gitlinks etc. + * + * @since 4.3 + */ + public FileTreeIterator(Repository repo, FileModeStrategy fileModeStrategy) { this(repo.getWorkTree(), repo.getFS(), - repo.getConfig().get(WorkingTreeOptions.KEY)); + repo.getConfig().get(WorkingTreeOptions.KEY), + fileModeStrategy); initRootIterator(repo); } @@ -103,9 +128,32 @@ public FileTreeIterator(Repository repo) { * working tree options to be used */ public FileTreeIterator(final File root, FS fs, WorkingTreeOptions options) { + this(root, fs, options, DefaultFileModeStrategy.INSTANCE); + } + + /** + * Create a new iterator to traverse the given directory and its children. + * + * @param root + * the starting directory. This directory should correspond to + * the root of the repository. + * @param fs + * the file system abstraction which will be necessary to perform + * certain file system operations. + * @param options + * working tree options to be used + * @param fileModeStrategy + * the strategy to use to determine the FileMode for a FileEntry; + * controls gitlinks etc. + * + * @since 4.3 + */ + public FileTreeIterator(final File root, FS fs, WorkingTreeOptions options, + FileModeStrategy fileModeStrategy) { super(options); directory = root; this.fs = fs; + this.fileModeStrategy = fileModeStrategy; init(entries()); } @@ -114,25 +162,71 @@ public FileTreeIterator(final File root, FS fs, WorkingTreeOptions options) { * * @param p * the parent iterator we were created from. - * @param fs - * the file system abstraction which will be necessary to perform - * certain file system operations. * @param root * the subdirectory. This should be a directory contained within * the parent directory. + * @param fs + * the file system abstraction which will be necessary to perform + * certain file system operations. + * @since 4.3 + * @deprecated use {@link #FileTreeIterator(FileTreeIterator, File, FS)} + * instead. */ protected FileTreeIterator(final WorkingTreeIterator p, final File root, FS fs) { + this(p, root, fs, DefaultFileModeStrategy.INSTANCE); + } + + /** + * Create a new iterator to traverse a subdirectory. + * + * @param p + * the parent iterator we were created from. + * @param root + * the subdirectory. This should be a directory contained within + * the parent directory. + * @param fs + * the file system abstraction which will be necessary to perform + * certain file system operations. + * + * @since 4.3 + */ + protected FileTreeIterator(final FileTreeIterator p, final File root, + FS fs) { + this(p, root, fs, p.fileModeStrategy); + } + + /** + * Create a new iterator to traverse a subdirectory, given the specified + * FileModeStrategy. + * + * @param p + * the parent iterator we were created from. + * @param root + * the subdirectory. This should be a directory contained within + * the parent directory + * @param fs + * the file system abstraction which will be necessary to perform + * certain file system operations. + * @param fileModeStrategy + * the strategy to use to determine the FileMode for a given + * FileEntry. + * + * @since 4.3 + */ + protected FileTreeIterator(final WorkingTreeIterator p, final File root, + FS fs, FileModeStrategy fileModeStrategy) { super(p); directory = root; this.fs = fs; + this.fileModeStrategy = fileModeStrategy; init(entries()); } @Override public AbstractTreeIterator createSubtreeIterator(final ObjectReader reader) throws IncorrectObjectTypeException, IOException { - return new FileTreeIterator(this, ((FileEntry) current()).getFile(), fs); + return new FileTreeIterator(this, ((FileEntry) current()).getFile(), fs, fileModeStrategy); } private Entry[] entries() { @@ -141,10 +235,62 @@ private Entry[] entries() { return EOF; final Entry[] r = new Entry[all.length]; for (int i = 0; i < r.length; i++) - r[i] = new FileEntry(all[i], fs); + r[i] = new FileEntry(all[i], fs, fileModeStrategy); return r; } + /** + * An interface representing the methods used to determine the FileMode for + * a FileEntry. + * + * @since 4.3 + */ + public interface FileModeStrategy { + /** + * Compute the FileMode for a given File, based on its attributes. + * + * @param f + * the file to return a FileMode for + * @param attributes + * the attributes of a file + * @return a FileMode indicating whether the file is a regular file, a + * directory, a gitlink, etc. + */ + FileMode getMode(File f, FS.Attributes attributes); + } + + /** + * A default implementation of a FileModeStrategy; defaults to treating + * nested .git directories as gitlinks, etc. + * + * @since 4.3 + */ + static public class DefaultFileModeStrategy implements FileModeStrategy { + /** + * a singleton instance of the default FileModeStrategy + */ + public final static DefaultFileModeStrategy INSTANCE = + new DefaultFileModeStrategy(); + + @Override + public FileMode getMode(File f, FS.Attributes attributes) { + if (attributes.isSymbolicLink()) { + return FileMode.SYMLINK; + } else if (attributes.isDirectory()) { + if (new File(f, Constants.DOT_GIT).exists()) { + return FileMode.GITLINK; + } else { + return FileMode.TREE; + } + } else if (attributes.isExecutable()) { + return FileMode.EXECUTABLE_FILE; + } else { + return FileMode.REGULAR_FILE; + } + } + } + + /** * Wrapper for a standard Java IO file */ @@ -164,20 +310,27 @@ static public class FileEntry extends Entry { * file system */ public FileEntry(File f, FS fs) { + this(f, fs, DefaultFileModeStrategy.INSTANCE); + } + + /** + * Create a new file entry given the specified FileModeStrategy + * + * @param f + * file + * @param fs + * file system + * @param fileModeStrategy + * the strategy to use when determining the FileMode of a + * file; controls gitlinks etc. + * + * @since 4.3 + */ + public FileEntry(File f, FS fs, FileModeStrategy fileModeStrategy) { this.fs = fs; f = fs.normalize(f); attributes = fs.getAttributes(f); - if (attributes.isSymbolicLink()) - mode = FileMode.SYMLINK; - else if (attributes.isDirectory()) { - if (new File(f, Constants.DOT_GIT).exists()) - mode = FileMode.GITLINK; - else - mode = FileMode.TREE; - } else if (attributes.isExecutable()) - mode = FileMode.EXECUTABLE_FILE; - else - mode = FileMode.REGULAR_FILE; + mode = fileModeStrategy.getMode(f, attributes); } @Override