Repository: iterative/dvc

Issue:
`dvc diff --show-hash --show-md <commit>` shows only file paths, doesn't show md5 hash
## Bug Report
`dvc diff` command has option to `--show-hash` for debug purpose. 

Also the output can be formatted to json or md using `--show-json` and `--show-md` option.

Currently `--show-hash` works fine with normal output and `--show-json`. It is not showing hash for `--show-md`.

Example:
## Normal Text
```bash
~ dvc diff --show-hash b34c3e4
Modified:
    84f9553c..201bc54b  data/models/ensemble/metrics.csv
    f9298672..2db2b3bc  data/models/ensemble/model.gz
    f858c58a..aa2ba7d8  data/models/logistic/metrics.csv
    24d7f7f2..00a3ccdf  data/models/logistic/model.gz
```
## Json format:
```bash
~ dvc diff --show-hash --show-json b34c3e4
{"added": [], "deleted": [], "modified": [{"path": "data/models/ensemble/metrics.csv", "hash": {"old": "84f9553c5ddd26baae037290d523446a", "new": "201bc54bbf36d82e5c432da32970effe"}}, {"path": "data/models/ensemble/model.gz", "hash": {"old": "f9298672fe7952354e7bc23cb91fce9b", "new": "2db2b3bc48a82f563001a9da177993ed"}}, {"path": "data/models/logistic/metrics.csv", "hash": {"old": "f858c58a1a5be10510adb64764b610d9", "new": "aa2ba7d85bbfb318bb091f4de1aeaa0e"}}, {"path": "data/models/logistic/model.gz", "hash": {"old": "24d7f7f28c068ce6b6e432391adeef09", "new": "00a3ccdf8f698a4504a2f226ff0052f9"}}]}
```

##  Markdown format
```bash
~ dvc diff --show-hash --show-md b34c3e4
| Status   | Path                              |
|----------|-----------------------------------|
| modified | data/models/ensemble/metrics.csv  |
| modified | data/models/ensemble/model.gz     |
| modified | data/models/logistic/metrics.csv  |
| modified | data/models/logistic/model.gz     |
```

### Please provide information about your setup

**Output of `dvc version`:**

```console
$ dvc version
DVC version: 1.2.2
Python version: 3.8.3
Platform: macOS-10.14.6-x86_64-i386-64bit
Binary: False
Package: pip
Supported remotes: hdfs, http, https, s3
Cache: reflink - supported, hardlink - supported, symlink - supported
Filesystem type (cache directory): ('apfs', '/dev/disk1s1')
Repo: dvc, git
Filesystem type (workspace): ('apfs', '/dev/disk1s1')
```


Discussion:

Resolving patch:
diff --git a/dvc/command/diff.py b/dvc/command/diff.py
--- a/dvc/command/diff.py
+++ b/dvc/command/diff.py
@@ -11,19 +11,31 @@
 logger = logging.getLogger(__name__)
 
 
-def _show_md(diff):
+def _digest(checksum):
+    if isinstance(checksum, str):
+        return checksum[0:8]
+    return "{}..{}".format(checksum["old"][0:8], checksum["new"][0:8])
+
+
+def _show_md(diff, show_hash=False):
     from dvc.utils.diff import table
 
+    header = ["Status", "Hash", "Path"] if show_hash else ["Status", "Path"]
     rows = []
     for status in ["added", "deleted", "modified"]:
         entries = diff.get(status, [])
         if not entries:
             continue
-        paths = sorted(entry["path"] for entry in entries)
-        for path in paths:
-            rows.append([status, path])
+        sorted_entries = sorted(entries, key=lambda entry: entry["path"])
+        for entry in sorted_entries:
+            path = entry["path"]
+            if show_hash:
+                check_sum = _digest(entry.get("hash", ""))
+                rows.append([status, check_sum, path])
+            else:
+                rows.append([status, path])
 
-    return table(["Status", "Path"], rows, True)
+    return table(header, rows, True)
 
 
 class CmdDiff(CmdBase):
