We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
diff: unexpected error when diffing workspace after dvc remove
# Bug Report

## Description

`dvc diff` will raise unexpected error with no other output if both the `.dvc` file and the original data file are removed from the workspace (i.e. after running `dvc remove`).

```
$ git status                                                                                                                  ⏎
On branch master
Changes not staged for commit:
        deleted:    .gitignore
        deleted:    foo.txt.dvc

no changes added to commit

$ dvc diff -v
2020-12-28 15:43:46,270 DEBUG: Check for update is enabled.
2020-12-28 15:43:46,584 ERROR: unexpected error
------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/pmrowla/git/dvc/dvc/main.py", line 90, in main
    ret = cmd.run()
  File "/Users/pmrowla/git/dvc/dvc/command/diff.py", line 131, in run
    diff = self.repo.diff(
  File "/Users/pmrowla/git/dvc/dvc/repo/__init__.py", line 53, in wrapper
    return f(repo, *args, **kwargs)
  File "/Users/pmrowla/git/dvc/dvc/repo/diff.py", line 60, in diff
    missing = sorted(_filter_missing(self, deleted_or_missing))
  File "/Users/pmrowla/git/dvc/dvc/repo/diff.py", line 151, in _filter_missing
    metadata = repo_tree.metadata(path)
  File "/Users/pmrowla/git/dvc/dvc/tree/repo.py", line 446, in metadata
    raise FileNotFoundError
FileNotFoundError
------------------------------------------------------------
```

### Reproduce

```bash
#!/bin/bash

set -e
set -x

REPO="test_repo"

rm -rf $REPO
mkdir $REPO
pushd $REPO

git init
dvc init
echo "foo" > foo.txt
dvc add foo.txt
git add .
git commit -m "init"

dvc remove foo.txt.dvc
rm foo.txt
dvc diff -v

popd
```

