Implement the DIR_NO_GITLINKS setting with the same functionality it provides in cGit. Bug: 436200 Change-Id: I8304e42df2d7e8d7925f515805e075a92ff6ce28 Signed-off-by: Preben Ingvaldsen <preben@puppetlabs.com>tags/v4.3.0.201604071810-r
@@ -53,6 +53,7 @@ import java.io.File; | |||
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.DirCacheBuilder; | |||
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 class AddCommandTest extends RepositoryTestCase { | |||
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<String> 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 @@ public class AddCommandTest extends RepositoryTestCase { | |||
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); | |||
} | |||
} | |||
} |
@@ -64,7 +64,12 @@ 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.*; | |||
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 class FileTreeIteratorTest extends RepositoryTestCase { | |||
} | |||
} | |||
@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 class FileTreeIteratorTest extends RepositoryTestCase { | |||
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 class FileTreeIteratorTest extends RepositoryTestCase { | |||
walk.addTree(indexIter); | |||
walk.addTree(workTreeIter); | |||
walk.setFilter(PathFilter.create(path)); | |||
assertTrue(walk.next()); | |||
assertTrue(indexIter.idEqual(workTreeIter)); | |||
} |
@@ -45,6 +45,7 @@ package org.eclipse.jgit.api; | |||
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 class AddCommand extends GitCommand<DirCache> { | |||
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. |
@@ -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"; | |||
@@ -729,4 +729,13 @@ public abstract class AbstractTreeIterator { | |||
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; | |||
} | |||
} |
@@ -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 class FileTreeIterator extends WorkingTreeIterator { | |||
} | |||
} | |||
/** | |||
* 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 |
@@ -1187,7 +1187,12 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { | |||
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(); |
@@ -264,7 +264,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { | |||
// 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 abstract class WorkingTreeIterator extends AbstractTreeIterator { | |||
return zeroid; | |||
} | |||
@Override | |||
public boolean isWorkTree() { | |||
return true; | |||
} | |||
/** | |||
* Get submodule id for given entry. | |||
* | |||
@@ -916,17 +921,31 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { | |||
*/ | |||
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; | |||
} | |||
@@ -73,6 +73,8 @@ public class WorkingTreeOptions { | |||
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 @@ public class WorkingTreeOptions { | |||
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 class WorkingTreeOptions { | |||
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; } | |||
} |