Repository: iterative/dvc

Issue:
pipeline: show: --outs: DAG has improper edges
- DVC version : 0.92, MacOS, installed via pip.
- Related issue : https://github.com/iterative/dvc/issues/1744 : Seems like the exact same problem but status says closed / merged.

Issue : 
In the DAG for `--outs` , all outputs from a connected stage are linked to the output of the stage.

Steps to recreations :
```sh
dvc run -o alice -o bob -f step1.dvc ' touch alice | touch bob ' 
dvc run -d alice -o carol -f step2.dvc ' touch carol '
dvc pipeline show --ascii --outs step2.dvc
```

<img width="176" alt="Screen Shot 2020-04-03 at 8 40 59 AM" src="https://user-images.githubusercontent.com/49366236/78379201-da7e5000-7586-11ea-9ce0-28c2b32264ba.png">

There shouldn't be an edge going from `bob` to `carol`.

Probably related to this line ? 
https://github.com/iterative/dvc/blob/a338fad036bd9ac8bdd79ecec964f8c5558609c4/dvc/command/pipeline.py#L63

Discussion:

Patch:
diff --git a/dvc/command/pipeline.py b/dvc/command/pipeline.py
index 7049066de..f18e17781 100644
--- a/dvc/command/pipeline.py
+++ b/dvc/command/pipeline.py
@@ -31,6 +31,31 @@ class CmdPipelineShow(CmdBase):
             else:
                 logger.info(stage.addressing)
 
+    @staticmethod
+    def _build_output_graph(G, target_stage):
+        import networkx
+        from itertools import product
+
+        nodes = {str(out) for out in target_stage.outs}
+        edges = []
+
+        for from_stage, to_stage in networkx.edge_dfs(G, target_stage):
+            from_stage_deps = {dep.path_info.parts for dep in from_stage.deps}
+            to_outs = {
+                to_out
+                for to_out in to_stage.outs
+                if to_out.path_info.parts in from_stage_deps
+            }
+            from_outs = {
+                from_out
+                for from_out in from_stage.outs
+                if str(from_out) in nodes
+            }
+            nodes |= {str(to_out) for to_out in to_outs}
+            for from_out, to_out in product(from_outs, to_outs):
+                edges.append((str(from_out), str(to_out)))
+        return nodes, edges
+
     def _build_graph(self, target, commands=False, outs=False):
         import networkx
         from dvc import dvcfile
@@ -47,10 +72,7 @@ class CmdPipelineShow(CmdBase):
                 if stage.cmd is None:
                     continue
                 nodes.add(stage.cmd)
-            elif outs:
-                for out in stage.outs:
-                    nodes.add(str(out))
-            else:
+            elif not outs:
                 nodes.add(stage.addressing)
 
         edges = []
@@ -59,13 +81,12 @@ class CmdPipelineShow(CmdBase):
                 if to_stage.cmd is None:
                     continue
                 edges.append((from_stage.cmd, to_stage.cmd))
-            elif outs:
-                for from_out in from_stage.outs:
-                    for to_out in to_stage.outs:
-                        edges.append((str(from_out), str(to_out)))
-            else:
+            elif not outs:
                 edges.append((from_stage.addressing, to_stage.addressing))
 
+        if outs:
+            nodes, edges = self._build_output_graph(G, target_stage)
+
         return list(nodes), edges, networkx.is_tree(G)
 
     def _show_ascii(self, target, commands, outs):
diff --git a/dvc/command/remote.py b/dvc/command/remote.py
index 615ba5375..c5f46a1a0 100644
--- a/dvc/command/remote.py
+++ b/dvc/command/remote.py
@@ -85,7 +85,18 @@ class CmdRemoteDefault(CmdRemote):
                 if self.args.unset:
                     conf["core"].pop("remote", None)
                 else:
-                    conf["core"]["remote"] = self.args.name
+                    merged_conf = self.config.load_config_to_level(
+                        self.args.level
+                    )
+                    if (
+                        self.args.name in conf["remote"]
+                        or self.args.name in merged_conf["remote"]
+                    ):
+                        conf["core"]["remote"] = self.args.name
+                    else:
+                        raise ConfigError(
+                            "default remote must be present in remote list."
+                        )
         return 0
 
 
