Repository: iterative/dvc

Issue:
provide helpful message on output duplications
## Bug Report

#### Repro
```console
$ cat dvc.yaml
stages:
  plot-metrics:
    cmd: cp metrics-temp.csv metrics.csv
    deps:
    - metrics-temp.csv
    plots:
    - metrics.csv
  generate-foo:
    cmd: echo 'foo0\nfoo1' > foo && cat foo
    outs:
    - foo
    - metrics.csv  # notice the output duplication
$ python -c '
import dvc.api

with dvc.api.open("metrics.csv") as f:
  print(f.read())'

Traceback (most recent call last):
  File "/home/saugat/repos/iterative/dvc/dvc/repo/__init__.py", line 585, in open_by_relpath
    remote=remote,
  File "/home/saugat/repos/iterative/dvc/dvc/repo/tree.py", line 257, in open
    path, mode=mode, encoding=encoding, **kwargs
  File "/home/saugat/repos/iterative/dvc/dvc/repo/tree.py", line 67, in open
    raise IsADirectoryError
IsADirectoryError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<string>", line 4, in <module>
  File "/home/saugat/.pyenv/versions/3.6.6/lib/python3.6/contextlib.py", line 81, in __enter__
    return next(self.gen)
  File "/home/saugat/repos/iterative/dvc/dvc/api.py", line 74, in _open
    path, remote=remote, mode=mode, encoding=encoding
  File "/home/saugat/.pyenv/versions/3.6.6/lib/python3.6/contextlib.py", line 81, in __enter__
    return next(self.gen)
  File "/home/saugat/repos/iterative/dvc/dvc/repo/__init__.py", line 591, in open_by_relpath
    raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
dvc.exceptions.IsADirectoryError: '/home/saugat/dvc_try/test_multistage/metrics.csv' is a directory
```

Similar things happens on `plots diff` (message is cryptic):
```console
$ dvc plots diff -t metrics.csv HEAD~
ERROR: unexpected error

Having any troubles? Hit us up at https://dvc.org/support, we are always happy to help!
$ dvc plots diff -t metrics.csv HEAD~ -v
2020-06-10 18:28:50,571 ERROR: unexpected error
------------------------------------------------------------
Traceback (most recent call last):
  File "/home/saugat/repos/iterative/dvc/dvc/main.py", line 53, in main
    ret = cmd.run()
  File "/home/saugat/repos/iterative/dvc/dvc/command/plots.py", line 53, in run
    plots = self._func(targets=self.args.targets, props=self._props())
  File "/home/saugat/repos/iterative/dvc/dvc/command/plots.py", line 88, in _func
    return self.repo.plots.diff(*args, revs=self.args.revisions, **kwargs)
  File "/home/saugat/repos/iterative/dvc/dvc/repo/plots/__init__.py", line 82, in diff
    return diff(self.repo, *args, **kwargs)
  File "/home/saugat/repos/iterative/dvc/dvc/repo/plots/diff.py", line 12, in diff
    *args, revs=_revisions(revs, repo.scm.is_dirty()), **kwargs
  File "/home/saugat/repos/iterative/dvc/dvc/repo/plots/__init__.py", line 65, in show
    data = self.collect(targets, revs)
  File "/home/saugat/repos/iterative/dvc/dvc/repo/plots/__init__.py", line 41, in collect
    with tree.open(datafile) as fd:
  File "/home/saugat/repos/iterative/dvc/dvc/repo/tree.py", line 257, in open
    path, mode=mode, encoding=encoding, **kwargs
  File "/home/saugat/repos/iterative/dvc/dvc/repo/tree.py", line 67, in open
    raise IsADirectoryError
IsADirectoryError
------------------------------------------------------------
```

### Please provide information about your setup

**Output of `dvc version`:**

```console
$ dvc version
dvc version
DVC version: 1.0.0a10+91a719
Python version: 3.6.6
Platform: Linux-5.5.2-arch1-1-x86_64-with-arch
Binary: False
Package: None
Supported remotes: azure, gdrive, gs, hdfs, http, https, s3, ssh, oss
Cache: reflink - not supported, hardlink - supported, symlink - supported
Filesystem type (cache directory): ('ext4', '/dev/sda9')
Repo: dvc, git
Filesystem type (workspace): ('ext4', '/dev/sda9')
```

**Additional Information (if any):**

#### Expected Behavior
Complain that output is duplicated. The error message is not helpful at all.

We do provide helpful message on `run/repro/add/imp`, but not on these checkouts. 
Also, we should not throw out error on `dvc  checkout foo` as it will affect workflow.



Discussion:
Also, with our effort to enable user for easier writability, this might be a common occurence, and better output message would be very helpful here.

