Repository: iterative/dvc

Issue:
stage add: improve error for overlapping outputs
If I already have `model.pkl.dvc` and try to add a stage with that as an output, I get:

```bash
$ dvc stage add -n train -d train.py -o model.pkl python train.py
ERROR: output 'model.pkl' is already specified in stages:
        - train
        - model.pkl.dvc
```

This is unhelpful for a couple reasons:
1. It's not already specified in `train`. That's what I'm trying to add.
2. It should provide instructions for how to remedy it, like `Use 'dvc remove model.pkl.dvc' first to track 'model.pkl' as an output of stage 'train'`.

This matters for a scenario like https://github.com/iterative/example-repos-dev/issues/171. Someone starts using experiments without pipelines and adds the model to track in DVC. If they later want to add a pipeline, it's not clear how to do so.


Discussion:

Patch:
diff --git a/dvc/exceptions.py b/dvc/exceptions.py
--- a/dvc/exceptions.py
+++ b/dvc/exceptions.py
@@ -31,14 +31,19 @@ def __init__(self, output, stages):
 
         assert isinstance(output, str)
         assert all(hasattr(stage, "relpath") for stage in stages)
+        msg = ""
+        stage_names = [s.addressing for s in stages]
+        stages_str = " ".join(stage_names)
         if len(stages) == 1:
-            msg = "output '{}' is already specified in {}.".format(
-                output, first(stages)
-            )
+            stage_name = first(stages)
+            msg = f"output '{output}' is already specified in {stage_name}."
         else:
             msg = "output '{}' is already specified in stages:\n{}".format(
-                output, "\n".join(f"\t- {s.addressing}" for s in stages)
+                output, "\n".join(f"\t- {s}" for s in stage_names)
             )
+        msg += (
+            f"\nUse `dvc remove {stages_str}` to stop tracking the overlapping output."
+        )
         super().__init__(msg)
         self.stages = stages
         self.output = output
diff --git a/dvc/repo/stage.py b/dvc/repo/stage.py
--- a/dvc/repo/stage.py
+++ b/dvc/repo/stage.py
@@ -5,7 +5,11 @@
 from functools import wraps
 from typing import Iterable, List, NamedTuple, Optional, Set, Tuple, Union
 
-from dvc.exceptions import NoOutputOrStageError, OutputNotFoundError
+from dvc.exceptions import (
+    NoOutputOrStageError,
+    OutputDuplicationError,
+    OutputNotFoundError,
+)
 from dvc.repo import lock_repo
 from dvc.ui import ui
 from dvc.utils import as_posix, parse_target
@@ -176,7 +180,12 @@ def create(
 
                 check_stage_exists(self.repo, stage, stage.path)
 
-            self.repo.check_graph(stages={stage})
+            try:
+                self.repo.check_graph(stages={stage})
+            except OutputDuplicationError as exc:
+                # Don't include the stage currently being added.
+                exc.stages.remove(stage)
+                raise OutputDuplicationError(exc.output, exc.stages) from None
 
         restore_fields(stage)
         return stage


Test patch:
diff --git a/tests/func/test_stage.py b/tests/func/test_stage.py
--- a/tests/func/test_stage.py
+++ b/tests/func/test_stage.py
@@ -4,6 +4,7 @@
 
 from dvc.annotations import Annotation
 from dvc.dvcfile import SingleStageFile
+from dvc.exceptions import OutputDuplicationError
 from dvc.fs import LocalFileSystem
 from dvc.output import Output
 from dvc.repo import Repo, lock_repo
@@ -333,3 +334,14 @@ def test_stage_run_checkpoint(tmp_dir, dvc, mocker, checkpoint):
     mock_cmd_run.assert_called_with(
         stage, checkpoint_func=callback, dry=False, run_env=None
     )
+
+
+def test_stage_add_duplicated_output(tmp_dir, dvc):
+    tmp_dir.dvc_gen("foo", "foo")
+    dvc.add("foo")
+
+    with pytest.raises(
+        OutputDuplicationError,
+        match="Use `dvc remove foo.dvc` to stop tracking the overlapping output.",
+    ):
+        dvc.stage.add(name="duplicated", cmd="echo bar > foo", outs=["foo"])