Repository: iterative/dvc

Issue:
diff: compare dirty vs committed workspace by default
By default `dvc diff` compares workspace to HEAD (or to a single commit if only one arg is provided). This is the one of the most important use cases. I would like to see the "uncommitted" (in a DVC-sense - when we update md5) changes to data files and directories:

```
git clone https://github.com/iterative/example-get-started
cd example-get-started
dvc pull
rm -f data/data.xml
touch data/features/foo
dvc diff
```

The last command produces an empty result. Not very informative to compare my "current" experiment with the baseline.

UPDATE:

Another interesting point that `dvc status` does exactly this - it analyzes actual changes.



Comments:
> Another interesting point that `dvc status` does exactly this - it analyzes actual changes.

Exactly. We decided not to cover this case in the first `diff` implementation and created #3255.
Duplicate of #3255
I would not consider this as a duplicate of that larger discussion (basically what status should be doing). They are relevant indeed, but if we more or less understand what do we expect from `dvc diff` let's focus on this one? And I would vote for showing the changes (uncommitted in the DVC sense).
Reopening this for now, let's keep both since it's an important issue to discuss and solve if needed. If we are lucky enough to close both at once that would be a great outcome.
> Seems like maybe this issue and #3385 could be merged? I'm just seeing lots of diff issues, may be hard to manage them.

Patch:
diff --git a/dvc/repo/diff.py b/dvc/repo/diff.py
--- a/dvc/repo/diff.py
+++ b/dvc/repo/diff.py
@@ -3,6 +3,7 @@
 from dvc.exceptions import DvcException
 from dvc.repo import locked
 from dvc.scm.git import Git
+from dvc.scm.tree import is_working_tree
 
 
 @locked
@@ -36,10 +37,23 @@ def _to_path(output):
                 else os.path.join(str(output), "")
             )
 
+        on_working_tree = is_working_tree(self.tree)
+
+        def _to_checksum(output):
+            if on_working_tree:
+                return self.cache.local.get_checksum(output.path_info)
+            return output.checksum
+
+        def _exists(output):
+            if on_working_tree:
+                return output.exists
+            return True
+
         return {
-            _to_path(output): output.checksum
+            _to_path(output): _to_checksum(output)
             for stage in self.stages
             for output in stage.outs
+            if _exists(output)
         }
 
     working_tree = self.tree


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
@@ -159,21 +159,51 @@ 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"
-    )
+    tmp_dir.dvc_gen({"dir": {"file": "file content"}}, commit="first")
     scm.tag("v1")
+
     tmp_dir.dvc_gen(
         {"dir": {"file": "modified file content"}}, commit="second"
     )
+    scm.tag("v2")
 
-    remove(first(stage.outs).cache_path)
-    remove("dir")
+    remove(dvc.cache.local.cache_dir)
 
     # invalidate_dir_info to force cache loading
     dvc.cache.local._dir_info = {}
 
-    diff = dvc.diff("v1")
+    diff = dvc.diff("v1", "v2")
     assert diff["added"] == []
     assert diff["deleted"] == []
     assert first(diff["modified"])["path"] == os.path.join("dir", "")
+
+
+def test_diff_dirty(tmp_dir, scm, dvc):
+    tmp_dir.dvc_gen(
+        {"file": "file_content", "dir": {"dir_file1": "dir file content"}},
+        commit="initial",
+    )
+
+    (tmp_dir / "file").unlink()
+    tmp_dir.gen({"dir": {"dir_file2": "dir file 2 content"}})
+    tmp_dir.dvc_gen("new_file", "new_file_content")
+
+    result = dvc.diff()
+
+    assert result == {
+        "added": [
+            {"hash": "86d049de17c76ac44cdcac146042ec9b", "path": "new_file"}
+        ],
+        "deleted": [
+            {"hash": "7f0b6bb0b7e951b7fd2b2a4a326297e1", "path": "file"}
+        ],
+        "modified": [
+            {
+                "hash": {
+                    "new": "38175ad60f0e58ac94e0e2b7688afd81.dir",
+                    "old": "92daf39af116ca2fb245acaeb2ae65f7.dir",
+                },
+                "path": os.path.join("dir", ""),
+            }
+        ],
+    }