Resolving patch:
diff --git a/dvc/repo/__init__.py b/dvc/repo/__init__.py
--- a/dvc/repo/__init__.py
+++ b/dvc/repo/__init__.py
@@ -375,8 +375,8 @@ def stages(self):
         return self.stage.collect_repo(onerror=error_handler)
 
     def find_outs_by_path(self, path, outs=None, recursive=False, strict=True):
-        if not outs:
-            outs = [out for stage in self.stages for out in stage.outs]
+        # using `outs_graph` to ensure graph checks are run
+        outs = outs or self.outs_graph
 
         abs_path = os.path.abspath(path)
         path_info = PathInfo(abs_path)
diff --git a/dvc/repo/collect.py b/dvc/repo/collect.py
--- a/dvc/repo/collect.py
+++ b/dvc/repo/collect.py
@@ -21,7 +21,7 @@ def _collect_outs(
 ) -> Outputs:
     outs = {
         out
-        for stage in repo.stages
+        for stage in repo.graph  # using `graph` to ensure graph checks run
         for out in (stage.deps if deps else stage.outs)
     }
     return set(filter(output_filter, outs)) if output_filter else outs


Test patch:
diff --git a/tests/func/metrics/test_common.py b/tests/func/metrics/test_common.py
--- a/tests/func/metrics/test_common.py
+++ b/tests/func/metrics/test_common.py
@@ -2,7 +2,14 @@
 
 import pytest
 
-from dvc.exceptions import NoMetricsFoundError, NoMetricsParsedError
+from dvc.exceptions import (
+    NoMetricsFoundError,
+    NoMetricsParsedError,
+    OverlappingOutputPathsError,
+)
+from dvc.path_info import PathInfo
+from dvc.utils.fs import remove
+from dvc.utils.serialize import dump_yaml, modify_yaml
 from tests.func.metrics.utils import _write_json
 
 
@@ -61,3 +68,41 @@ def test_show_malformed_metric(
 def test_show_no_metrics_files(tmp_dir, dvc, caplog, show):
     with pytest.raises(NoMetricsFoundError):
         show(dvc)
+
+
+@pytest.mark.parametrize("clear_before_run", [True, False])
+@pytest.mark.parametrize("typ", ["metrics", "plots"])
+def test_metrics_show_overlap(
+    tmp_dir, dvc, run_copy_metrics, clear_before_run, typ
+):
+    data_dir = PathInfo("data")
+    (tmp_dir / data_dir).mkdir()
+
+    outs = {typ: [str(data_dir / "m1.yaml")]}
+    dump_yaml(data_dir / "m1_temp.yaml", {"a": {"b": {"c": 2, "d": 1}}})
+    run_copy_metrics(
+        str(data_dir / "m1_temp.yaml"),
+        str(data_dir / "m1.yaml"),
+        single_stage=False,
+        commit=f"add m1 {typ}",
+        name="cp-m1",
+        **outs,
+    )
+    with modify_yaml("dvc.yaml") as d:
+        # trying to make an output overlaps error
+        d["stages"]["corrupted-stage"] = {
+            "cmd": "mkdir data",
+            "outs": ["data"],
+        }
+
+    # running by clearing and not clearing stuffs
+    # so as it works even for optimized cases
+    if clear_before_run:
+        remove(data_dir)
+        remove(dvc.cache.local.cache_dir)
+
+    dvc._reset()
+
+    show = dvc.metrics.show if typ == "metrics" else dvc.plots.show
+    with pytest.raises(OverlappingOutputPathsError):
+        show()
diff --git a/tests/unit/repo/test_repo.py b/tests/unit/repo/test_repo.py
--- a/tests/unit/repo/test_repo.py
+++ b/tests/unit/repo/test_repo.py
@@ -1,7 +1,9 @@
 import os
+import shutil
 
 import pytest
 
+from dvc.exceptions import OutputDuplicationError
 from dvc.repo import NotDvcRepoError, Repo, locked
 from dvc.utils.fs import remove
 
@@ -29,6 +31,15 @@ def test_find_outs_by_path(tmp_dir, dvc, path):
     assert outs[0].path_info == stage.outs[0].path_info
 
 
+def test_find_outs_by_path_does_graph_checks(tmp_dir, dvc):
+    tmp_dir.dvc_gen("foo", "foo")
+    shutil.copyfile("foo.dvc", "foo-2.dvc")
+
+    dvc._reset()
+    with pytest.raises(OutputDuplicationError):
+        dvc.find_outs_by_path("foo")
+
+
 @pytest.mark.parametrize(
     "path",
     [os.path.join("dir", "subdir", "file"), os.path.join("dir", "subdir")],