Repository: iterative/dvc

Issue:
dvc metrics -T raises exception if there are tags unrelated to the DVC sub-directory
DVC version: 0.94.0
Python version: 3.6.7
Platform: Windows-10-10.0.18362-SP0
Binary: False
Package: pip
Supported remotes: http, https
Filesystem type (workspace): ('NTFS', 'C:\\')

```
NT> dvc metrics show -T
ERROR: unexpected error - [Errno 2] No such file
 
Having any troubles? Hit us up at https://dvc.org/support, we are always happy to help!
NT> dvc metrics show -T -v
2020-05-18 08:19:19,707 DEBUG: Trying to spawn '['c:\\program files\\python36\\python.exe', 'C:\\Program Files\\Python36
\\Scripts\\dvc', 'daemon', '-q', 'updater']'
2020-05-18 08:19:19,778 DEBUG: Spawned '['c:\\program files\\python36\\python.exe', 'C:\\Program Files\\Python36\\Script
s\\dvc', 'daemon', '-q', 'updater']'
2020-05-18 08:19:19,794 DEBUG: PRAGMA user_version;
2020-05-18 08:19:19,794 DEBUG: fetched: [(3,)]
2020-05-18 08:19:19,794 DEBUG: CREATE TABLE IF NOT EXISTS state (inode INTEGER PRIMARY KEY, mtime TEXT NOT NULL, size TE
XT NOT NULL, md5 TEXT NOT NULL, timestamp TEXT NOT NULL)
2020-05-18 08:19:19,825 DEBUG: CREATE TABLE IF NOT EXISTS state_info (count INTEGER)
2020-05-18 08:19:19,825 DEBUG: CREATE TABLE IF NOT EXISTS link_state (path TEXT PRIMARY KEY, inode INTEGER NOT NULL, mti
me TEXT NOT NULL)
2020-05-18 08:19:19,825 DEBUG: INSERT OR IGNORE INTO state_info (count) SELECT 0 WHERE NOT EXISTS (SELECT * FROM state_i
nfo)
2020-05-18 08:19:19,825 DEBUG: PRAGMA user_version = 3;
2020-05-18 08:19:19,965 DEBUG: SELECT count from state_info WHERE rowid=?
2020-05-18 08:19:19,981 DEBUG: fetched: [(38,)]
2020-05-18 08:19:19,981 DEBUG: UPDATE state_info SET count = ? WHERE rowid = ?
2020-05-18 08:19:20,059 ERROR: unexpected error - [Errno 2] No such file
------------------------------------------------------------
Traceback (most recent call last):
  File "c:\program files\python36\lib\site-packages\dvc\main.py", line 49, in main
    ret = cmd.run()
  File "c:\program files\python36\lib\site-packages\dvc\command\metrics.py", line 49, in run
    recursive=self.args.recursive,
  File "c:\program files\python36\lib\site-packages\dvc\repo\metrics\__init__.py", line 13, in show
    return show(self.repo, *args, **kwargs)
  File "c:\program files\python36\lib\site-packages\dvc\repo\__init__.py", line 30, in wrapper
    ret = f(repo, *args, **kwargs)
  File "c:\program files\python36\lib\site-packages\dvc\repo\metrics\show.py", line 99, in show
    metrics = _collect_metrics(repo, targets, recursive)
  File "c:\program files\python36\lib\site-packages\dvc\repo\metrics\show.py", line 18, in _collect_metrics
    for stage in repo.stages:
  File "c:\program files\python36\lib\site-packages\funcy\objects.py", line 28, in __get__
    res = instance.__dict__[self.fget.__name__] = self.fget(instance)
  File "c:\program files\python36\lib\site-packages\dvc\repo\__init__.py", line 424, in stages
    return self._collect_stages()
  File "c:\program files\python36\lib\site-packages\dvc\repo\__init__.py", line 432, in _collect_stages
    for root, dirs, files in self.tree.walk(self.root_dir):
  File "c:\program files\python36\lib\site-packages\dvc\ignore.py", line 137, in walk
    for root, dirs, files in self.tree.walk(top, topdown):
  File "c:\program files\python36\lib\site-packages\dvc\scm\git\tree.py", line 139, in walk
    raise IOError(errno.ENOENT, "No such file")
FileNotFoundError: [Errno 2] No such file
------------------------------------------------------------
 
Having any troubles? Hit us up at https://dvc.org/support, we are always happy to help!
 ```

My repo has lots of other code including other tags unrelated to DVC. I have initialised DVC and set up a pipeline in a sub-directory. I have a single tag that relates to a metric file and a few other tags that existed before DVC was initialised.
 
Does it expect all tags to contain metrics? It would seem so, given my “fix” below.
 
If I insert some diagnostics I can see that it is looking at a tag unrelated to my DVC directory and finding that the sub-directory does not exist in the GIT tree for a tag.
 
If I insert a try/except in site-packages/dvc/ignore.py:CleanTree:walk(), I can get it to work. I don’t suppose this is the best fix for this issue, but I’m unfamiliar with this code, so I’m not sure where it is iterating through the tags and I shouldn’t spend much more time on this.

__Suggestion__:
You might imagine a requirement where you only want to see metrics in tags that were branched from where “dvc init” was called, or only tags that are related to the sub-directory where “dvc init” was called.


Discussion:
Reproducible with

