Repository: iterative/dvc

Issue:
dvc add a file from data directory
From a discussion with users: https://opendatascience.slack.com/archives/CGGLZJ119/p1579704007005600 (you need access to this community)

```
$ mkdir datadir
$ cp ~/whatever/* datadir/
$ dvc add datadir/
WARNING: Output 'datadir' of 'datadir.dvc' changed because it is 'modified'
To track the changes with git, run:

          git add datadir.dvc
$ cp ~/Downloads/newfile.csv datadir/jan2020.csv
$ dvc add datadir/jan2020.csv
ERROR: Paths for outs:                  # <-- error is terrible btw (not related to this issue).
'datadir'('datadir.dvc')
'datadir/file4'('datadir/file4.dvc')
overlap. To avoid unpredictable behaviour, rerun command with non overlapping outs paths.
```

The last command fails because the file is inside a data dir and you suppose to update (`dvc add`) the entire dir. However, a user intuition says (for some users) to add a single file.

Ideally, this should work:
```
$ dvc add datadir/jan2020.csv
'jan2020' was added to dir 'datadir' and 'datadir.dvc' changed because it is 'modified'
100% Add|██████████████████████████████████|1.00/1.00 [00:00<00:00,  2.76file/s]

To track the changes with git, run:

	git add datadir.dvc
```


Discussion:
I'm not sure about this implicit behavior. A user might not even notice that we did that trick and be then surprised that his file doesn't have their own dvc file. This is the only case where we've seen a report like that, so I would rather wait for someone else to report it, to understand if this is a one-off cofusion of particular user or something reoccurring.
From conversation with @dmpetrov it is clear that he has seen other occurences of this issue and it would be great to at least improve the error 

Resolving patch:
diff --git a/dvc/exceptions.py b/dvc/exceptions.py
--- a/dvc/exceptions.py
+++ b/dvc/exceptions.py
@@ -188,17 +188,10 @@ def __init__(self):
 
 
 class OverlappingOutputPathsError(DvcException):
-    def __init__(self, out_1, out_2):
-        super().__init__(
-            "Paths for outs:\n'{}'('{}')\n'{}'('{}')\noverlap. To avoid "
-            "unpredictable behaviour, rerun command with non overlapping outs "
-            "paths.".format(
-                str(out_1),
-                out_1.stage.relpath,
-                str(out_2),
-                out_2.stage.relpath,
-            )
-        )
+    def __init__(self, parent, overlapping_out, message):
+        self.parent = parent
+        self.overlapping_out = overlapping_out
+        super().__init__(message)
 
 
 class CheckoutErrorSuggestGit(DvcException):
diff --git a/dvc/repo/__init__.py b/dvc/repo/__init__.py
--- a/dvc/repo/__init__.py
+++ b/dvc/repo/__init__.py
@@ -334,7 +334,17 @@ def _collect_graph(self, stages=None):
             for out in stage.outs:
                 for p in out.path_info.parents:
                     if p in outs:
-                        raise OverlappingOutputPathsError(outs[p], out)
+                        msg = (
+                            "Paths for outs:\n'{}'('{}')\n'{}'('{}')\n"
+                            "overlap. To avoid unpredictable behaviour, "
+                            "rerun command with non overlapping outs paths."
+                        ).format(
+                            str(outs[p]),
+                            outs[p].stage.relpath,
+                            str(out),
+                            out.stage.relpath,
+                        )
+                        raise OverlappingOutputPathsError(outs[p], out, msg)
 
         for stage in stages:
             stage_path_info = PathInfo(stage.path)
diff --git a/dvc/repo/add.py b/dvc/repo/add.py
--- a/dvc/repo/add.py
+++ b/dvc/repo/add.py
@@ -4,7 +4,10 @@
 import colorama
 
 from . import locked
-from ..exceptions import RecursiveAddingWhileUsingFilename
+from ..exceptions import (
+    RecursiveAddingWhileUsingFilename,
+    OverlappingOutputPathsError,
+)
 from ..output.base import OutputDoesNotExistError
 from ..progress import Tqdm
 from ..repo.scm_context import scm_context
@@ -49,7 +52,22 @@ def add(repo, targets, recursive=False, no_commit=False, fname=None):
 
             stages = _create_stages(repo, sub_targets, fname, pbar=pbar)
 
-            repo.check_modified_graph(stages)
+            try:
+                repo.check_modified_graph(stages)
+            except OverlappingOutputPathsError as exc:
+                msg = (
+                    "Cannot add '{out}', because it is overlapping with other "
+                    "DVC tracked output: '{parent}'.\n"
+                    "To include '{out}' in '{parent}', run "
+                    "'dvc commit {parent_stage}'"
+                ).format(
+                    out=exc.overlapping_out.path_info,
+                    parent=exc.parent.path_info,
+                    parent_stage=exc.parent.stage.relpath,
+                )
+                raise OverlappingOutputPathsError(
+                    exc.parent, exc.overlapping_out, msg
+                )
 
             with Tqdm(
                 total=len(stages),


Test patch:
diff --git a/tests/func/test_add.py b/tests/func/test_add.py
--- a/tests/func/test_add.py
+++ b/tests/func/test_add.py
@@ -11,7 +11,7 @@
 
 import dvc as dvc_module
 from dvc.cache import Cache
-from dvc.exceptions import DvcException
+from dvc.exceptions import DvcException, OverlappingOutputPathsError
 from dvc.exceptions import RecursiveAddingWhileUsingFilename
 from dvc.exceptions import StageFileCorruptedError
 from dvc.main import main
@@ -641,3 +641,17 @@ def test_escape_gitignore_entries(tmp_dir, scm, dvc):
 
     tmp_dir.dvc_gen(fname, "...")
     assert ignored_fname in get_gitignore_content()
+
+
+def test_add_from_data_dir(tmp_dir, scm, dvc):
+    tmp_dir.dvc_gen({"dir": {"file1": "file1 content"}})
+
+    tmp_dir.gen({"dir": {"file2": "file2 content"}})
+
+    with pytest.raises(OverlappingOutputPathsError) as e:
+        dvc.add(os.path.join("dir", "file2"))
+    assert str(e.value) == (
+        "Cannot add '{out}', because it is overlapping with other DVC "
+        "tracked output: 'dir'.\n"
+        "To include '{out}' in 'dir', run 'dvc commit dir.dvc'"
+    ).format(out=os.path.join("dir", "file2"))