diff --git a/dvc/config.py b/dvc/config.py
index ff968a682..dde2cb081 100644
--- a/dvc/config.py
+++ b/dvc/config.py
@@ -289,7 +289,7 @@ class Config(dict):
         Raises:
             ConfigError: thrown if config has an invalid format.
         """
-        conf = self._load_config_to_level()
+        conf = self.load_config_to_level()
 
         if validate:
             conf = self.validate(conf)
@@ -349,7 +349,7 @@ class Config(dict):
         }
         return Schema(dirs_schema, extra=ALLOW_EXTRA)(conf)
 
-    def _load_config_to_level(self, level=None):
+    def load_config_to_level(self, level=None):
         merged_conf = {}
         for merge_level in self.LEVELS:
             if merge_level == level:
@@ -368,7 +368,7 @@ class Config(dict):
 
         conf = self._save_paths(conf, self.files[level])
 
-        merged_conf = self._load_config_to_level(level)
+        merged_conf = self.load_config_to_level(level)
         _merge(merged_conf, conf)
         self.validate(merged_conf)
 


Test patch:
diff --git a/tests/func/test_pipeline.py b/tests/func/test_pipeline.py
index d08f59bfe..765b45dfa 100644
--- a/tests/func/test_pipeline.py
+++ b/tests/func/test_pipeline.py
@@ -274,7 +274,7 @@ def test_split_pipeline(tmp_dir, scm, dvc):
     )
 
     command = CmdPipelineShow([])
-    nodes, edges, is_tree = command._build_graph(
+    nodes, edges, _ = command._build_graph(
         stage.path, commands=False, outs=True
     )
     assert set(nodes) == {"data", "data_train", "data_valid", "result"}
@@ -319,11 +319,36 @@ def test_pipeline_ascii_multistage(tmp_dir, dvc, run_copy):
     run_copy("foo", "bar", name="copy-foo-bar")
     run_copy("bar", "foobar", single_stage=True)
     command = CmdPipelineShow([])
-    nodes, edges, is_tree = command._build_graph("foobar.dvc")
+    nodes, edges, _ = command._build_graph("foobar.dvc")
     assert set(nodes) == {"dvc.yaml:copy-foo-bar", "foobar.dvc"}
     assert set(edges) == {
         ("foobar.dvc", "dvc.yaml:copy-foo-bar"),
     }
 
-    nodes, edges, is_tree = command._build_graph("dvc.yaml:copy-foo-bar")
+    nodes, *_ = command._build_graph("dvc.yaml:copy-foo-bar")
     assert set(nodes) == {"dvc.yaml:copy-foo-bar"}
+
+
+def test_pipeline_multi_outputs_stages(dvc):
+    dvc.run(
+        outs=["alice", "bob"],
+        cmd="echo alice>alice && echo bob>bob",
+        single_stage=True,
+    )
+    dvc.run(
+        deps=["alice"],
+        outs=["mary", "mike"],
+        cmd="echo mary>mary && echo mike>mike",
+        single_stage=True,
+    )
+    stage = dvc.run(
+        deps=["mary"],
+        outs=["carol"],
+        cmd="echo carol>carol",
+        single_stage=True,
+    )
+
+    command = CmdPipelineShow([])
+    nodes, edges, _ = command._build_graph(stage.path, outs=True)
+    assert set(nodes) == {"alice", "mary", "carol"}
+    assert set(edges) == {("carol", "mary"), ("mary", "alice")}
diff --git a/tests/func/test_remote.py b/tests/func/test_remote.py
index 269feb8d2..a8d30d819 100644
--- a/tests/func/test_remote.py
+++ b/tests/func/test_remote.py
@@ -123,6 +123,7 @@ class TestRemoteDefault(TestDvc):
 
 
 def test_show_default(dvc, capsys):
+    assert main(["remote", "add", "foo", "s3://bucket/name"]) == 0
     assert main(["remote", "default", "foo"]) == 0
     assert main(["remote", "default"]) == 0
     out, _ = capsys.readouterr()
@@ -272,3 +273,21 @@ def test_remote_modify_validation(dvc):
     )
     config = configobj.ConfigObj(dvc.config.files["repo"])
     assert unsupported_config not in config['remote "{}"'.format(remote_name)]
+
+
+def test_remote_modify_default(dvc):
+    remote_repo = "repo_level"
+    remote_local = "local_level"
+    wrong_name = "anything"
+    assert main(["remote", "add", remote_repo, "s3://bucket/repo"]) == 0
+    assert main(["remote", "add", remote_local, "s3://bucket/local"]) == 0
+
+    assert main(["remote", "default", wrong_name]) == 251
+    assert main(["remote", "default", remote_repo]) == 0
+    assert main(["remote", "default", "--local", remote_local]) == 0
+
+    repo_config = configobj.ConfigObj(dvc.config.files["repo"])
+    local_config = configobj.ConfigObj(dvc.config.files["local"])
+
+    assert repo_config["core"]["remote"] == remote_repo
+    assert local_config["core"]["remote"] == remote_local