```py
def test_show_with_preexisting_tags(tmp_dir, scm):
    tmp_dir.gen("foo", "foo")
    scm.add("foo")
    scm.commit("init")
    scm.tag("no-metrics")

    tmp_dir.gen({"subdir": {}})
    subrepo_dir = tmp_dir / "subdir"
    with subrepo_dir.chdir():
        dvc = Repo.init(subdir=True)
        scm.commit("init dvc")

        subrepo_dir.gen("metrics.yaml", "foo: 1")
        dvc.run(metrics=["metrics.yaml"], single_stage=True)

    scm.add(
        [
            str(subrepo_dir / "metrics.yaml"),
            str(subrepo_dir / "metrics.yaml.dvc"),
        ]
    )
    scm.commit("init metrics")
    scm.tag("v1")

    assert dvc.metrics.show(all_tags=True) == {
        "working tree": {"subdir/metrics.yaml": {"foo": 1}},
        "v1": {"subdir/metrics.yaml": {"foo": 1}},
    }
```

Also worth noting it is only reproducible if the tag was for a commit before DVC was initialized AND DVC was initialized with `subdir=True`. I cannot reproduce the issue if DVC is initialized with `subdir=False`.

Resolving patch:
diff --git a/dvc/ignore.py b/dvc/ignore.py
--- a/dvc/ignore.py
+++ b/dvc/ignore.py
@@ -115,8 +115,12 @@ def __call__(self, root, dirs, files):
 
 
 class CleanTree(BaseTree):
-    def __init__(self, tree):
+    def __init__(self, tree, tree_root=None):
         self.tree = tree
+        if tree_root:
+            self._tree_root = tree_root
+        else:
+            self._tree_root = self.tree.tree_root
 
     @cached_property
     def dvcignore(self):
@@ -124,7 +128,7 @@ def dvcignore(self):
 
     @property
     def tree_root(self):
-        return self.tree.tree_root
+        return self._tree_root
 
     def open(self, path, mode="r", encoding="utf-8"):
         if self.isfile(path):
@@ -146,8 +150,11 @@ def isdir(self, path):
         )
 
     def _valid_dirname(self, path):
-        dirname, basename = os.path.split(os.path.normpath(path))
-        dirs, _ = self.dvcignore(os.path.abspath(dirname), [basename], [])
+        path = os.path.abspath(path)
+        if path == self.tree_root:
+            return True
+        dirname, basename = os.path.split(path)
+        dirs, _ = self.dvcignore(dirname, [basename], [])
         if dirs:
             return True
         return False
diff --git a/dvc/repo/__init__.py b/dvc/repo/__init__.py
--- a/dvc/repo/__init__.py
+++ b/dvc/repo/__init__.py
@@ -17,6 +17,7 @@
 from dvc.ignore import CleanTree
 from dvc.path_info import PathInfo
 from dvc.repo.tree import RepoTree
+from dvc.scm.tree import is_working_tree
 from dvc.utils.fs import path_isin
 
 from ..stage.exceptions import StageFileDoesNotExistError, StageNotFound
@@ -133,7 +134,13 @@ def tree(self):
 
     @tree.setter
     def tree(self, tree):
-        self._tree = tree if isinstance(tree, CleanTree) else CleanTree(tree)
+        if is_working_tree(tree) or tree.tree_root == self.root_dir:
+            root = None
+        else:
+            root = self.root_dir
+        self._tree = (
+            tree if isinstance(tree, CleanTree) else CleanTree(tree, root)
+        )
         # Our graph cache is no longer valid, as it was based on the previous
         # tree.
         self._reset()
diff --git a/dvc/repo/brancher.py b/dvc/repo/brancher.py
--- a/dvc/repo/brancher.py
+++ b/dvc/repo/brancher.py
@@ -48,6 +48,9 @@ def brancher(  # noqa: E302
         if revs:
             for sha, names in group_by(scm.resolve_rev, revs).items():
                 self.tree = scm.get_tree(sha)
-                yield ", ".join(names)
+                # ignore revs that don't contain repo root
+                # (i.e. revs from before a subdir=True repo was init'ed)
+                if self.tree.exists(self.root_dir):
+                    yield ", ".join(names)
     finally:
         self.tree = saved_tree


Test patch:
diff --git a/tests/func/metrics/test_show.py b/tests/func/metrics/test_show.py
--- a/tests/func/metrics/test_show.py
+++ b/tests/func/metrics/test_show.py
@@ -1,5 +1,8 @@
+import os
+
 import pytest
 
+from dvc.repo import Repo
 from dvc.repo.metrics.show import NoMetricsError
 
 
@@ -56,3 +59,37 @@ def test_show_branch(tmp_dir, scm, dvc, run_copy_metrics):
         "working tree": {"metrics.yaml": {"foo": 1}},
         "branch": {"metrics.yaml": {"foo": 2}},
     }
+
+
+def test_show_subrepo_with_preexisting_tags(tmp_dir, scm):
+    tmp_dir.gen("foo", "foo")
+    scm.add("foo")
+    scm.commit("init")
+    scm.tag("no-metrics")
+
+    tmp_dir.gen({"subdir": {}})
+    subrepo_dir = tmp_dir / "subdir"
+    with subrepo_dir.chdir():
+        dvc = Repo.init(subdir=True)
+        scm.commit("init dvc")
+
+        dvc.run(
+            cmd="echo foo: 1 > metrics.yaml",
+            metrics=["metrics.yaml"],
+            single_stage=True,
+        )
+
+    scm.add(
+        [
+            str(subrepo_dir / "metrics.yaml"),
+            str(subrepo_dir / "metrics.yaml.dvc"),
+        ]
+    )
+    scm.commit("init metrics")
+    scm.tag("v1")
+
+    expected_path = os.path.join("subdir", "metrics.yaml")
+    assert dvc.metrics.show(all_tags=True) == {
+        "working tree": {expected_path: {"foo": 1}},
+        "v1": {expected_path: {"foo": 1}},
+    }