This issue only affects workspace diff. If the changes after remove are `git commit`ed and then the two commits are `dvc diff`ed, the diff will work as expected. Issue can also be reproduced by doing `git rm <file>.dvc; rm <file>` instead of using `dvc remove`.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `dvc/repo/diff.py`
Content:
```
1 import logging
2 import os
3 
4 from dvc.exceptions import PathMissingError
5 from dvc.repo import locked
6 from dvc.tree.local import LocalTree
7 from dvc.tree.repo import RepoTree
8 
9 logger = logging.getLogger(__name__)
10 
11 
12 @locked
13 def diff(self, a_rev="HEAD", b_rev=None, targets=None):
14     """
15     By default, it compares the workspace with the last commit's tree.
16 
17     This implementation differs from `git diff` since DVC doesn't have
18     the concept of `index`, but it keeps the same interface, thus,
19     `dvc diff` would be the same as `dvc diff HEAD`.
20     """
21 
22     if self.scm.no_commits:
23         return {}
24 
25     b_rev = b_rev if b_rev else "workspace"
26     results = {}
27     missing_targets = {}
28     for rev in self.brancher(revs=[a_rev, b_rev]):
29         if rev == "workspace" and rev != b_rev:
30             # brancher always returns workspace, but we only need to compute
31             # workspace paths/checksums if b_rev was None
32             continue
33 
34         targets_path_infos = None
35         if targets is not None:
36             # convert targets to path_infos, and capture any missing targets
37             targets_path_infos, missing_targets[rev] = _targets_to_path_infos(
38                 self, targets
39             )
40 
41         results[rev] = _paths_checksums(self, targets_path_infos)
42 
43     if targets is not None:
44         # check for overlapping missing targets between a_rev and b_rev
45         for target in set(missing_targets[a_rev]) & set(
46             missing_targets[b_rev]
47         ):
48             raise PathMissingError(target, self)
49 
50     old = results[a_rev]
51     new = results[b_rev]
52 
53     # Compare paths between the old and new tree.
54     # set() efficiently converts dict keys to a set
55     added = sorted(set(new) - set(old))
56     deleted_or_missing = set(old) - set(new)
57     if b_rev == "workspace":
58         # missing status is only applicable when diffing local workspace
59         # against a commit
60         missing = sorted(_filter_missing(self, deleted_or_missing))
61     else:
62         missing = []
63     deleted = sorted(deleted_or_missing - set(missing))
64     modified = sorted(set(old) & set(new))
65 
66     ret = {
67         "added": [{"path": path, "hash": new[path]} for path in added],
68         "deleted": [{"path": path, "hash": old[path]} for path in deleted],
69         "modified": [
70             {"path": path, "hash": {"old": old[path], "new": new[path]}}
71             for path in modified
72             if old[path] != new[path]
73         ],
74         "not in cache": [
75             {"path": path, "hash": old[path]} for path in missing
76         ],
77     }
78 
79     return ret if any(ret.values()) else {}
80 
81 
82 def _paths_checksums(repo, targets):
83     """
84     A dictionary of checksums addressed by relpaths collected from
85     the current tree outputs.
86 
87     To help distinguish between a directory and a file output,
88     the former one will come with a trailing slash in the path:
89 
90         directory: "data/"
91         file:      "data"
92     """
93 
94     return dict(_output_paths(repo, targets))
95 
96 
97 def _output_paths(repo, targets):
98     repo_tree = RepoTree(repo, stream=True)
99     on_working_tree = isinstance(repo.tree, LocalTree)
100 
101     def _exists(output):
102         if on_working_tree:
103             return output.exists
104         return True
105 
106     def _to_path(output):
107         return (
108             str(output)
109             if not output.is_dir_checksum
110             else os.path.join(str(output), "")
111         )
112 
113     def _to_checksum(output):
114         if on_working_tree:
115             return repo.cache.local.tree.get_hash(output.path_info).value
116         return output.hash_info.value
117 
118     for stage in repo.stages:
119         for output in stage.outs:
120             if _exists(output):
121                 yield_output = targets is None or any(
122                     output.path_info.isin_or_eq(target) for target in targets
123                 )
124 
125                 if yield_output:
126                     yield _to_path(output), _to_checksum(output)
127 
128                 if output.is_dir_checksum and (
129                     yield_output
130                     or any(target.isin(output.path_info) for target in targets)
131                 ):
132                     yield from _dir_output_paths(repo_tree, output, targets)
133 
134 
135 def _dir_output_paths(repo_tree, output, targets=None):
136     from dvc.config import NoRemoteError
137 
138     try:
139         for fname in repo_tree.walk_files(output.path_info):
140             if targets is None or any(
141                 fname.isin_or_eq(target) for target in targets
142             ):
143                 yield str(fname), repo_tree.get_file_hash(fname).value
144     except NoRemoteError:
145         logger.warning("dir cache entry for '%s' is missing", output)
146 
147 
148 def _filter_missing(repo, paths):
149     repo_tree = RepoTree(repo, stream=True)
150     for path in paths:
151         metadata = repo_tree.metadata(path)
152         if metadata.is_dvc:
153             out = metadata.outs[0]
154             if out.status().get(str(out)) == "not in cache":
155                 yield path
156 
157 
158 def _targets_to_path_infos(repo, targets):
159     path_infos = []
160     missing = []
161 
162     repo_tree = RepoTree(repo, stream=True)
163 
164     for target in targets:
165         if repo_tree.exists(target):
166             path_infos.append(repo_tree.metadata(target).path_info)
167         else:
168             missing.append(target)
169 
170     return path_infos, missing
171
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/dvc/repo/diff.py b/dvc/repo/diff.py
--- a/dvc/repo/diff.py
+++ b/dvc/repo/diff.py
@@ -148,11 +148,14 @@
 def _filter_missing(repo, paths):
     repo_tree = RepoTree(repo, stream=True)
     for path in paths:
-        metadata = repo_tree.metadata(path)
-        if metadata.is_dvc:
-            out = metadata.outs[0]
-            if out.status().get(str(out)) == "not in cache":
-                yield path
+        try:
+            metadata = repo_tree.metadata(path)
+            if metadata.is_dvc:
+                out = metadata.outs[0]
+                if out.status().get(str(out)) == "not in cache":
+                    yield path
+        except FileNotFoundError:
+            pass
 
 
 def _targets_to_path_infos(repo, targets):

{"golden_diff": "diff --git a/dvc/repo/diff.py b/dvc/repo/diff.py\n--- a/dvc/repo/diff.py\n+++ b/dvc/repo/diff.py\n@@ -148,11 +148,14 @@\n def _filter_missing(repo, paths):\n     repo_tree = RepoTree(repo, stream=True)\n     for path in paths:\n-        metadata = repo_tree.metadata(path)\n-        if metadata.is_dvc:\n-            out = metadata.outs[0]\n-            if out.status().get(str(out)) == \"not in cache\":\n-                yield path\n+        try:\n+            metadata = repo_tree.metadata(path)\n+            if metadata.is_dvc:\n+                out = metadata.outs[0]\n+                if out.status().get(str(out)) == \"not in cache\":\n+                    yield path\n+        except FileNotFoundError:\n+            pass\n \n \n def _targets_to_path_infos(repo, targets):\n", "issue": "diff: unexpected error when diffing workspace after dvc remove\n# Bug Report\r\n\r\n## Description\r\n\r\n`dvc diff` will raise unexpected error with no other output if both the `.dvc` file and the original data file are removed from the workspace (i.e. after running `dvc remove`).\r\n\r\n```\r\n$ git status                                                                                                                  \u23ce\r\nOn branch master\r\nChanges not staged for commit:\r\n        deleted:    .gitignore\r\n        deleted:    foo.txt.dvc\r\n\r\nno changes added to commit\r\n\r\n$ dvc diff -v\r\n2020-12-28 15:43:46,270 DEBUG: Check for update is enabled.\r\n2020-12-28 15:43:46,584 ERROR: unexpected error\r\n------------------------------------------------------------\r\nTraceback (most recent call last):\r\n  File \"/Users/pmrowla/git/dvc/dvc/main.py\", line 90, in main\r\n    ret = cmd.run()\r\n  File \"/Users/pmrowla/git/dvc/dvc/command/diff.py\", line 131, in run\r\n    diff = self.repo.diff(\r\n  File \"/Users/pmrowla/git/dvc/dvc/repo/__init__.py\", line 53, in wrapper\r\n    return f(repo, *args, **kwargs)\r\n  File \"/Users/pmrowla/git/dvc/dvc/repo/diff.py\", line 60, in diff\r\n    missing = sorted(_filter_missing(self, deleted_or_missing))\r\n  File \"/Users/pmrowla/git/dvc/dvc/repo/diff.py\", line 151, in _filter_missing\r\n    metadata = repo_tree.metadata(path)\r\n  File \"/Users/pmrowla/git/dvc/dvc/tree/repo.py\", line 446, in metadata\r\n    raise FileNotFoundError\r\nFileNotFoundError\r\n------------------------------------------------------------\r\n```\r\n\r\n### Reproduce\r\n\r\n```bash\r\n#!/bin/bash\r\n\r\nset -e\r\nset -x\r\n\r\nREPO=\"test_repo\"\r\n\r\nrm -rf $REPO\r\nmkdir $REPO\r\npushd $REPO\r\n\r\ngit init\r\ndvc init\r\necho \"foo\" > foo.txt\r\ndvc add foo.txt\r\ngit add .\r\ngit commit -m \"init\"\r\n\r\ndvc remove foo.txt.dvc\r\nrm foo.txt\r\ndvc diff -v\r\n\r\npopd\r\n```\r\n\r\nThis issue only affects workspace diff. If the changes after remove are `git commit`ed and then the two commits are `dvc diff`ed, the diff will work as expected. Issue can also be reproduced by doing `git rm <file>.dvc; rm <file>` instead of using `dvc remove`.\n", "before_files": [{"content": "import logging\nimport os\n\nfrom dvc.exceptions import PathMissingError\nfrom dvc.repo import locked\nfrom dvc.tree.local import LocalTree\nfrom dvc.tree.repo import RepoTree\n\nlogger = logging.getLogger(__name__)\n\n\n@locked\ndef diff(self, a_rev=\"HEAD\", b_rev=None, targets=None):\n    \"\"\"\n    By default, it compares the workspace with the last commit's tree.\n\n    This implementation differs from `git diff` since DVC doesn't have\n    the concept of `index`, but it keeps the same interface, thus,\n    `dvc diff` would be the same as `dvc diff HEAD`.\n    \"\"\"\n\n    if self.scm.no_commits:\n        return {}\n\n    b_rev = b_rev if b_rev else \"workspace\"\n    results = {}\n    missing_targets = {}\n    for rev in self.brancher(revs=[a_rev, b_rev]):\n        if rev == \"workspace\" and rev != b_rev:\n            # brancher always returns workspace, but we only need to compute\n            # workspace paths/checksums if b_rev was None\n            continue\n\n        targets_path_infos = None\n        if targets is not None:\n            # convert targets to path_infos, and capture any missing targets\n            targets_path_infos, missing_targets[rev] = _targets_to_path_infos(\n                self, targets\n            )\n\n        results[rev] = _paths_checksums(self, targets_path_infos)\n\n    if targets is not None:\n        # check for overlapping missing targets between a_rev and b_rev\n        for target in set(missing_targets[a_rev]) & set(\n            missing_targets[b_rev]\n        ):\n            raise PathMissingError(target, self)\n\n    old = results[a_rev]\n    new = results[b_rev]\n\n    # Compare paths between the old and new tree.\n    # set() efficiently converts dict keys to a set\n    added = sorted(set(new) - set(old))\n    deleted_or_missing = set(old) - set(new)\n    if b_rev == \"workspace\":\n        # missing status is only applicable when diffing local workspace\n        # against a commit\n        missing = sorted(_filter_missing(self, deleted_or_missing))\n    else:\n        missing = []\n    deleted = sorted(deleted_or_missing - set(missing))\n    modified = sorted(set(old) & set(new))\n\n    ret = {\n        \"added\": [{\"path\": path, \"hash\": new[path]} for path in added],\n        \"deleted\": [{\"path\": path, \"hash\": old[path]} for path in deleted],\n        \"modified\": [\n            {\"path\": path, \"hash\": {\"old\": old[path], \"new\": new[path]}}\n            for path in modified\n            if old[path] != new[path]\n        ],\n        \"not in cache\": [\n            {\"path\": path, \"hash\": old[path]} for path in missing\n        ],\n    }\n\n    return ret if any(ret.values()) else {}\n\n\ndef _paths_checksums(repo, targets):\n    \"\"\"\n    A dictionary of checksums addressed by relpaths collected from\n    the current tree outputs.\n\n    To help distinguish between a directory and a file output,\n    the former one will come with a trailing slash in the path:\n\n        directory: \"data/\"\n        file:      \"data\"\n    \"\"\"\n\n    return dict(_output_paths(repo, targets))\n\n\ndef _output_paths(repo, targets):\n    repo_tree = RepoTree(repo, stream=True)\n    on_working_tree = isinstance(repo.tree, LocalTree)\n\n    def _exists(output):\n        if on_working_tree:\n            return output.exists\n        return True\n\n    def _to_path(output):\n        return (\n            str(output)\n            if not output.is_dir_checksum\n            else os.path.join(str(output), \"\")\n        )\n\n    def _to_checksum(output):\n        if on_working_tree:\n            return repo.cache.local.tree.get_hash(output.path_info).value\n        return output.hash_info.value\n\n    for stage in repo.stages:\n        for output in stage.outs:\n            if _exists(output):\n                yield_output = targets is None or any(\n                    output.path_info.isin_or_eq(target) for target in targets\n                )\n\n                if yield_output:\n                    yield _to_path(output), _to_checksum(output)\n\n                if output.is_dir_checksum and (\n                    yield_output\n                    or any(target.isin(output.path_info) for target in targets)\n                ):\n                    yield from _dir_output_paths(repo_tree, output, targets)\n\n\ndef _dir_output_paths(repo_tree, output, targets=None):\n    from dvc.config import NoRemoteError\n\n    try:\n        for fname in repo_tree.walk_files(output.path_info):\n            if targets is None or any(\n                fname.isin_or_eq(target) for target in targets\n            ):\n                yield str(fname), repo_tree.get_file_hash(fname).value\n    except NoRemoteError:\n        logger.warning(\"dir cache entry for '%s' is missing\", output)\n\n\ndef _filter_missing(repo, paths):\n    repo_tree = RepoTree(repo, stream=True)\n    for path in paths:\n        metadata = repo_tree.metadata(path)\n        if metadata.is_dvc:\n            out = metadata.outs[0]\n            if out.status().get(str(out)) == \"not in cache\":\n                yield path\n\n\ndef _targets_to_path_infos(repo, targets):\n    path_infos = []\n    missing = []\n\n    repo_tree = RepoTree(repo, stream=True)\n\n    for target in targets:\n        if repo_tree.exists(target):\n            path_infos.append(repo_tree.metadata(target).path_info)\n        else:\n            missing.append(target)\n\n    return path_infos, missing\n", "path": "dvc/repo/diff.py"}], "after_files": [{"content": "import logging\nimport os\n\nfrom dvc.exceptions import PathMissingError\nfrom dvc.repo import locked\nfrom dvc.tree.local import LocalTree\nfrom dvc.tree.repo import RepoTree\n\nlogger = logging.getLogger(__name__)\n\n\n@locked\ndef diff(self, a_rev=\"HEAD\", b_rev=None, targets=None):\n    \"\"\"\n    By default, it compares the workspace with the last commit's tree.\n\n    This implementation differs from `git diff` since DVC doesn't have\n    the concept of `index`, but it keeps the same interface, thus,\n    `dvc diff` would be the same as `dvc diff HEAD`.\n    \"\"\"\n\n    if self.scm.no_commits:\n        return {}\n\n    b_rev = b_rev if b_rev else \"workspace\"\n    results = {}\n    missing_targets = {}\n    for rev in self.brancher(revs=[a_rev, b_rev]):\n        if rev == \"workspace\" and rev != b_rev:\n            # brancher always returns workspace, but we only need to compute\n            # workspace paths/checksums if b_rev was None\n            continue\n\n        targets_path_infos = None\n        if targets is not None:\n            # convert targets to path_infos, and capture any missing targets\n            targets_path_infos, missing_targets[rev] = _targets_to_path_infos(\n                self, targets\n            )\n\n        results[rev] = _paths_checksums(self, targets_path_infos)\n\n    if targets is not None:\n        # check for overlapping missing targets between a_rev and b_rev\n        for target in set(missing_targets[a_rev]) & set(\n            missing_targets[b_rev]\n        ):\n            raise PathMissingError(target, self)\n\n    old = results[a_rev]\n    new = results[b_rev]\n\n    # Compare paths between the old and new tree.\n    # set() efficiently converts dict keys to a set\n    added = sorted(set(new) - set(old))\n    deleted_or_missing = set(old) - set(new)\n    if b_rev == \"workspace\":\n        # missing status is only applicable when diffing local workspace\n        # against a commit\n        missing = sorted(_filter_missing(self, deleted_or_missing))\n    else:\n        missing = []\n    deleted = sorted(deleted_or_missing - set(missing))\n    modified = sorted(set(old) & set(new))\n\n    ret = {\n        \"added\": [{\"path\": path, \"hash\": new[path]} for path in added],\n        \"deleted\": [{\"path\": path, \"hash\": old[path]} for path in deleted],\n        \"modified\": [\n            {\"path\": path, \"hash\": {\"old\": old[path], \"new\": new[path]}}\n            for path in modified\n            if old[path] != new[path]\n        ],\n        \"not in cache\": [\n            {\"path\": path, \"hash\": old[path]} for path in missing\n        ],\n    }\n\n    return ret if any(ret.values()) else {}\n\n\ndef _paths_checksums(repo, targets):\n    \"\"\"\n    A dictionary of checksums addressed by relpaths collected from\n    the current tree outputs.\n\n    To help distinguish between a directory and a file output,\n    the former one will come with a trailing slash in the path:\n\n        directory: \"data/\"\n        file:      \"data\"\n    \"\"\"\n\n    return dict(_output_paths(repo, targets))\n\n\ndef _output_paths(repo, targets):\n    repo_tree = RepoTree(repo, stream=True)\n    on_working_tree = isinstance(repo.tree, LocalTree)\n\n    def _exists(output):\n        if on_working_tree:\n            return output.exists\n        return True\n\n    def _to_path(output):\n        return (\n            str(output)\n            if not output.is_dir_checksum\n            else os.path.join(str(output), \"\")\n        )\n\n    def _to_checksum(output):\n        if on_working_tree:\n            return repo.cache.local.tree.get_hash(output.path_info).value\n        return output.hash_info.value\n\n    for stage in repo.stages:\n        for output in stage.outs:\n            if _exists(output):\n                yield_output = targets is None or any(\n                    output.path_info.isin_or_eq(target) for target in targets\n                )\n\n                if yield_output:\n                    yield _to_path(output), _to_checksum(output)\n\n                if output.is_dir_checksum and (\n                    yield_output\n                    or any(target.isin(output.path_info) for target in targets)\n                ):\n                    yield from _dir_output_paths(repo_tree, output, targets)\n\n\ndef _dir_output_paths(repo_tree, output, targets=None):\n    from dvc.config import NoRemoteError\n\n    try:\n        for fname in repo_tree.walk_files(output.path_info):\n            if targets is None or any(\n                fname.isin_or_eq(target) for target in targets\n            ):\n                yield str(fname), repo_tree.get_file_hash(fname).value\n    except NoRemoteError:\n        logger.warning(\"dir cache entry for '%s' is missing\", output)\n\n\ndef _filter_missing(repo, paths):\n    repo_tree = RepoTree(repo, stream=True)\n    for path in paths:\n        try:\n            metadata = repo_tree.metadata(path)\n            if metadata.is_dvc:\n                out = metadata.outs[0]\n                if out.status().get(str(out)) == \"not in cache\":\n                    yield path\n        except FileNotFoundError:\n            pass\n\n\ndef _targets_to_path_infos(repo, targets):\n    path_infos = []\n    missing = []\n\n    repo_tree = RepoTree(repo, stream=True)\n\n    for target in targets:\n        if repo_tree.exists(target):\n            path_infos.append(repo_tree.metadata(target).path_info)\n        else:\n            missing.append(target)\n\n    return path_infos, missing\n", "path": "dvc/repo/diff.py"}]}