Repository: iterative/dvc

Issue:
Introduce `--purge` option for `dvc remove`
`dvc delete stage.dvc` should remove outputs of the specified stage(same as `dvc remove`) and also delete the dvc file itself and do things like cleaning up .gitignore.

Kudos @analystanand !


Discussion:
Won't `delete` and `remove` be ambiguous? Perhaps this is more intuitive if specified with CLI flags.
But at least to me, `dvc remove` is a bit of a misnomer. Semantically speaking, you pass an argument to a command called `remove`, you expect that argument to be removed.

I'd suggest that the current behavior should be used with `dvc remove file.dvc --outputs-only` or something like that, and the proposed `dvc delete` be its default behavior.
Hi @villasv !

Yep, that would be a bit ambiguous, I agree :) Though, `delete` usually means something more permanent and `remove` is for something that can be brought back. Don't quote me on that please :) . In that semantic `dvc remove` is something that just removes the outputs, not removing the info and cache, but `dvc delete` is what really exterminates the given stage by removing all the info about it.

Unfortunately we can't switch default behavior of `dvc remove` in a minor release, as it will break backward compatibility that we've promised when releasing 0.9.0. Thus I think that `dvc delete` is the most acceptable option that will allow us to add any optional args on top and switch to any default behavior in the future major releases if we really need to.

Thanks,
Ruslan
@efiop Is there a way to introduce an option for `dvc remove` command?
After quite an extensive discussion with @dmpetrov and @shcheklein , we have reached the consensus of adding `--purge` option for `dvc remove`(I.e. `dvc remove --purge stage.dvc`) and not introducing a separate command. I'll also add `-o|--outs` command that is currently the default behavior and we will switch from it to `-p|--purge` in the next major release(https://github.com/iterative/dvc/issues/763).

Resolving patch:
diff --git a/dvc/cli.py b/dvc/cli.py
--- a/dvc/cli.py
+++ b/dvc/cli.py
@@ -226,6 +226,17 @@ def parse_args(argv=None):
                         'remove',
                         parents=[parent_parser],
                         help='Remove outputs of DVC file.')
+    remove_parser_group = remove_parser.add_mutually_exclusive_group()
+    remove_parser_group.add_argument('-o',
+                        '--outs',
+                        action='store_true',
+                        default=True,
+                        help='Only remove DVC file outputs.')
+    remove_parser_group.add_argument('-p',
+                        '--purge',
+                        action='store_true',
+                        default=False,
+                        help='Remove DVC file and all its outputs')
     remove_parser.add_argument('targets',
                         nargs='+',
                         help='DVC files.')
diff --git a/dvc/command/remove.py b/dvc/command/remove.py
--- a/dvc/command/remove.py
+++ b/dvc/command/remove.py
@@ -4,9 +4,10 @@
 
 class CmdRemove(CmdBase):
     def run(self):
+        outs_only = not self.args.purge
         for target in self.args.targets:
             try:
-                self.project.remove(target)
+                self.project.remove(target, outs_only=outs_only)
             except DvcException as ex:
                 self.project.logger.error('Failed to remove {}', ex)
                 return 1
diff --git a/dvc/project.py b/dvc/project.py
--- a/dvc/project.py
+++ b/dvc/project.py
@@ -123,13 +123,15 @@ def add(self, fname):
         stage.dump()
         return stage
 
-    def remove(self, target):
+    def remove(self, target, outs_only=False):
         if not Stage.is_stage_file(target):
             raise StageNotFoundError(target)
 
         stage = Stage.load(self, target)
-        for out in stage.outs:
-            out.remove()
+        if outs_only:
+            stage.remove_outs()
+        else:
+            stage.remove()
 
         return stage
 


Test patch:
diff --git a/tests/test_remove.py b/tests/test_remove.py
--- a/tests/test_remove.py
+++ b/tests/test_remove.py
@@ -11,11 +11,17 @@
 class TestRemove(TestDvc):
     def test(self):
         stage = self.dvc.add(self.FOO)
-        stage_removed = self.dvc.remove(stage.path)
+        stage_removed = self.dvc.remove(stage.path, outs_only=True)
+
+        self.assertIsInstance(stage_removed, Stage)
+        self.assertEqual(stage.path, stage_removed.path)
+        self.assertFalse(os.path.isfile(self.FOO))
 
+        stage_removed = self.dvc.remove(stage.path)
         self.assertIsInstance(stage_removed, Stage)
         self.assertEqual(stage.path, stage_removed.path)
         self.assertFalse(os.path.isfile(self.FOO))
+        self.assertFalse(os.path.exists(stage.path))
 
 
 class TestRemoveNonExistentFile(TestDvc):
@@ -30,7 +36,7 @@ def test(self):
         stage_removed = self.dvc.remove(stage_add.path)
         self.assertEqual(stage_add.path, stage_removed.path)
         self.assertFalse(os.path.exists(self.DATA_DIR))
-        self.assertTrue(os.path.exists(stage_removed.path))
+        self.assertFalse(os.path.exists(stage_removed.path))
 
 
 class TestCmdRemove(TestDvc):