Repository: iterative/dvc

Issue:
remove -p: add a confirmation prompt
E.g. see `dvc/prompt.py`.

Discussion:
shcheklein: Looks good! Do we have tests for remove? I thinks it would be good to add a negative one.
mroutis: > Looks good! Do we have tests for remove? I thinks it would be good to add a negative one.

We do, @shcheklein , but there's no `--purge` one because testing the interaction is quite tiresome, but I can add one that test `remove` with `outs_only=False` (that's basically the same as doing --purge and input `yes` at the prompt)
efiop: @shcheklein  We don't have a framework for interactive stuff 🙁

@mroutis Please take a look at `dvc destroy` https://github.com/iterative/dvc/blob/master/dvc/command/destroy.py#L13 , where we raise an exception if user didn't confirm the action. I think we also should add `--force` option for `dvc remove`, to force `purging` without confirmation.

Patch:
diff --git a/dvc/__init__.py b/dvc/__init__.py
index f2d1b572f..51eb1913f 100644
--- a/dvc/__init__.py
+++ b/dvc/__init__.py
@@ -7,7 +7,7 @@ import os
 import warnings
 
 
-VERSION_BASE = '0.19.15'
+VERSION_BASE = '0.20.0'
 __version__ = VERSION_BASE
 
 PACKAGEPATH = os.path.abspath(os.path.dirname(__file__))
diff --git a/dvc/cli.py b/dvc/cli.py
index 495dca064..a40f2eb20 100644
--- a/dvc/cli.py
+++ b/dvc/cli.py
@@ -501,6 +501,12 @@ def parse_args(argv=None):
                         action='store_true',
                         default=False,
                         help='Remove DVC file and all its outputs.')
+    remove_parser.add_argument(
+                        '-f',
+                        '--force',
+                        action='store_true',
+                        default=False,
+                        help='Force purge.')
     remove_parser.add_argument(
                         'targets',
                         nargs='+',
diff --git a/dvc/command/remove.py b/dvc/command/remove.py
index f37d1bb8c..41de21e2f 100644
--- a/dvc/command/remove.py
+++ b/dvc/command/remove.py
@@ -4,10 +4,32 @@ from dvc.logger import Logger
 
 
 class CmdRemove(CmdBase):
+    def _is_outs_only(self, target):
+        if not self.args.purge:
+            return True
+
+        if self.args.force:
+            return False
+
+        msg = (
+            u'Are you sure you want to remove {} with its outputs?'
+            .format(target)
+        )
+
+        confirmed = self.project.prompt.prompt(msg)
+
+        if confirmed:
+            return False
+
+        raise DvcException(
+            u'Cannot purge without a confirmation from the user.'
+            u" Use '-f' to force."
+        )
+
     def run(self):
-        outs_only = not self.args.purge
         for target in self.args.targets:
             try:
+                outs_only = self._is_outs_only(target)
                 self.project.remove(target, outs_only=outs_only)
             except DvcException as ex:
                 Logger.error('Failed to remove {}'.format(target), ex)


Test patch:
diff --git a/tests/test_remove.py b/tests/test_remove.py
index 49b44c7df..66729f15e 100644
--- a/tests/test_remove.py
+++ b/tests/test_remove.py
@@ -2,9 +2,11 @@ import os
 
 from dvc.main import main
 from dvc.stage import Stage, StageFileDoesNotExistError
+from dvc.exceptions import DvcException
 from dvc.command.remove import CmdRemove
 
 from tests.basic_env import TestDvc
+from mock import patch
 
 
 class TestRemove(TestDvc):
@@ -57,3 +59,23 @@ class TestCmdRemove(TestDvc):
         ret = main(['remove',
                     'non-existing-dvc-file'])
         self.assertNotEqual(ret, 0)
+
+class TestRemovePurge(TestDvc):
+    def test(self):
+        dvcfile = self.dvc.add(self.FOO)[0].path
+        ret = main(['remove', '--purge', '--force', dvcfile])
+
+        self.assertEqual(ret, 0)
+        self.assertFalse(os.path.exists(self.FOO))
+        self.assertFalse(os.path.exists(dvcfile))
+
+    @patch('dvc.prompt.Prompt.prompt')
+    def test_force(self, mock_prompt):
+        mock_prompt.return_value = False
+
+        dvcfile = self.dvc.add(self.FOO)[0].path
+        ret = main(['remove', '--purge', dvcfile])
+
+        mock_prompt.assert_called()
+        self.assertEqual(ret, 1)
+        self.assertRaises(DvcException)