From 403f04d8dd744a6cc1c2a61540c34483b94c06b5 Mon Sep 17 00:00:00 2001 From: Preben Ingvaldsen Date: Tue, 1 Mar 2016 11:37:54 -0800 Subject: [PATCH] Implement DIR_NO_GITLINKS Implement the DIR_NO_GITLINKS setting with the same functionality it provides in cGit. Bug: 436200 Change-Id: I8304e42df2d7e8d7925f515805e075a92ff6ce28 Signed-off-by: Preben Ingvaldsen --- .../org/eclipse/jgit/api/AddCommandTest.java | 116 +++++++++++++++++- .../jgit/treewalk/FileTreeIteratorTest.java | 42 ++++++- .../src/org/eclipse/jgit/api/AddCommand.java | 6 +- .../org/eclipse/jgit/lib/ConfigConstants.java | 6 + .../jgit/treewalk/AbstractTreeIterator.java | 9 ++ .../jgit/treewalk/FileTreeIterator.java | 34 ++++- .../org/eclipse/jgit/treewalk/TreeWalk.java | 7 +- .../jgit/treewalk/WorkingTreeIterator.java | 35 ++++-- .../jgit/treewalk/WorkingTreeOptions.java | 13 ++ 9 files changed, 248 insertions(+), 20 deletions(-) diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java index 4fefdfdda..126ca5cdd 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java @@ -53,6 +53,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.PrintWriter; +import java.util.Set; import org.eclipse.jgit.api.errors.FilterFailedException; import org.eclipse.jgit.api.errors.GitAPIException; @@ -62,14 +63,11 @@ import org.eclipse.jgit.dircache.DirCacheEntry; 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.ObjectInserter; -import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.lib.*; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.junit.Test; @@ -1002,6 +1000,91 @@ public boolean isCaseSensitive() { assertEquals(FileMode.EXECUTABLE_FILE, walk.getFileMode(0)); } + @Test + public void testAddGitlink() throws Exception { + createNestedRepo("git-link-dir"); + try (Git git = new Git(db)) { + git.add().addFilepattern("git-link-dir").call(); + + assertEquals( + "[git-link-dir, mode:160000]", + indexState(0)); + Set untrackedFiles = git.status().call().getUntracked(); + assert (untrackedFiles.isEmpty()); + } + + } + + @Test + public void testAddSubrepoWithDirNoGitlinks() throws Exception { + createNestedRepo("nested-repo"); + + // Set DIR_NO_GITLINKS + StoredConfig config = db.getConfig(); + config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, true); + config.save(); + + assert (db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks()); + + try (Git git = new Git(db)) { + git.add().addFilepattern("nested-repo").call(); + + assertEquals( + "[nested-repo/README1.md, mode:100644]" + + "[nested-repo/README2.md, mode:100644]", + indexState(0)); + } + + // Turn off DIR_NO_GITLINKS, ensure nested-repo is still treated as + // a normal directory + // Set DIR_NO_GITLINKS + config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, false); + config.save(); + + writeTrashFile("nested-repo", "README3.md", "content"); + + try (Git git = new Git(db)) { + git.add().addFilepattern("nested-repo").call(); + + assertEquals( + "[nested-repo/README1.md, mode:100644]" + + "[nested-repo/README2.md, mode:100644]" + + "[nested-repo/README3.md, mode:100644]", + indexState(0)); + } + } + + @Test + public void testAddGitlinkDoesNotChange() throws Exception { + createNestedRepo("nested-repo"); + + try (Git git = new Git(db)) { + git.add().addFilepattern("nested-repo").call(); + + assertEquals( + "[nested-repo, mode:160000]", + indexState(0)); + } + + // Set DIR_NO_GITLINKS + StoredConfig config = db.getConfig(); + config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, true); + config.save(); + + assert (db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks()); + + try (Git git = new Git(db)) { + git.add().addFilepattern("nested-repo").call(); + + assertEquals( + "[nested-repo, mode:160000]", + indexState(0)); + } + } + private static DirCacheEntry addEntryToBuilder(String path, File file, ObjectInserter newObjectInserter, DirCacheBuilder builder, int stage) throws IOException { @@ -1029,4 +1112,25 @@ private void assumeUnchanged(String path) throws IOException { throw new IOException("could not commit"); } + private void createNestedRepo(String path) throws IOException { + File gitLinkDir = new File(db.getWorkTree(), path); + FileUtils.mkdir(gitLinkDir); + + FileRepositoryBuilder nestedBuilder = new FileRepositoryBuilder(); + nestedBuilder.setWorkTree(gitLinkDir); + + Repository nestedRepo = nestedBuilder.build(); + nestedRepo.create(); + + writeTrashFile(path, "README1.md", "content"); + writeTrashFile(path, "README2.md", "content"); + + // Commit these changes in the subrepo + try (Git git = new Git(nestedRepo)) { + git.add().addFilepattern(".").call(); + git.commit().setMessage("subrepo commit").call(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } } 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 f80e117cf..bf1d0e6d2 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 @@ -64,7 +64,12 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; -import org.eclipse.jgit.lib.*; +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.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.treewalk.WorkingTreeIterator.MetadataDiff; @@ -279,6 +284,37 @@ public void testDirCacheMatchingId() throws Exception { } } + @Test + public void testTreewalkEnterSubtree() throws Exception { + try (Git git = new Git(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); + + TreeWalk tw = new TreeWalk(db); + 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"); @@ -345,7 +381,7 @@ public void submoduleHeadMatchesIndex() throws Exception { DirCache cache = db.lockDirCache(); DirCacheEditor editor = cache.editor(); editor.add(new PathEdit(path) { - + public void apply(DirCacheEntry ent) { ent.setFileMode(FileMode.GITLINK); ent.setObjectId(id); @@ -362,7 +398,7 @@ public void apply(DirCacheEntry ent) { walk.addTree(indexIter); walk.addTree(workTreeIter); walk.setFilter(PathFilter.create(path)); - + assertTrue(walk.next()); assertTrue(indexIter.idEqual(workTreeIter)); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java index 3b94f16f1..1f37833a4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java @@ -45,6 +45,7 @@ import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.eclipse.jgit.lib.FileMode.GITLINK; +import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK; import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; import java.io.IOException; @@ -201,7 +202,10 @@ public DirCache call() throws GitAPIException, NoFilepatternException { continue; } - if (f.getEntryRawMode() == TYPE_TREE) { + if ((f.getEntryRawMode() == TYPE_TREE + && f.getIndexFileMode(c) != FileMode.GITLINK) || + (f.getEntryRawMode() == TYPE_GITLINK + && f.getIndexFileMode(c) == FileMode.TREE)) { // Index entry exists and is symlink, gitlink or file, // otherwise the tree would have been entered above. // Replace the index entry by diving into tree of files. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 054d19301..abc8c269f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -235,6 +235,12 @@ public class ConfigConstants { */ public static final String CONFIG_KEY_HIDEDOTFILES = "hidedotfiles"; + /** + * The "dirnogitlinks" key + * @since 4.3 + */ + public static final String CONFIG_KEY_DIRNOGITLINKS = "dirNoGitLinks"; + /** The "precomposeunicode" key */ public static final String CONFIG_KEY_PRECOMPOSEUNICODE = "precomposeunicode"; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java index dc835e4f3..07fc829db 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java @@ -729,4 +729,13 @@ public void getName(byte[] buffer, int offset) { public String toString() { return getClass().getSimpleName() + "[" + getEntryPathString() + "]"; //$NON-NLS-1$ } + + /** + * @return whether or not this Iterator is iterating through the Work Tree + * + * @since 4.3 + */ + public boolean isWorkTree() { + return false; + } } 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 eb4f1a87d..db81e1af9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java @@ -94,7 +94,10 @@ public class FileTreeIterator extends WorkingTreeIterator { * the repository whose working tree will be scanned. */ public FileTreeIterator(Repository repo) { - this(repo, DefaultFileModeStrategy.INSTANCE); + this(repo, + repo.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks() ? + NoGitlinksStrategy.INSTANCE : + DefaultFileModeStrategy.INSTANCE); } /** @@ -291,6 +294,35 @@ public FileMode getMode(File f, FS.Attributes attributes) { } } + /** + * A FileModeStrategy that implements native git's DIR_NO_GITLINKS + * behavior. This is the same as the default FileModeStrategy, except + * all directories will be treated as directories regardless of whether + * or not they contain a .git directory or file. + * + * @since 4.3 + */ + static public class NoGitlinksStrategy implements FileModeStrategy { + + /** + * a singleton instance of the default FileModeStrategy + */ + public final static NoGitlinksStrategy INSTANCE = new NoGitlinksStrategy(); + + @Override + public FileMode getMode(File f, FS.Attributes attributes) { + if (attributes.isSymbolicLink()) { + return FileMode.SYMLINK; + } else if (attributes.isDirectory()) { + return FileMode.TREE; + } else if (attributes.isExecutable()) { + return FileMode.EXECUTABLE_FILE; + } else { + return FileMode.REGULAR_FILE; + } + } + } + /** * Wrapper for a standard Java IO file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java index aecbac11e..501d67673 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -1187,7 +1187,12 @@ public void enterSubtree() throws MissingObjectException, for (int i = 0; i < trees.length; i++) { final AbstractTreeIterator t = trees[i]; final AbstractTreeIterator n; - if (t.matches == ch && !t.eof() && FileMode.TREE.equals(t.mode)) + // If we find a GITLINK when attempting to enter a subtree, then the + // GITLINK must exist as a TREE in the index, meaning the working tree + // entry should be treated as a TREE + if (t.matches == ch && !t.eof() && + (FileMode.TREE.equals(t.mode) + || (FileMode.GITLINK.equals(t.mode) && t.isWorkTree()))) n = t.createSubtreeIterator(reader, idBuffer); else n = t.createEmptyTreeIterator(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java index ca8f9aa37..39176c621 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -264,7 +264,7 @@ public byte[] idBuffer() { // the cached index information for the path. // DirCacheIterator i = state.walk.getTree(state.dirCacheTree, - DirCacheIterator.class); + DirCacheIterator.class); if (i != null) { DirCacheEntry ent = i.getDirCacheEntry(); if (ent != null && compareMetadata(ent) == MetadataDiff.EQUAL) { @@ -289,6 +289,11 @@ public byte[] idBuffer() { return zeroid; } + @Override + public boolean isWorkTree() { + return true; + } + /** * Get submodule id for given entry. * @@ -916,17 +921,31 @@ public boolean isModified(DirCacheEntry entry, boolean forceContentCheck, */ public FileMode getIndexFileMode(final DirCacheIterator indexIter) { final FileMode wtMode = getEntryFileMode(); - if (indexIter == null) - return wtMode; - if (getOptions().isFileMode()) + if (indexIter == null) { return wtMode; + } final FileMode iMode = indexIter.getEntryFileMode(); - if (FileMode.REGULAR_FILE == wtMode - && FileMode.EXECUTABLE_FILE == iMode) + if (getOptions().isFileMode() && iMode != FileMode.GITLINK && iMode != FileMode.TREE) { + return wtMode; + } + if (!getOptions().isFileMode()) { + if (FileMode.REGULAR_FILE == wtMode + && FileMode.EXECUTABLE_FILE == iMode) { + return iMode; + } + if (FileMode.EXECUTABLE_FILE == wtMode + && FileMode.REGULAR_FILE == iMode) { + return iMode; + } + } + if (FileMode.GITLINK == iMode + && FileMode.TREE == wtMode) { return iMode; - if (FileMode.EXECUTABLE_FILE == wtMode - && FileMode.REGULAR_FILE == iMode) + } + if (FileMode.TREE == iMode + && FileMode.GITLINK == wtMode) { return iMode; + } return wtMode; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeOptions.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeOptions.java index a8990b1e9..dea07c197 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeOptions.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeOptions.java @@ -73,6 +73,8 @@ public WorkingTreeOptions parse(final Config cfg) { private final HideDotFiles hideDotFiles; + private final boolean dirNoGitLinks; + private WorkingTreeOptions(final Config rc) { fileMode = rc.getBoolean(ConfigConstants.CONFIG_CORE_SECTION, ConfigConstants.CONFIG_KEY_FILEMODE, true); @@ -87,6 +89,9 @@ private WorkingTreeOptions(final Config rc) { hideDotFiles = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_HIDEDOTFILES, HideDotFiles.DOTGITONLY); + dirNoGitLinks = rc.getBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, + false); } /** @return true if the execute bit on working files should be trusted. */ @@ -131,4 +136,12 @@ public SymLinks getSymLinks() { public HideDotFiles getHideDotFiles() { return hideDotFiles; } + + /** + * @return whether or not we treat nested repos as directories. + * If true, folders containing .git entries will not be + * treated as gitlinks. + * @since 4.3 + */ + public boolean isDirNoGitLinks() { return dirNoGitLinks; } }