Repository: iterative/dvc

Issue:
add `pipeline show --tree`
Unfold dependencies into a tree same way it is done in npm:

```
│ │ │   └── ms@2.0.0
  │ │ ├── lru-cache@4.1.1 deduped
  │ │ ├── mississippi@1.3.0 deduped
  │ │ ├─┬ node-fetch-npm@2.0.2
  │ │ │ ├─┬ encoding@0.1.12
  │ │ │ │ └── iconv-lite@0.4.19
  │ │ │ ├── json-parse-better-errors@1.0.1
  │ │ │ └── safe-buffer@5.1.1 deduped
  │ │ ├── promise-retry@1.1.1 deduped
  │ │ ├─┬ socks-proxy-agent@3.0.1
  │ │ │ ├─┬ agent-base@4.1.2
  │ │ │ │ └─┬ es6-promisify@5.0.0
  │ │ │ │   └── es6-promise@4.1.1
  │ │ │ └─┬ socks@1.1.10
  │ │ │   ├── ip@1.1.5
  │ │ │   └── smart-buffer@1.1.15
  │ │ └── ssri@5.0.0 deduped
  │ ├─┬ minimatch@3.0.4
```

Kudos @shcheklein 


Discussion:

Patch:
diff --git a/dvc/command/pipeline.py b/dvc/command/pipeline.py
--- a/dvc/command/pipeline.py
+++ b/dvc/command/pipeline.py
@@ -71,23 +71,47 @@ def __build_graph(self, target, commands, outs):
             else:
                 edges.append((from_stage.relpath, to_stage.relpath))
 
-        return nodes, edges
+        return nodes, edges, networkx.is_tree(G)
 
     def _show_ascii(self, target, commands, outs):
         from dvc.dagascii import draw
 
-        nodes, edges = self.__build_graph(target, commands, outs)
+        nodes, edges, _ = self.__build_graph(target, commands, outs)
 
         if not nodes:
             return
 
         draw(nodes, edges)
 
+    def _show_dependencies_tree(self, target, commands, outs):
+        from treelib import Tree
+
+        nodes, edges, is_tree = self.__build_graph(target, commands, outs)
+        if not nodes:
+            return
+        if not is_tree:
+            raise DvcException(
+                "DAG is not a tree, can not print it in tree-structure way, "
+                "please use --ascii instead"
+            )
+
+        tree = Tree()
+        tree.create_node(target, target)  # Root node
+        observe_list = [target]
+        while len(observe_list) > 0:
+            current_root = observe_list[0]
+            for edge in edges:
+                if edge[0] == current_root:
+                    tree.create_node(edge[1], edge[1], parent=current_root)
+                    observe_list.append(edge[1])
+            observe_list.pop(0)
+        tree.show()
+
     def __write_dot(self, target, commands, outs, filename):
         import networkx
         from networkx.drawing.nx_pydot import write_dot
 
-        _, edges = self.__build_graph(target, commands, outs)
+        _, edges, _ = self.__build_graph(target, commands, outs)
         edges = [edge[::-1] for edge in edges]
 
         simple_g = networkx.DiGraph()
@@ -111,6 +135,10 @@ def run(self, unlock=False):
                         self.args.outs,
                         self.args.dot,
                     )
+                elif self.args.tree:
+                    self._show_dependencies_tree(
+                        target, self.args.commands, self.args.outs
+                    )
                 else:
                     self._show(target, self.args.commands, self.args.outs)
             except DvcException:
@@ -185,6 +213,12 @@ def add_parser(subparsers, parent_parser):
     pipeline_show_parser.add_argument(
         "--dot", help="Write DAG in .dot format."
     )
+    pipeline_show_parser.add_argument(
+        "--tree",
+        action="store_true",
+        default=False,
+        help="Output DAG as Dependencies Tree.",
+    )
     pipeline_show_parser.add_argument(
         "targets", nargs="*", help="DVC files. 'Dvcfile' by default."
     )
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -22,6 +22,7 @@
     "asciimatics>=1.10.0",
     "distro>=1.3.0",
     "appdirs>=1.4.3",
+    "treelib>=1.5.5",
 ]
 
 # Extra dependencies for remote integrations


Test patch:
diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py
--- a/tests/test_pipeline.py
+++ b/tests/test_pipeline.py
@@ -34,6 +34,10 @@ def test_dot(self):
         self.assertEqual(ret, 0)
         self.assertTrue(os.path.isfile(self.dotFile))
 
+    def test_tree(self):
+        ret = main(["pipeline", "show", "--tree", self.stage])
+        self.assertEqual(ret, 0)
+
     def test_ascii_commands(self):
         ret = main(["pipeline", "show", "--ascii", self.stage, "--commands"])
         self.assertEqual(ret, 0)