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;
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;
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 {
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);
+ }
+ }
}
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;
}
}
+ @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");
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);
walk.addTree(indexIter);
walk.addTree(workTreeIter);
walk.setFilter(PathFilter.create(path));
-
+
assertTrue(walk.next());
assertTrue(indexIter.idEqual(workTreeIter));
}
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;
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.
*/
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";
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;
+ }
}
* 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);
}
/**
}
}
+ /**
+ * 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
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();
// 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) {
return zeroid;
}
+ @Override
+ public boolean isWorkTree() {
+ return true;
+ }
+
/**
* Get submodule id for given entry.
*
*/
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;
}
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);
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. */
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; }
}