Repository: iterative/dvc

Issue:
checkout: don't ask for removal confirmation if we have that file in the cache
```
#!/bin/bash

set -e
set -x

rm -rf myrepo
mkdir myrepo
cd myrepo
git init
dvc init
git commit -m"init"

git checkout -b foo
echo foo > file

dvc add file
git add file.dvc .gitignore
git commit -m foo

git checkout master
git checkout -b bar
dvc checkout

echo bar > file
dvc add file
git add file.dvc .gitignore
git commit -m bar

git checkout foo
dvc checkout
```

shouldn't ask for confirmation when deleting `file`, since it has that version in the cache already.


Discussion:
We would need to recompute the checksum for every file we check, as we don't know if it has changed or not.

```diff
#!/bin/bash

set -e
set -x

rm -rf myrepo
mkdir myrepo
cd myrepo
git init
dvc init
git commit -m"init"

git checkout -b foo
echo foo > file

dvc add file
git add file.dvc .gitignore
git commit -m foo

git checkout master
git checkout -b bar
dvc checkout

echo bar > file
dvc add file
git add file.dvc .gitignore
git commit -m bar

git checkout foo
+ echo other_content > file
dvc checkout
```

What we are trying to achieve is preventing the user from deleting files that are "work in progress" (a dirty working directory) when `checking out`.

I'll submit a patch for it, but I can't imagine how expensive would be to recalculate MD5s in order to know which files are safe to delete without prompting.

What do you think, @efiop ?
@mroutis If that file is already in cache and was linked to the workspace by dvc, then the checksum for it is already cached and costs nothing to retrieve. Otherwise we do re-compute the checksum, since we need to ensure that checksum is the one or not the one we need. We actually do that already, see https://github.com/iterative/dvc/blob/master/dvc/state.py#L87 . Or, another possibility, is to simply check if that file is in our link_state table(https://github.com/iterative/dvc/blob/master/dvc/state.py#L346), so we quickly know that it was indeed created by us and can be removed safely.

Resolving patch:
diff --git a/dvc/remote/local.py b/dvc/remote/local.py
--- a/dvc/remote/local.py
+++ b/dvc/remote/local.py
@@ -293,9 +293,11 @@ def checkout(self, path_info, checksum_info, force=False):
         Logger.info(msg.format(os.path.relpath(path), md5))
 
         if not self.is_dir_cache(cache):
-
             if os.path.exists(path):
-                remove(path) if force else self._safe_remove(path)
+                if force or self._already_cached(path):
+                    remove(path)
+                else:
+                    self._safe_remove(path)
 
             self.link(cache, path)
             self.state.update_link(path)
@@ -319,17 +321,16 @@ def checkout(self, path_info, checksum_info, force=False):
             p = os.path.join(path, relpath)
             c = self.get(m)
 
-            entry_info = {
-                'scheme': path_info['scheme'],
-                self.PARAM_PATH: p,
-            }
+            entry_info = {'scheme': path_info['scheme'], self.PARAM_PATH: p}
 
-            entry_checksum_info = {
-                self.PARAM_MD5: m,
-            }
+            entry_checksum_info = {self.PARAM_MD5: m}
 
             if self.changed(entry_info, entry_checksum_info):
-                remove(p) if force else self._safe_remove(p)
+                if force or self._already_cached(p):
+                    remove(p)
+                else:
+                    self._safe_remove(p)
+
                 self.link(c, p)
 
             if bar:
@@ -342,6 +343,11 @@ def checkout(self, path_info, checksum_info, force=False):
         if bar:
             progress.finish_target(dir_relpath)
 
+    def _already_cached(self, path):
+        current_md5 = self.state.update(path)
+
+        return not self.changed_cache(current_md5)
+
     def _discard_working_directory_changes(self, path, dir_info, force=False):
         working_dir_files = set(
             os.path.join(root, file)
@@ -357,7 +363,10 @@ def _discard_working_directory_changes(self, path, dir_info, force=False):
         delta = working_dir_files - cached_files
 
         for file in delta:
-            remove(file) if force else self._safe_remove(file)
+            if force or self._already_cached(file):
+                remove(file)
+            else:
+                self._safe_remove(file)
 
     def _safe_remove(self, file):
         msg = (
@@ -373,6 +382,8 @@ def _safe_remove(self, file):
                                " from the user. Use '-f' to force."
                                .format(file))
 
+        remove(file)
+
     def _move(self, inp, outp):
         # moving in two stages to make last the move atomic in
         # case inp and outp are in different filesystems


Test patch:
diff --git a/tests/test_checkout.py b/tests/test_checkout.py
--- a/tests/test_checkout.py
+++ b/tests/test_checkout.py
@@ -162,9 +162,12 @@ def test(self):
 
 
 class TestCheckoutCleanWorkingDir(CheckoutBase):
-    def test(self):
+    @patch('dvc.prompt.Prompt.prompt')
+    def test(self, mock_prompt):
         from tests.test_data_cloud import sleep
 
+        mock_prompt.return_value = True
+
         stages = self.dvc.add(self.DATA_DIR)
         stage = stages[0]
 
@@ -176,7 +179,7 @@ def test(self):
 
         sleep()
 
-        ret = main(['checkout', '--force', stage.relpath])
+        ret = main(['checkout', stage.relpath])
         self.assertEqual(ret, 0)
         self.assertFalse(os.path.exists(working_dir_change))
 
@@ -189,15 +192,15 @@ def test_force(self, mock_prompt):
         stages = self.dvc.add(self.DATA_DIR)
         self.assertEqual(len(stages), 1)
         stage = stages[0]
-        
+
         sleep()
-        
+
         working_dir_change = os.path.join(self.DATA_DIR, 'not_cached.txt')
         with open(working_dir_change, 'w') as f:
             f.write('not_cached')
 
         sleep()
-            
+
         ret = main(['checkout', stage.relpath])
         self.assertNotEqual(ret, 0)