Repository: iterative/dvc
Issue: `dvc gc` does not remove files under dir.unpacked

Setup is dvc 0.75.0 with the .deb package under Ubuntu 18.04.
Also tested with windows 10 with .exe package. I tried with different configurations for the cache type (default, copy and symlink) and get a similar behavior.

I'm testing DVC and trying to understand how it manages the datasets in its cache.
I initialize an empty repository with `dvc init` and add data with `dvc add data` in directory that contains to datasets:

- `data/data.json` of size 240M 
- `data/data_1.json` of size 65M

I then run a script that produces an output dataset: `dvc run -f prepare_data.dvc -d src/prepare_data.py -d data -o output python src prepare_data.py data`. It creates: `output/prepared_data.npy` of size 360M.
The `.dvc/cache ` directory now contains the following files (names are simplified):

- `06/file1` of size 240M at inode 60294316
- `14/file2` of size 360M at inode 60949288
- `20/file3.dir` at inode 60294287
- `59/file4` of size 65M at inode 60294327
- `dd/file5.dir` at inode 60949289

I commit to git and tag it as expe1. I now modify my script and run `dvc repro prepare_data.dvc`. It produces a new file: `output/prepared_data.npy` of size 500M. The `.dvc/cache ` directory now contains the following files:

- `06/file1` of size 240M at inode 60294316
- `14/file2` of size 360M at inode 60949288
- `19/file6` of size 500M at inode 60949290
- `20/file3.dir` at inode 60294287
- `20/file3.dir.unpacked/data.json` of size 240M at inode 60294316
- `20/file3.dir.unpacked/data_1.json` of size 65M at inode 60294327
- `22/file7.dir` at inode 60294291
- `59/file4` of size 65M at inode 60294327
- `dd/file5.dir` at inode 60949289
- `dd/file5.dir.unpacked/prepared_data.npy` of size 360M at inode 60949288

I commit and tag it as expe2. I now want to clean the cache to remove previous outputs from expe1 and run `dvc gc`. The `.dvc/cache ` directory now contains the following files:

- `06/file1` of size 240M at inode 60294316
- empty directory `14`
- `19/file6` of size 500M at inode 60949290
- `20/file3.dir` at inode 60294287
- `20/file3.dir.unpacked/data.json` of size 240M at inode 60294316
- `20/file3.dir.unpacked/data_1.json` of size 65M at inode 60294327
- `22/file7.dir` at inode 60294291
- `59/file4` of size 65M at inode 60294327
- `dd/file5.dir` at inode 60949289
- `dd/file5.dir.unpacked/prepared_data.npy` of size 360M at inode 60949288

Therefore contrary to what I expect, the previous version of the output file is still present in the cache at `dd/file5.dir.unpacked/prepared_data.npy` . Is this the expected behavior ?
How can I properly clean the cache ?
It seems that this file is useless as if try to checkout expe1, it raises an error, and I have to reproduce the experiment anyway:

    $ git checkout expe1
    $ dvc checkout
    ERROR: unexpected error - Checkout failed for the following target: 
       output
    Did you forget to fetch ?``

Changed files:
dvc/remote/local.py, dvc/remote/base.py, tests/func/test_gc.py

diff --git a/dvc/remote/base.py b/dvc/remote/base.py
index 32e14069e..1c316416b 100644
--- a/dvc/remote/base.py
+++ b/dvc/remote/base.py
@@ -697,6 +697,8 @@ class RemoteBASE(object):
             if checksum in used:
                 continue
             path_info = self.checksum_to_path_info(checksum)
+            if self.is_dir_checksum(checksum):
+                self._remove_unpacked_dir(checksum)
             self.remove(path_info)
             removed = True
         return removed
@@ -1009,3 +1011,6 @@ class RemoteBASE(object):
 
     def _update_unpacked_dir(self, checksum):
         pass
+
+    def _remove_unpacked_dir(self, checksum):
+        pass
diff --git a/dvc/remote/local.py b/dvc/remote/local.py
index f7a1ff4c8..1117ca993 100644
--- a/dvc/remote/local.py
+++ b/dvc/remote/local.py
@@ -464,6 +464,10 @@ class RemoteLOCAL(RemoteBASE):
         info = self.checksum_to_path_info(checksum)
         return info.with_name(info.name + self.UNPACKED_DIR_SUFFIX)
 
+    def _remove_unpacked_dir(self, checksum):
+        path_info = self._get_unpacked_dir_path_info(checksum)
+        self.remove(path_info)
+
     def _path_info_changed(self, path_info):
         if self.exists(path_info) and self.state.get(path_info):
             return False
diff --git a/tests/func/test_gc.py b/tests/func/test_gc.py
index 386f168cf..688f11396 100644
--- a/tests/func/test_gc.py
+++ b/tests/func/test_gc.py
@@ -7,6 +7,7 @@ from git import Repo
 from dvc.exceptions import CollectCacheError
 from dvc.main import main
 from dvc.repo import Repo as DvcRepo
+from dvc.remote.local import RemoteLOCAL
 from tests.basic_env import TestDir, TestDvcGit
 
 
@@ -204,3 +205,18 @@ def test_gc_no_dir_cache(tmp_dir, dvc, repo_template):
 
 def _count_files(path):
     return sum(len(files) for _, _, files in os.walk(path))
+
+
+def test_gc_no_unpacked_dir(tmp_dir, dvc, repo_template):
+    dir_stages = dvc.add("dir")
+    dvc.status()
+
+    os.remove("dir.dvc")
+    unpackeddir = (
+        dir_stages[0].outs[0].cache_path + RemoteLOCAL.UNPACKED_DIR_SUFFIX
+    )
+
+    assert os.path.exists(unpackeddir)
+
+    dvc.gc(force=True)
+    assert not os.path.exists(unpackeddir)