Repository: iterative/dvc

Issue:
pull/push/etc: support -R option
E.g. `dvc pull -R ./subfolder` would walk down the subfolder, find all the .dvc files and pull all of them.


Discussion:
@pared Just noticed that in the current implementation -R has an argument, instead of it being a flag that affects how `targets` argument is treated. E.g. so you could do stuff like `dvc pull -R dataset1/ dataset2/`, `dvc pull dataset1/ dataset2/ -R` and so on. Same as `-R` works in `dvc add`. Looks like we should change `-R` on pull/push/... work same as `-R` in`dvc add`. What do you think?
Oh, yest, that is inconsistent. Ill look into that.

Resolving patch:
diff --git a/dvc/command/data_sync.py b/dvc/command/data_sync.py
--- a/dvc/command/data_sync.py
+++ b/dvc/command/data_sync.py
@@ -30,7 +30,7 @@ def do_run(self, target=None):
                               all_tags=self.args.all_tags,
                               with_deps=self.args.with_deps,
                               force=self.args.force,
-                              from_directory=self.args.recursive)
+                              recursive=self.args.recursive)
         except Exception:
             logger.error('failed to pull data from the cloud')
             return 1
@@ -47,7 +47,7 @@ def do_run(self, target=None):
                               all_branches=self.args.all_branches,
                               all_tags=self.args.all_tags,
                               with_deps=self.args.with_deps,
-                              from_directory=self.args.recursive)
+                              recursive=self.args.recursive)
         except Exception:
             logger.error('failed to push data to the cloud')
             return 1
@@ -64,7 +64,7 @@ def do_run(self, target=None):
                                all_branches=self.args.all_branches,
                                all_tags=self.args.all_tags,
                                with_deps=self.args.with_deps,
-                               from_directory=self.args.recursive)
+                               recursive=self.args.recursive)
         except Exception:
             logger.error('failed to fetch data from the cloud')
             return 1
@@ -133,6 +133,8 @@ def add_parser(subparsers, parent_parser):
     pull_parser.add_argument(
         '-R',
         '--recursive',
+        action='store_true',
+        default=False,
         help='Pull cache for subdirectories of the specified directory.')
     pull_parser.set_defaults(func=CmdDataPull)
 
@@ -168,6 +170,8 @@ def add_parser(subparsers, parent_parser):
     push_parser.add_argument(
         '-R',
         '--recursive',
+        action='store_true',
+        default=False,
         help='Push cache from subdirectories of specified directory.')
     push_parser.set_defaults(func=CmdDataPush)
 
@@ -204,6 +208,8 @@ def add_parser(subparsers, parent_parser):
     fetch_parser.add_argument(
         '-R',
         '--recursive',
+        action='store_true',
+        default=False,
         help='Fetch cache for subdirectories of specified directory.')
     fetch_parser.set_defaults(func=CmdDataFetch)
 
diff --git a/dvc/project.py b/dvc/project.py
--- a/dvc/project.py
+++ b/dvc/project.py
@@ -568,12 +568,14 @@ def checkout(self,
                  target=None,
                  with_deps=False,
                  force=False,
-                 from_directory=None):
-        all_stages = self.active_stages(from_directory)
-        stages = all_stages
+                 recursive=False):
 
-        if target:
+        if target and not recursive:
+            all_stages = self.active_stages()
             stages = self._collect(target, with_deps=with_deps)
+        else:
+            all_stages = self.active_stages(target)
+            stages = all_stages
 
         with self.state:
             self._cleanup_unused_links(all_stages)
@@ -695,7 +697,7 @@ def _used_cache(self,
                     remote=None,
                     force=False,
                     jobs=None,
-                    from_directory=None):
+                    recursive=False):
         cache = {}
         cache['local'] = []
         cache['s3'] = []
@@ -707,12 +709,15 @@ def _used_cache(self,
         for branch in self.scm.brancher(all_branches=all_branches,
                                         all_tags=all_tags):
             if target:
-                stages = self._collect(target,
-                                       with_deps=with_deps)
+                if recursive:
+                    stages = self.stages(target)
+                else:
+                    stages = self._collect(target,
+                                           with_deps=with_deps)
             elif active:
-                stages = self.active_stages(from_directory)
+                stages = self.active_stages()
             else:
-                stages = self.stages(from_directory)
+                stages = self.stages()
 
             for stage in stages:
                 if active and not target and stage.locked:
@@ -836,7 +841,7 @@ def push(self,
              show_checksums=False,
              with_deps=False,
              all_tags=False,
-             from_directory=None):
+             recursive=False):
         with self.state:
             used = self._used_cache(target,
                                     all_branches=all_branches,
@@ -845,7 +850,7 @@ def push(self,
                                     force=True,
                                     remote=remote,
                                     jobs=jobs,
-                                    from_directory=from_directory)['local']
+                                    recursive=recursive)['local']
             self.cloud.push(used,
                             jobs,
                             remote=remote,
@@ -859,7 +864,7 @@ def fetch(self,
               show_checksums=False,
               with_deps=False,
               all_tags=False,
-              from_directory=None):
+              recursive=False):
         with self.state:
             used = self._used_cache(target,
                                     all_branches=all_branches,
@@ -868,7 +873,7 @@ def fetch(self,
                                     force=True,
                                     remote=remote,
                                     jobs=jobs,
-                                    from_directory=from_directory)['local']
+                                    recursive=recursive)['local']
             self.cloud.pull(used,
                             jobs,
                             remote=remote,
@@ -883,7 +888,7 @@ def pull(self,
              with_deps=False,
              all_tags=False,
              force=False,
-             from_directory=None):
+             recursive=False):
         self.fetch(target,
                    jobs,
                    remote=remote,
@@ -891,11 +896,11 @@ def pull(self,
                    all_tags=all_tags,
                    show_checksums=show_checksums,
                    with_deps=with_deps,
-                   from_directory=from_directory)
+                   recursive=recursive)
         self.checkout(target=target,
                       with_deps=with_deps,
                       force=force,
-                      from_directory=from_directory)
+                      recursive=recursive)
 
     def _local_status(self, target=None, with_deps=False):
         status = {}


Test patch: