Repository: iterative/dvc

Issue:
diff: does not behave properly when cache is not available
## Version

```
DVC version: 0.86.5+f67314.mod
Python version: 3.7.6
Platform: Darwin-18.2.0-x86_64-i386-64bit
Binary: False
Package: None
Cache: reflink - supported, hardlink - supported, symlink - supported
Filesystem type (cache directory): ('apfs', '/dev/disk1s1')
Filesystem type (workspace): ('apfs', '/dev/disk1s1')
```

## Reproduce

```
git clone git@github.com:iterative/example-get-started.git
cd example-get-started
dvc pull
dvc diff baseline-experiment bigrams-experiment
```

## Output

```
Added:
    data/features/test.pkl
    data/features/train.pkl

Modified:
    auc.metric
    data/features/
    model.pkl

files summary: 2 added, 0 deleted, 2 modified
```

which is wrong. Files `data/features/test.pkl` and `data/features/train.pkl` actually exist in both experiments. The only difference is that one of them cached, one is not.


Discussion:
@shcheklein good catch!

> The only difference is that one of them cached, one is not.

Just a clarification: these two files are not cached for `master`/`bigrams-experiment` but not for `baseline-experiment`.

I'm very surprised that `dvc diff` is reading the cache and depends on its state. We agreed that #2998 will be also included in the first version of the diff. It is crucial for CI/CD scenarios. Reopening it.

PS: @shcheklein  the url didn't work by default
```
git clone git@github.com:iterative/example-get-started.git
Cloning into 'example-get-started'...
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
```
> the url didn't work by default
>
> `git clone git@github.com:iterative/example-get-started.git`

Hmmm weird. The repo is public. Is it because of the git@ SSH URL? Try with `git clone git@github.com:iterative/example-get-started.git` @dmpetrov?

If this is the case, we should update in https://dvc.org/doc/command-reference/get#example-compare-different-versions-of-data-or-model

UPDATE: I'm changing this just in case in a regular updates PR to docs.
Worth noting that after `pull` on baseline, message seems to be ok (that both paths are modified).

Resolving patch:
diff --git a/dvc/repo/diff.py b/dvc/repo/diff.py
--- a/dvc/repo/diff.py
+++ b/dvc/repo/diff.py
@@ -22,29 +22,25 @@ def _paths_checksums():
         A dictionary of checksums addressed by relpaths collected from
         the current tree outputs.
 
-        Unpack directories to include their entries
-
         To help distinguish between a directory and a file output,
         the former one will come with a trailing slash in the path:
 
             directory: "data/"
             file:      "data"
         """
-        result = {}
-
-        for stage in self.stages:
-            for output in stage.outs:
-                if not output.is_dir_checksum:
-                    result.update({str(output): output.checksum})
-                    continue
-
-                result.update({os.path.join(str(output), ""): output.checksum})
-
-                for entry in output.dir_cache:
-                    path = str(output.path_info / entry["relpath"])
-                    result.update({path: entry["md5"]})
 
-        return result
+        def _to_path(output):
+            return (
+                str(output)
+                if not output.is_dir_checksum
+                else os.path.join(str(output), "")
+            )
+
+        return {
+            _to_path(output): output.checksum
+            for stage in self.stages
+            for output in stage.outs
+        }
 
     working_tree = self.tree
     a_tree = self.scm.get_tree(a_rev)


Test patch:
diff --git a/tests/func/test_diff.py b/tests/func/test_diff.py
--- a/tests/func/test_diff.py
+++ b/tests/func/test_diff.py
@@ -3,6 +3,8 @@
 import pytest
 import shutil
 
+from funcy import first
+
 from dvc.compat import fspath
 from dvc.exceptions import DvcException
 
@@ -40,11 +42,7 @@ def test_no_cache_entry(tmp_dir, scm, dvc):
     dir_checksum = "5fb6b29836c388e093ca0715c872fe2a.dir"
 
     assert dvc.diff() == {
-        "added": [
-            {"path": os.path.join("dir", ""), "hash": dir_checksum},
-            {"path": os.path.join("dir", "1"), "hash": digest("1")},
-            {"path": os.path.join("dir", "2"), "hash": digest("2")},
-        ],
+        "added": [{"path": os.path.join("dir", ""), "hash": dir_checksum}],
         "deleted": [],
         "modified": [
             {
@@ -126,15 +124,13 @@ def test_directories(tmp_dir, scm, dvc):
                 "path": os.path.join("dir", ""),
                 "hash": "5fb6b29836c388e093ca0715c872fe2a.dir",
             },
-            {"path": os.path.join("dir", "1"), "hash": digest("1")},
-            {"path": os.path.join("dir", "2"), "hash": digest("2")},
         ],
         "deleted": [],
         "modified": [],
     }
 
     assert dvc.diff(":/directory", ":/modify") == {
-        "added": [{"path": os.path.join("dir", "3"), "hash": digest("3")}],
+        "added": [],
         "deleted": [],
         "modified": [
             {
@@ -144,16 +140,12 @@ def test_directories(tmp_dir, scm, dvc):
                     "new": "9b5faf37366b3370fd98e3e60ca439c1.dir",
                 },
             },
-            {
-                "path": os.path.join("dir", "2"),
-                "hash": {"old": digest("2"), "new": digest("two")},
-            },
         ],
     }
 
     assert dvc.diff(":/modify", ":/delete") == {
         "added": [],
-        "deleted": [{"path": os.path.join("dir", "2"), "hash": digest("two")}],
+        "deleted": [],
         "modified": [
             {
                 "path": os.path.join("dir", ""),
@@ -164,3 +156,24 @@ def test_directories(tmp_dir, scm, dvc):
             }
         ],
     }
+
+
+def test_diff_no_cache(tmp_dir, scm, dvc):
+    (stage,) = tmp_dir.dvc_gen(
+        {"dir": {"file": "file content"}}, commit="first"
+    )
+    scm.tag("v1")
+    tmp_dir.dvc_gen(
+        {"dir": {"file": "modified file content"}}, commit="second"
+    )
+
+    os.remove(first(stage.outs).cache_path)
+    shutil.rmtree("dir")
+
+    # invalidate_dir_info to force cache loading
+    dvc.cache.local._dir_info = {}
+
+    diff = dvc.diff("v1")
+    assert diff["added"] == []
+    assert diff["deleted"] == []
+    assert first(diff["modified"])["path"] == os.path.join("dir", "")