@@ -55,11 +67,6 @@ def _format(diff):
         At the bottom, include a summary with the number of files per state.
         """
 
-        def _digest(checksum):
-            if isinstance(checksum, str):
-                return checksum[0:8]
-            return "{}..{}".format(checksum["old"][0:8], checksum["new"][0:8])
-
         colors = {
             "added": colorama.Fore.GREEN,
             "modified": colorama.Fore.YELLOW,
@@ -111,7 +118,8 @@ def run(self):
         try:
             diff = self.repo.diff(self.args.a_rev, self.args.b_rev)
 
-            if not self.args.show_hash:
+            show_hash = self.args.show_hash
+            if not show_hash:
                 for _, entries in diff.items():
                     for entry in entries:
                         del entry["hash"]
@@ -119,7 +127,7 @@ def run(self):
             if self.args.show_json:
                 logger.info(json.dumps(diff))
             elif self.args.show_md:
-                logger.info(_show_md(diff))
+                logger.info(_show_md(diff, show_hash))
             elif diff:
                 logger.info(self._format(diff))
 


Test patch:
diff --git a/tests/unit/command/test_diff.py b/tests/unit/command/test_diff.py
--- a/tests/unit/command/test_diff.py
+++ b/tests/unit/command/test_diff.py
@@ -2,8 +2,23 @@
 import logging
 import os
 
+import mock
+import pytest
+
 from dvc.cli import parse_args
-from dvc.command.diff import _show_md
+from dvc.command.diff import _digest, _show_md
+
+
+@pytest.mark.parametrize(
+    "checksum, expected",
+    [
+        ("wxyz1234pq", "wxyz1234"),
+        (dict(old="1234567890", new="0987654321"), "12345678..09876543"),
+    ],
+    ids=["str", "dict"],
+)
+def test_digest(checksum, expected):
+    assert expected == _digest(checksum)
 
 
 def test_default(mocker, caplog):
@@ -90,6 +105,21 @@ def test_show_json_and_hash(mocker, caplog):
     assert '"modified": []' in caplog.text
 
 
+@pytest.mark.parametrize("show_hash", [None, True, False])
+@mock.patch("dvc.command.diff._show_md")
+def test_diff_show_md_and_hash(mock_show_md, mocker, caplog, show_hash):
+    options = ["diff", "--show-md"] + (["--show-hash"] if show_hash else [])
+    args = parse_args(options)
+    cmd = args.func(args)
+
+    diff = {}
+    show_hash = show_hash if show_hash else False
+    mocker.patch("dvc.repo.Repo.diff", return_value=diff)
+
+    assert 0 == cmd.run()
+    mock_show_md.assert_called_once_with(diff, show_hash)
+
+
 def test_no_changes(mocker, caplog):
     args = parse_args(["diff", "--show-json"])
     cmd = args.func(args)
@@ -120,15 +150,13 @@ def test_show_md_empty():
 def test_show_md():
     diff = {
         "deleted": [
-            {"path": "zoo", "hash": "22222"},
-            {"path": os.path.join("data", ""), "hash": "XXXXXXXX.dir"},
-            {"path": os.path.join("data", "foo"), "hash": "11111111"},
-            {"path": os.path.join("data", "bar"), "hash": "00000000"},
-        ],
-        "modified": [
-            {"path": "file", "hash": {"old": "AAAAAAAA", "new": "BBBBBBBB"}}
+            {"path": "zoo"},
+            {"path": os.path.join("data", "")},
+            {"path": os.path.join("data", "foo")},
+            {"path": os.path.join("data", "bar")},
         ],
-        "added": [{"path": "file", "hash": "00000000"}],
+        "modified": [{"path": "file"}],
+        "added": [{"path": "file"}],
     }
     assert _show_md(diff) == (
         "| Status   | Path     |\n"
@@ -140,3 +168,28 @@ def test_show_md():
         "| deleted  | zoo      |\n"
         "| modified | file     |\n"
     ).format(sep=os.path.sep)
+
+
+def test_show_md_with_hash():
+    diff = {
+        "deleted": [
+            {"path": "zoo", "hash": "22222"},
+            {"path": os.path.join("data", ""), "hash": "XXXXXXXX.dir"},
+            {"path": os.path.join("data", "foo"), "hash": "11111111"},
+            {"path": os.path.join("data", "bar"), "hash": "00000000"},
+        ],
+        "modified": [
+            {"path": "file", "hash": {"old": "AAAAAAAA", "new": "BBBBBBBB"}}
+        ],
+        "added": [{"path": "file", "hash": "00000000"}],
+    }
+    assert _show_md(diff, show_hash=True) == (
+        "| Status   | Hash               | Path     |\n"
+        "|----------|--------------------|----------|\n"
+        "| added    | 00000000           | file     |\n"
+        "| deleted  | XXXXXXXX           | data{sep}    |\n"
+        "| deleted  | 00000000           | data{sep}bar |\n"
+        "| deleted  | 11111111           | data{sep}foo |\n"
+        "| deleted  | 22222              | zoo      |\n"
+        "| modified | AAAAAAAA..BBBBBBBB | file     |\n"
+    ).format(sep=os.path.sep)