mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
RepoTree: add support for subrepo traversal (#4381)
Right now, no actual "functionality" is surfaced as `subrepos` is always set to False by default for now.
This commit is contained in:
+3
-3
@@ -76,9 +76,9 @@ def _ls(repo, path_info, recursive=None, dvc_only=False):
|
||||
if not recursive:
|
||||
for dname in dirs:
|
||||
info = PathInfo(root) / dname
|
||||
if not dvc_only or (
|
||||
tree.dvctree and tree.dvctree.exists(info)
|
||||
):
|
||||
# pylint:disable=protected-access
|
||||
_, dvctree = tree._get_tree_pairs(info) # noqa
|
||||
if not dvc_only or (dvctree and dvctree.exists(info)):
|
||||
dvc = tree.isdvc(info)
|
||||
path = str(info.relative_to(path_info))
|
||||
ret[path] = {
|
||||
|
||||
+142
-56
@@ -1,5 +1,11 @@
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from itertools import takewhile
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from funcy import wrap_with
|
||||
from pygtrie import StringTrie
|
||||
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
from dvc.exceptions import OutputNotFoundError
|
||||
@@ -248,25 +254,82 @@ class RepoTree(BaseTree): # pylint:disable=abstract-method
|
||||
scheme = "local"
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
def __init__(self, repo, **kwargs):
|
||||
def __init__(self, repo, subrepos=False, repo_factory=None, **kwargs):
|
||||
super().__init__(repo, {"url": repo.root_dir})
|
||||
if hasattr(repo, "dvc_dir"):
|
||||
self.dvctree = DvcTree(repo, **kwargs)
|
||||
|
||||
if not repo_factory:
|
||||
from dvc.repo import Repo
|
||||
|
||||
self.repo_factory = Repo
|
||||
else:
|
||||
# git-only erepo's do not need dvctree
|
||||
self.dvctree = None
|
||||
self.repo_factory = repo_factory
|
||||
|
||||
self._main_repo = repo
|
||||
self.root_dir = repo.root_dir
|
||||
self._traverse_subrepos = subrepos
|
||||
|
||||
self._discovered_subrepos = StringTrie(separator=os.sep)
|
||||
self._discovered_subrepos[self.root_dir] = repo
|
||||
|
||||
self._dvctrees = {}
|
||||
self._dvctree_configs = kwargs
|
||||
|
||||
if hasattr(repo, "dvc_dir"):
|
||||
self._dvctrees[repo.root_dir] = DvcTree(repo, **kwargs)
|
||||
|
||||
def _get_repo(self, path):
|
||||
repo = self._discovered_subrepos.get(path)
|
||||
if repo:
|
||||
return repo
|
||||
|
||||
prefix, repo = self._discovered_subrepos.longest_prefix(path)
|
||||
if not prefix:
|
||||
return None
|
||||
|
||||
parents = (parent.fspath for parent in PathInfo(path).parents)
|
||||
dirs = [path] + list(takewhile(lambda p: p != prefix, parents))
|
||||
dirs.reverse()
|
||||
self._update(dirs, starting_repo=repo)
|
||||
return self._discovered_subrepos.get(path)
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _update(self, dirs, starting_repo):
|
||||
repo = starting_repo
|
||||
for d in dirs:
|
||||
if self._is_dvc_repo(d):
|
||||
repo = self.repo_factory(d)
|
||||
self._dvctrees[repo.root_dir] = DvcTree(
|
||||
repo, **self._dvctree_configs
|
||||
)
|
||||
self._discovered_subrepos[d] = repo
|
||||
|
||||
def _is_dvc_repo(self, dir_path):
|
||||
if not self._traverse_subrepos:
|
||||
return False
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
repo_path = os.path.join(dir_path, Repo.DVC_DIR)
|
||||
return self._main_repo.tree.isdir(repo_path, use_dvcignore=False)
|
||||
|
||||
def _get_tree_pairs(self, path) -> Tuple["BaseTree", Optional["DvcTree"]]:
|
||||
path = os.path.abspath(path)
|
||||
repo = self._get_repo(path)
|
||||
if not repo:
|
||||
# path could be outside of the repo, so we just send them the main
|
||||
# tree instead
|
||||
return self._main_repo.tree, self._dvctrees.get(self.root_dir)
|
||||
|
||||
dvc_tree = self._dvctrees.get(repo.root_dir)
|
||||
return repo.tree, dvc_tree
|
||||
|
||||
@property
|
||||
def fetch(self):
|
||||
if self.dvctree:
|
||||
return self.dvctree.fetch
|
||||
return False
|
||||
return "fetch" in self._dvctree_configs
|
||||
|
||||
@property
|
||||
def stream(self):
|
||||
if self.dvctree:
|
||||
return self.dvctree.stream
|
||||
return False
|
||||
return "stream" in self._dvctree_configs
|
||||
|
||||
def open(
|
||||
self, path, mode="r", encoding="utf-8", **kwargs
|
||||
@@ -274,52 +337,65 @@ class RepoTree(BaseTree): # pylint:disable=abstract-method
|
||||
if "b" in mode:
|
||||
encoding = None
|
||||
|
||||
if self.dvctree and self.dvctree.exists(path):
|
||||
return self.dvctree.open(
|
||||
path, mode=mode, encoding=encoding, **kwargs
|
||||
)
|
||||
return self.repo.tree.open(path, mode=mode, encoding=encoding)
|
||||
tree, dvc_tree = self._get_tree_pairs(path)
|
||||
if dvc_tree and dvc_tree.exists(path):
|
||||
return dvc_tree.open(path, mode=mode, encoding=encoding, **kwargs)
|
||||
return tree.open(path, mode=mode, encoding=encoding)
|
||||
|
||||
def exists(
|
||||
self, path, use_dvcignore=True
|
||||
): # pylint: disable=arguments-differ
|
||||
return self.repo.tree.exists(path) or (
|
||||
self.dvctree and self.dvctree.exists(path)
|
||||
)
|
||||
tree, dvc_tree = self._get_tree_pairs(path)
|
||||
return tree.exists(path) or (dvc_tree and dvc_tree.exists(path))
|
||||
|
||||
def isdir(self, path): # pylint: disable=arguments-differ
|
||||
return self.repo.tree.isdir(path) or (
|
||||
self.dvctree and self.dvctree.isdir(path)
|
||||
)
|
||||
tree, dvc_tree = self._get_tree_pairs(path)
|
||||
return tree.isdir(path) or (dvc_tree and dvc_tree.isdir(path))
|
||||
|
||||
def isdvc(self, path, **kwargs):
|
||||
return self.dvctree is not None and self.dvctree.isdvc(path, **kwargs)
|
||||
_, dvc_tree = self._get_tree_pairs(path)
|
||||
return dvc_tree is not None and dvc_tree.isdvc(path, **kwargs)
|
||||
|
||||
def isfile(self, path): # pylint: disable=arguments-differ
|
||||
return self.repo.tree.isfile(path) or (
|
||||
self.dvctree and self.dvctree.isfile(path)
|
||||
)
|
||||
tree, dvc_tree = self._get_tree_pairs(path)
|
||||
return tree.isfile(path) or (dvc_tree and dvc_tree.isfile(path))
|
||||
|
||||
def isexec(self, path):
|
||||
if self.dvctree and self.dvctree.exists(path):
|
||||
return self.dvctree.isexec(path)
|
||||
return self.repo.tree.isexec(path)
|
||||
tree, dvc_tree = self._get_tree_pairs(path)
|
||||
if dvc_tree and dvc_tree.exists(path):
|
||||
return dvc_tree.isexec(path)
|
||||
return tree.isexec(path)
|
||||
|
||||
def stat(self, path):
|
||||
return self.repo.tree.stat(path)
|
||||
tree, _ = self._get_tree_pairs(path)
|
||||
return tree.stat(path)
|
||||
|
||||
def _walk_one(self, walk):
|
||||
def _dvc_walk(self, walk):
|
||||
try:
|
||||
root, dirs, files = next(walk)
|
||||
except StopIteration:
|
||||
return
|
||||
yield root, dirs, files
|
||||
for _ in dirs:
|
||||
yield from self._walk_one(walk)
|
||||
yield from self._dvc_walk(walk)
|
||||
|
||||
def _walk(self, dvc_walk, repo_walk, dvcfiles=False):
|
||||
def _subrepo_walk(self, dir_path, **kwargs):
|
||||
tree, dvc_tree = self._get_tree_pairs(dir_path)
|
||||
tree_walk = tree.walk(
|
||||
dir_path, topdown=True, ignore_subrepos=not self._traverse_subrepos
|
||||
)
|
||||
if dvc_tree:
|
||||
dvc_walk = dvc_tree.walk(dir_path, topdown=True, **kwargs)
|
||||
else:
|
||||
dvc_walk = None
|
||||
yield from self._walk(tree_walk, dvc_walk, **kwargs)
|
||||
|
||||
def _walk(self, repo_walk, dvc_walk=None, dvcfiles=False):
|
||||
assert repo_walk
|
||||
try:
|
||||
_, dvc_dirs, dvc_fnames = next(dvc_walk)
|
||||
_, dvc_dirs, dvc_fnames = (
|
||||
next(dvc_walk) if dvc_walk else (None, [], [])
|
||||
)
|
||||
repo_root, repo_dirs, repo_fnames = next(repo_walk)
|
||||
except StopIteration:
|
||||
return
|
||||
@@ -347,12 +423,15 @@ class RepoTree(BaseTree): # pylint:disable=abstract-method
|
||||
repo_dirs[:] = [dirname for dirname in dirs if dirname in repo_set]
|
||||
|
||||
for dirname in dirs:
|
||||
if dirname in shared:
|
||||
yield from self._walk(dvc_walk, repo_walk, dvcfiles=dvcfiles)
|
||||
dir_path = os.path.join(repo_root, dirname)
|
||||
if self._is_dvc_repo(dir_path):
|
||||
yield from self._subrepo_walk(dir_path, dvcfiles=dvcfiles)
|
||||
elif dirname in shared:
|
||||
yield from self._walk(repo_walk, dvc_walk, dvcfiles=dvcfiles)
|
||||
elif dirname in dvc_set:
|
||||
yield from self._walk_one(dvc_walk)
|
||||
yield from self._dvc_walk(dvc_walk)
|
||||
elif dirname in repo_set:
|
||||
yield from self._walk_one(repo_walk)
|
||||
yield from self._walk(repo_walk, None, dvcfiles=dvcfiles)
|
||||
|
||||
def walk(
|
||||
self, top, topdown=True, onerror=None, dvcfiles=False, **kwargs
|
||||
@@ -382,22 +461,28 @@ class RepoTree(BaseTree): # pylint:disable=abstract-method
|
||||
onerror(NotADirectoryError(top))
|
||||
return
|
||||
|
||||
dvc_exists = self.dvctree and self.dvctree.exists(top)
|
||||
repo_exists = self.repo.tree.exists(top)
|
||||
if dvc_exists and not repo_exists:
|
||||
yield from self.dvctree.walk(
|
||||
top, topdown=topdown, onerror=onerror, **kwargs
|
||||
tree, dvc_tree = self._get_tree_pairs(top)
|
||||
dvc_exists = dvc_tree and dvc_tree.exists(top)
|
||||
repo_exists = tree.exists(top)
|
||||
if dvc_exists:
|
||||
dvc_walk = dvc_tree.walk(top, topdown=topdown, **kwargs)
|
||||
if repo_exists:
|
||||
repo_walk = tree.walk(
|
||||
top,
|
||||
topdown=topdown,
|
||||
ignore_subrepos=not self._traverse_subrepos,
|
||||
)
|
||||
yield from self._walk(repo_walk, dvc_walk, dvcfiles=dvcfiles)
|
||||
else:
|
||||
yield from dvc_walk
|
||||
else:
|
||||
repo_walk = tree.walk(
|
||||
top,
|
||||
topdown=topdown,
|
||||
onerror=onerror,
|
||||
ignore_subrepos=not self._traverse_subrepos,
|
||||
)
|
||||
return
|
||||
if repo_exists and not dvc_exists:
|
||||
yield from self.repo.tree.walk(
|
||||
top, topdown=topdown, onerror=onerror
|
||||
)
|
||||
return
|
||||
|
||||
dvc_walk = self.dvctree.walk(top, topdown=topdown, **kwargs)
|
||||
repo_walk = self.repo.tree.walk(top, topdown=topdown)
|
||||
yield from self._walk(dvc_walk, repo_walk, dvcfiles=dvcfiles)
|
||||
yield from self._walk(repo_walk, None, dvcfiles=dvcfiles)
|
||||
|
||||
def walk_files(self, top, **kwargs): # pylint: disable=arguments-differ
|
||||
for root, _, files in self.walk(top, **kwargs):
|
||||
@@ -413,9 +498,10 @@ class RepoTree(BaseTree): # pylint:disable=abstract-method
|
||||
"""
|
||||
if not self.exists(path_info):
|
||||
raise FileNotFoundError
|
||||
if self.dvctree and self.dvctree.exists(path_info):
|
||||
_, dvc_tree = self._get_tree_pairs(path_info)
|
||||
if dvc_tree and dvc_tree.exists(path_info):
|
||||
try:
|
||||
return self.dvctree.get_file_hash(path_info)
|
||||
return dvc_tree.get_file_hash(path_info)
|
||||
except OutputNotFoundError:
|
||||
pass
|
||||
return file_md5(path_info, self)[0]
|
||||
@@ -444,4 +530,4 @@ class RepoTree(BaseTree): # pylint:disable=abstract-method
|
||||
|
||||
@property
|
||||
def hash_jobs(self): # pylint: disable=invalid-overridden-method
|
||||
return self.repo.tree.hash_jobs
|
||||
return self._main_repo.tree.hash_jobs
|
||||
|
||||
@@ -240,3 +240,6 @@ class GitTree(BaseTree): # pylint:disable=abstract-method
|
||||
for file in files:
|
||||
# NOTE: os.path.join is ~5.5 times slower
|
||||
yield f"{root}{os.sep}{file}"
|
||||
|
||||
def _reset(self):
|
||||
return self.__dict__.pop("dvcignore", None)
|
||||
|
||||
@@ -357,3 +357,6 @@ class LocalTree(BaseTree):
|
||||
info = self.hash_to_path_info(hash_)
|
||||
path_info = info.with_name(info.name + self.UNPACKED_DIR_SUFFIX)
|
||||
self.remove(path_info)
|
||||
|
||||
def _reset(self):
|
||||
return self.__dict__.pop("dvcignore", None)
|
||||
|
||||
+16
-16
@@ -22,7 +22,7 @@ def test_ignore(tmp_dir, dvc, monkeypatch):
|
||||
tmp_dir.gen({"dir": {"ignored": "text", "other": "text2"}})
|
||||
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "dir/ignored")
|
||||
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
|
||||
path = PathInfo(tmp_dir)
|
||||
|
||||
@@ -32,7 +32,7 @@ def test_ignore(tmp_dir, dvc, monkeypatch):
|
||||
def test_ignore_unicode(tmp_dir, dvc):
|
||||
tmp_dir.gen({"dir": {"other": "text", "тест": "проверка"}})
|
||||
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "dir/тест")
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
path = PathInfo(tmp_dir)
|
||||
assert set(dvc.tree.walk_files(path / "dir")) == {path / "dir" / "other"}
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_rename_ignored_file(tmp_dir, dvc):
|
||||
tmp_dir.gen({"dir": {"ignored": "...", "other": "text"}})
|
||||
|
||||
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "ignored*")
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
|
||||
mtime, size = get_mtime_and_size("dir", dvc.tree)
|
||||
|
||||
@@ -64,7 +64,7 @@ def test_rename_file(tmp_dir, dvc):
|
||||
def test_remove_ignored_file(tmp_dir, dvc):
|
||||
tmp_dir.gen({"dir": {"ignored": "...", "other": "text"}})
|
||||
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "dir/ignored")
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
|
||||
mtime, size = get_mtime_and_size("dir", dvc.tree)
|
||||
|
||||
@@ -97,7 +97,7 @@ def test_ignore_collecting_dvcignores(tmp_dir, dvc, dname):
|
||||
|
||||
top_ignore_file = (tmp_dir / dname).with_name(DvcIgnore.DVCIGNORE_FILE)
|
||||
top_ignore_file.write_text(os.path.basename(dname))
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
|
||||
ignore_file = tmp_dir / dname / DvcIgnore.DVCIGNORE_FILE
|
||||
ignore_file.write_text("foo")
|
||||
@@ -128,7 +128,7 @@ def test_ignore_on_branch(tmp_dir, scm, dvc):
|
||||
with tmp_dir.branch("branch", new=True):
|
||||
tmp_dir.scm_gen(DvcIgnore.DVCIGNORE_FILE, "foo", commit="add ignore")
|
||||
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
path = PathInfo(tmp_dir)
|
||||
assert set(dvc.tree.walk_files(path)) == {
|
||||
path / "foo",
|
||||
@@ -152,7 +152,7 @@ def test_match_nested(tmp_dir, dvc):
|
||||
"dir": {"x.backup": "x backup", "tmp": "content"},
|
||||
}
|
||||
)
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
result = {os.fspath(os.path.normpath(f)) for f in dvc.tree.walk_files(".")}
|
||||
assert result == {".dvcignore", "foo"}
|
||||
|
||||
@@ -177,7 +177,7 @@ def test_ignore_subrepo(tmp_dir, scm, dvc):
|
||||
tmp_dir.gen({".dvcignore": "foo", "subdir": {"foo": "foo"}})
|
||||
scm.add([".dvcignore"])
|
||||
scm.commit("init parent dvcignore")
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
|
||||
subrepo_dir = tmp_dir / "subdir"
|
||||
assert not dvc.tree.exists(PathInfo(subrepo_dir / "foo"))
|
||||
@@ -198,7 +198,7 @@ def test_ignore_resurface_subrepo(tmp_dir, scm, dvc):
|
||||
with subrepo_dir.chdir():
|
||||
Repo.init(subdir=True)
|
||||
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
|
||||
dirs = ["subdir"]
|
||||
files = ["foo"]
|
||||
@@ -216,7 +216,7 @@ def test_ignore_resurface_subrepo(tmp_dir, scm, dvc):
|
||||
def test_ignore_blank_line(tmp_dir, dvc):
|
||||
tmp_dir.gen({"dir": {"ignored": "text", "other": "text2"}})
|
||||
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "foo\n\ndir/ignored")
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
path = PathInfo(tmp_dir)
|
||||
assert set(dvc.tree.walk_files(path / "dir")) == {path / "dir" / "other"}
|
||||
|
||||
@@ -251,7 +251,7 @@ def test_ignore_file_in_parent_path(
|
||||
):
|
||||
tmp_dir.gen(data_struct)
|
||||
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "\n".join(pattern_list))
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
path = PathInfo(tmp_dir)
|
||||
assert set(dvc.tree.walk_files(path / "dir")) == {
|
||||
path / relpath for relpath in result_set
|
||||
@@ -274,7 +274,7 @@ def test_ignore_sub_directory(tmp_dir, dvc):
|
||||
)
|
||||
tmp_dir.gen({"dir": {DvcIgnore.DVCIGNORE_FILE: "doc/fortz"}})
|
||||
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
path = PathInfo(tmp_dir)
|
||||
assert set(dvc.tree.walk_files(path / "dir")) == {
|
||||
path / "dir" / "a" / "doc" / "fortz" / "a",
|
||||
@@ -286,7 +286,7 @@ def test_ignore_sub_directory(tmp_dir, dvc):
|
||||
def test_ignore_directory(tmp_dir, dvc):
|
||||
tmp_dir.gen({"dir": {"fortz": {}, "a": {"fortz": {}}}})
|
||||
tmp_dir.gen({"dir": {DvcIgnore.DVCIGNORE_FILE: "fortz"}})
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
path = PathInfo(tmp_dir)
|
||||
assert set(dvc.tree.walk_files(path / "dir")) == {
|
||||
path / "dir" / DvcIgnore.DVCIGNORE_FILE,
|
||||
@@ -297,7 +297,7 @@ def test_multi_ignore_file(tmp_dir, dvc, monkeypatch):
|
||||
tmp_dir.gen({"dir": {"subdir": {"should_ignore": "1", "not_ignore": "1"}}})
|
||||
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "dir/subdir/*_ignore")
|
||||
tmp_dir.gen({"dir": {DvcIgnore.DVCIGNORE_FILE: "!subdir/not_ignore"}})
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
path = PathInfo(tmp_dir)
|
||||
assert set(dvc.tree.walk_files(path / "dir")) == {
|
||||
path / "dir" / "subdir" / "not_ignore",
|
||||
@@ -322,7 +322,7 @@ def test_pattern_trie_tree(tmp_dir, dvc):
|
||||
"other": {DvcIgnore.DVCIGNORE_FILE: "1\n2\n3"},
|
||||
}
|
||||
)
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
dvcignore = dvc.tree.dvcignore
|
||||
|
||||
ignore_pattern_top = dvcignore._get_trie_pattern(
|
||||
@@ -390,7 +390,7 @@ def test_ignore_in_added_dir(tmp_dir, dvc):
|
||||
".dvcignore": "**/ignored",
|
||||
}
|
||||
)
|
||||
dvc.tree.__dict__.pop("dvcignore", None)
|
||||
dvc.tree._reset()
|
||||
|
||||
ignored_path = tmp_dir / "dir" / "sub" / "ignored"
|
||||
assert not dvc.tree.exists(PathInfo(ignored_path))
|
||||
|
||||
@@ -258,7 +258,7 @@ def test_walk_dont_ignore_subrepos(tmp_dir, scm, dvc):
|
||||
scm.commit("Add subrepo")
|
||||
|
||||
dvc_tree = dvc.tree
|
||||
dvc_tree.__dict__.pop("dvcignore")
|
||||
dvc_tree._reset()
|
||||
scm_tree = scm.get_tree("HEAD", use_dvcignore=True)
|
||||
path = os.fspath(tmp_dir)
|
||||
get_dirs = itemgetter(1)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -177,3 +178,181 @@ def test_isdvc(tmp_dir, dvc):
|
||||
assert tree.isdvc("dir")
|
||||
assert not tree.isdvc("dir/baz")
|
||||
assert tree.isdvc("dir/baz", recursive=True, strict=False)
|
||||
|
||||
|
||||
def make_subrepo(dir_, scm, config=None):
|
||||
dir_.mkdir(parents=True)
|
||||
with dir_.chdir():
|
||||
dir_.scm = scm
|
||||
dir_.init(dvc=True, subdir=True)
|
||||
if config:
|
||||
dir_.add_remote(config=config)
|
||||
|
||||
|
||||
def test_subrepos(tmp_dir, scm, dvc):
|
||||
tmp_dir.scm_gen(
|
||||
{"dir": {"repo.txt": "file to confuse RepoTree"}},
|
||||
commit="dir/repo.txt",
|
||||
)
|
||||
|
||||
subrepo1 = tmp_dir / "dir" / "repo"
|
||||
subrepo2 = tmp_dir / "dir" / "repo2"
|
||||
|
||||
for repo in [subrepo1, subrepo2]:
|
||||
make_subrepo(repo, scm)
|
||||
|
||||
subrepo1.dvc_gen({"foo": "foo", "dir1": {"bar": "bar"}}, commit="FOO")
|
||||
subrepo2.dvc_gen(
|
||||
{"lorem": "lorem", "dir2": {"ipsum": "ipsum"}}, commit="BAR"
|
||||
)
|
||||
|
||||
dvc.tree._reset()
|
||||
tree = RepoTree(dvc, subrepos=True, fetch=True)
|
||||
|
||||
def assert_tree_belongs_to_repo(ret_val):
|
||||
method = tree._get_repo
|
||||
|
||||
def f(*args, **kwargs):
|
||||
r = method(*args, **kwargs)
|
||||
assert r.root_dir == ret_val.root_dir
|
||||
return r
|
||||
|
||||
return f
|
||||
|
||||
with mock.patch.object(
|
||||
tree,
|
||||
"_get_repo",
|
||||
side_effect=assert_tree_belongs_to_repo(subrepo1.dvc),
|
||||
):
|
||||
assert tree.exists(subrepo1 / "foo") is True
|
||||
assert tree.exists(subrepo1 / "bar") is False
|
||||
|
||||
assert tree.isfile(subrepo1 / "foo") is True
|
||||
assert tree.isfile(subrepo1 / "foo") is True
|
||||
assert tree.isfile(subrepo1 / "dir1" / "bar") is True
|
||||
assert tree.isfile(subrepo1 / "dir1") is False
|
||||
|
||||
assert tree.isdir(subrepo1 / "dir1") is True
|
||||
assert tree.isdir(subrepo1 / "dir1" / "bar") is False
|
||||
assert tree.isdvc(subrepo1 / "foo") is True
|
||||
|
||||
with mock.patch.object(
|
||||
tree,
|
||||
"_get_repo",
|
||||
side_effect=assert_tree_belongs_to_repo(subrepo2.dvc),
|
||||
):
|
||||
assert tree.exists(subrepo2 / "lorem") is True
|
||||
assert tree.exists(subrepo2 / "ipsum") is False
|
||||
|
||||
assert tree.isfile(subrepo2 / "lorem") is True
|
||||
assert tree.isfile(subrepo2 / "lorem") is True
|
||||
assert tree.isfile(subrepo2 / "dir2" / "ipsum") is True
|
||||
assert tree.isfile(subrepo2 / "dir2") is False
|
||||
|
||||
assert tree.isdir(subrepo2 / "dir2") is True
|
||||
assert tree.isdir(subrepo2 / "dir2" / "ipsum") is False
|
||||
assert tree.isdvc(subrepo2 / "lorem") is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dvcfiles,extra_expected",
|
||||
[
|
||||
(False, []),
|
||||
(
|
||||
True,
|
||||
[
|
||||
PathInfo("dir") / "repo" / "foo.dvc",
|
||||
PathInfo("dir") / "repo" / "dir1.dvc",
|
||||
PathInfo("dir") / "repo2" / "lorem.dvc",
|
||||
PathInfo("dir") / "repo2" / "dir2.dvc",
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_subrepo_walk(tmp_dir, scm, dvc, dvcfiles, extra_expected):
|
||||
tmp_dir.scm_gen(
|
||||
{"dir": {"repo.txt": "file to confuse RepoTree"}},
|
||||
commit="dir/repo.txt",
|
||||
)
|
||||
|
||||
subrepo1 = tmp_dir / "dir" / "repo"
|
||||
subrepo2 = tmp_dir / "dir" / "repo2"
|
||||
|
||||
subdirs = [subrepo1, subrepo2]
|
||||
for dir_ in subdirs:
|
||||
make_subrepo(dir_, scm)
|
||||
|
||||
subrepo1.dvc_gen({"foo": "foo", "dir1": {"bar": "bar"}}, commit="FOO")
|
||||
subrepo2.dvc_gen(
|
||||
{"lorem": "lorem", "dir2": {"ipsum": "ipsum"}}, commit="BAR"
|
||||
)
|
||||
|
||||
# using tree that does not have dvcignore
|
||||
dvc.tree._reset()
|
||||
tree = RepoTree(dvc, subrepos=True, fetch=True)
|
||||
expected = [
|
||||
PathInfo("dir") / "repo",
|
||||
PathInfo("dir") / "repo.txt",
|
||||
PathInfo("dir") / "repo2",
|
||||
PathInfo("dir") / "repo" / ".gitignore",
|
||||
PathInfo("dir") / "repo" / "foo",
|
||||
PathInfo("dir") / "repo" / "dir1",
|
||||
PathInfo("dir") / "repo" / "dir1" / "bar",
|
||||
PathInfo("dir") / "repo2" / ".gitignore",
|
||||
PathInfo("dir") / "repo2" / "lorem",
|
||||
PathInfo("dir") / "repo2" / "dir2",
|
||||
PathInfo("dir") / "repo2" / "dir2" / "ipsum",
|
||||
]
|
||||
|
||||
actual = []
|
||||
for root, dirs, files in tree.walk("dir", dvcfiles=dvcfiles):
|
||||
for entry in dirs + files:
|
||||
actual.append(os.path.join(root, entry))
|
||||
|
||||
expected = [str(path) for path in expected + extra_expected]
|
||||
assert set(actual) == set(expected)
|
||||
assert len(actual) == len(expected)
|
||||
|
||||
|
||||
def test_repo_tree_no_subrepos(tmp_dir, dvc, scm):
|
||||
tmp_dir.scm_gen(
|
||||
{"dir": {"repo.txt": "file to confuse RepoTree"}},
|
||||
commit="dir/repo.txt",
|
||||
)
|
||||
tmp_dir.dvc_gen({"lorem": "lorem"}, commit="add foo")
|
||||
|
||||
subrepo = tmp_dir / "dir" / "repo"
|
||||
make_subrepo(subrepo, scm)
|
||||
subrepo.dvc_gen({"foo": "foo", "dir1": {"bar": "bar"}}, commit="FOO")
|
||||
subrepo.scm_gen({"ipsum": "ipsum"}, commit="BAR")
|
||||
|
||||
# using tree that does not have dvcignore
|
||||
dvc.tree._reset()
|
||||
tree = RepoTree(dvc, subrepos=False, fetch=True)
|
||||
expected = [
|
||||
tmp_dir / ".gitignore",
|
||||
tmp_dir / "lorem",
|
||||
tmp_dir / "lorem.dvc",
|
||||
tmp_dir / "dir",
|
||||
tmp_dir / "dir" / "repo.txt",
|
||||
]
|
||||
|
||||
actual = []
|
||||
for root, dirs, files in tree.walk(tmp_dir, dvcfiles=True):
|
||||
for entry in dirs + files:
|
||||
actual.append(os.path.normpath(os.path.join(root, entry)))
|
||||
|
||||
expected = [str(path) for path in expected]
|
||||
assert set(actual) == set(expected)
|
||||
assert len(actual) == len(expected)
|
||||
|
||||
assert tree.isfile(tmp_dir / "lorem") is True
|
||||
assert tree.isfile(tmp_dir / "dir" / "repo" / "foo") is False
|
||||
assert tree.isdir(tmp_dir / "dir" / "repo") is False
|
||||
assert tree.isdir(tmp_dir / "dir") is True
|
||||
|
||||
assert tree.isdvc(tmp_dir / "lorem") is True
|
||||
assert tree.isdvc(tmp_dir / "dir" / "repo" / "dir1") is False
|
||||
|
||||
assert tree.exists(tmp_dir / "dir" / "repo.txt") is True
|
||||
assert tree.exists(tmp_dir / "repo" / "ipsum") is False
|
||||
|
||||
Reference in New Issue
Block a user