We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
pre-commit can delete/revert unstaged files if error occurs during git diff-index
### search you tried in the issue tracker

diff-index

### describe your issue

I performed a git commit with some modifications unstaged.  After the commit, most of the modifications had been reverted and my work was lost.  The diff saved in the patch directory had only a few of the modifications in - the ones that survived. The rest were gone.

To reproduce:
- Modify four files and stage one with `git add`
- Use `git status` to determine the order of the three unstaged files.
- Change the permission on the middle one so that git will not be able to read it
- Now do `git commit`: the changes to the first unstaged file will be preserved but the other two will be lost.

The key point, I think, is that the code in `staged_files_only.py` checks that the return code when creating the diff is non-zero which it takes to mean that the code is `1` meaning that there were diffs.   However, in this case the return code is `128` which *is* non-zero but does _not_ mean success - it means error.  So the code assumes the diff is OK even though it is incomplete.

### pre-commit --version

2.17.0

### .pre-commit-config.yaml

```yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: 56b4a7e506901ff86f8de5c2551bc41f8eacf717
  hooks:
     -   id: check-yaml
#     -   id: end-of-file-fixer
     -   id: trailing-whitespace
- repo: https://github.com/psf/black
  rev: 21.11b0
  hooks:
    -   id: black
        language_version: python3.6
- repo: https://github.com/PyCQA/isort
  rev: 5.10.1
  hooks:
    -   id: isort
        args: ["--profile", "black", "--filter-files"]
```


### ~/.cache/pre-commit/pre-commit.log (if present)

_No response_

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `pre_commit/staged_files_only.py`
Content:
```
1 from __future__ import annotations
2 
3 import contextlib
4 import logging
5 import os.path
6 import time
7 from typing import Generator
8 
9 from pre_commit import git
10 from pre_commit.util import CalledProcessError
11 from pre_commit.util import cmd_output
12 from pre_commit.util import cmd_output_b
13 from pre_commit.xargs import xargs
14 
15 
16 logger = logging.getLogger('pre_commit')
17 
18 # without forcing submodule.recurse=0, changes in nested submodules will be
19 # discarded if `submodule.recurse=1` is configured
20 # we choose this instead of `--no-recurse-submodules` because it works on
21 # versions of git before that option was added to `git checkout`
22 _CHECKOUT_CMD = ('git', '-c', 'submodule.recurse=0', 'checkout', '--', '.')
23 
24 
25 def _git_apply(patch: str) -> None:
26     args = ('apply', '--whitespace=nowarn', patch)
27     try:
28         cmd_output_b('git', *args)
29     except CalledProcessError:
30         # Retry with autocrlf=false -- see #570
31         cmd_output_b('git', '-c', 'core.autocrlf=false', *args)
32 
33 
34 @contextlib.contextmanager
35 def _intent_to_add_cleared() -> Generator[None, None, None]:
36     intent_to_add = git.intent_to_add_files()
37     if intent_to_add:
38         logger.warning('Unstaged intent-to-add files detected.')
39 
40         xargs(('git', 'rm', '--cached', '--'), intent_to_add)
41         try:
42             yield
43         finally:
44             xargs(('git', 'add', '--intent-to-add', '--'), intent_to_add)
45     else:
46         yield
47 
48 
49 @contextlib.contextmanager
50 def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
51     tree = cmd_output('git', 'write-tree')[1].strip()
52     retcode, diff_stdout_binary, _ = cmd_output_b(
53         'git', 'diff-index', '--ignore-submodules', '--binary',
54         '--exit-code', '--no-color', '--no-ext-diff', tree, '--',
55         check=False,
56     )
57     if retcode and diff_stdout_binary.strip():
58         patch_filename = f'patch{int(time.time())}-{os.getpid()}'
59         patch_filename = os.path.join(patch_dir, patch_filename)
60         logger.warning('Unstaged files detected.')
61         logger.info(f'Stashing unstaged files to {patch_filename}.')
62         # Save the current unstaged changes as a patch
63         os.makedirs(patch_dir, exist_ok=True)
64         with open(patch_filename, 'wb') as patch_file:
65             patch_file.write(diff_stdout_binary)
66 
67         # prevent recursive post-checkout hooks (#1418)
68         no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')
69 
70         try:
71             cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)
72             yield
73         finally:
74             # Try to apply the patch we saved
75             try:
76                 _git_apply(patch_filename)
77             except CalledProcessError:
78                 logger.warning(
79                     'Stashed changes conflicted with hook auto-fixes... '
80                     'Rolling back fixes...',
81                 )
82                 # We failed to apply the patch, presumably due to fixes made
83                 # by hooks.
84                 # Roll back the changes made by hooks.
85                 cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)
86                 _git_apply(patch_filename)
87 
88             logger.info(f'Restored changes from {patch_filename}.')
89     else:
90         # There weren't any staged files so we don't need to do anything
91         # special
92         yield
93 
94 
95 @contextlib.contextmanager
96 def staged_files_only(patch_dir: str) -> Generator[None, None, None]:
97     """Clear any unstaged changes from the git working directory inside this
98     context.
99     """
100     with _intent_to_add_cleared(), _unstaged_changes_cleared(patch_dir):
101         yield
102
```
--- 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/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py
--- a/pre_commit/staged_files_only.py
+++ b/pre_commit/staged_files_only.py
@@ -7,6 +7,7 @@
 from typing import Generator
 
 from pre_commit import git
+from pre_commit.errors import FatalError
 from pre_commit.util import CalledProcessError
 from pre_commit.util import cmd_output
 from pre_commit.util import cmd_output_b
@@ -49,12 +50,16 @@
 @contextlib.contextmanager
 def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
     tree = cmd_output('git', 'write-tree')[1].strip()
-    retcode, diff_stdout_binary, _ = cmd_output_b(
+    diff_cmd = (
         'git', 'diff-index', '--ignore-submodules', '--binary',
         '--exit-code', '--no-color', '--no-ext-diff', tree, '--',
-        check=False,
     )
-    if retcode and diff_stdout_binary.strip():
+    retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False)
+    if retcode == 0:
+        # There weren't any staged files so we don't need to do anything
+        # special
+        yield
+    elif retcode == 1 and diff_stdout.strip():
         patch_filename = f'patch{int(time.time())}-{os.getpid()}'
         patch_filename = os.path.join(patch_dir, patch_filename)
         logger.warning('Unstaged files detected.')
@@ -62,7 +67,7 @@
         # Save the current unstaged changes as a patch
         os.makedirs(patch_dir, exist_ok=True)
         with open(patch_filename, 'wb') as patch_file:
-            patch_file.write(diff_stdout_binary)
+            patch_file.write(diff_stdout)
 
         # prevent recursive post-checkout hooks (#1418)
         no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')
@@ -86,10 +91,12 @@
                 _git_apply(patch_filename)
 
             logger.info(f'Restored changes from {patch_filename}.')
-    else:
-        # There weren't any staged files so we don't need to do anything
-        # special
-        yield
+    else:  # pragma: win32 no cover
+        # some error occurred while requesting the diff
+        e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr)
+        raise FatalError(
+            f'pre-commit failed to diff -- perhaps due to permissions?\n\n{e}',
+        )
 
 
 @contextlib.contextmanager

{"golden_diff": "diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py\n--- a/pre_commit/staged_files_only.py\n+++ b/pre_commit/staged_files_only.py\n@@ -7,6 +7,7 @@\n from typing import Generator\n \n from pre_commit import git\n+from pre_commit.errors import FatalError\n from pre_commit.util import CalledProcessError\n from pre_commit.util import cmd_output\n from pre_commit.util import cmd_output_b\n@@ -49,12 +50,16 @@\n @contextlib.contextmanager\n def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:\n     tree = cmd_output('git', 'write-tree')[1].strip()\n-    retcode, diff_stdout_binary, _ = cmd_output_b(\n+    diff_cmd = (\n         'git', 'diff-index', '--ignore-submodules', '--binary',\n         '--exit-code', '--no-color', '--no-ext-diff', tree, '--',\n-        check=False,\n     )\n-    if retcode and diff_stdout_binary.strip():\n+    retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False)\n+    if retcode == 0:\n+        # There weren't any staged files so we don't need to do anything\n+        # special\n+        yield\n+    elif retcode == 1 and diff_stdout.strip():\n         patch_filename = f'patch{int(time.time())}-{os.getpid()}'\n         patch_filename = os.path.join(patch_dir, patch_filename)\n         logger.warning('Unstaged files detected.')\n@@ -62,7 +67,7 @@\n         # Save the current unstaged changes as a patch\n         os.makedirs(patch_dir, exist_ok=True)\n         with open(patch_filename, 'wb') as patch_file:\n-            patch_file.write(diff_stdout_binary)\n+            patch_file.write(diff_stdout)\n \n         # prevent recursive post-checkout hooks (#1418)\n         no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')\n@@ -86,10 +91,12 @@\n                 _git_apply(patch_filename)\n \n             logger.info(f'Restored changes from {patch_filename}.')\n-    else:\n-        # There weren't any staged files so we don't need to do anything\n-        # special\n-        yield\n+    else:  # pragma: win32 no cover\n+        # some error occurred while requesting the diff\n+        e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr)\n+        raise FatalError(\n+            f'pre-commit failed to diff -- perhaps due to permissions?\\n\\n{e}',\n+        )\n \n \n @contextlib.contextmanager\n", "issue": "pre-commit can delete/revert unstaged files if error occurs during git diff-index\n### search you tried in the issue tracker\n\ndiff-index\n\n### describe your issue\n\nI performed a git commit with some modifications unstaged.  After the commit, most of the modifications had been reverted and my work was lost.  The diff saved in the patch directory had only a few of the modifications in - the ones that survived. The rest were gone.\r\n\r\nTo reproduce:\r\n- Modify four files and stage one with `git add`\r\n- Use `git status` to determine the order of the three unstaged files.\r\n- Change the permission on the middle one so that git will not be able to read it\r\n- Now do `git commit`: the changes to the first unstaged file will be preserved but the other two will be lost.\r\n\r\nThe key point, I think, is that the code in `staged_files_only.py` checks that the return code when creating the diff is non-zero which it takes to mean that the code is `1` meaning that there were diffs.   However, in this case the return code is `128` which *is* non-zero but does _not_ mean success - it means error.  So the code assumes the diff is OK even though it is incomplete.\n\n### pre-commit --version\n\n2.17.0\n\n### .pre-commit-config.yaml\n\n```yaml\nrepos:\r\n- repo: https://github.com/pre-commit/pre-commit-hooks\r\n  rev: 56b4a7e506901ff86f8de5c2551bc41f8eacf717\r\n  hooks:\r\n     -   id: check-yaml\r\n#     -   id: end-of-file-fixer\r\n     -   id: trailing-whitespace\r\n- repo: https://github.com/psf/black\r\n  rev: 21.11b0\r\n  hooks:\r\n    -   id: black\r\n        language_version: python3.6\r\n- repo: https://github.com/PyCQA/isort\r\n  rev: 5.10.1\r\n  hooks:\r\n    -   id: isort\r\n        args: [\"--profile\", \"black\", \"--filter-files\"]\n```\n\n\n### ~/.cache/pre-commit/pre-commit.log (if present)\n\n_No response_\n", "before_files": [{"content": "from __future__ import annotations\n\nimport contextlib\nimport logging\nimport os.path\nimport time\nfrom typing import Generator\n\nfrom pre_commit import git\nfrom pre_commit.util import CalledProcessError\nfrom pre_commit.util import cmd_output\nfrom pre_commit.util import cmd_output_b\nfrom pre_commit.xargs import xargs\n\n\nlogger = logging.getLogger('pre_commit')\n\n# without forcing submodule.recurse=0, changes in nested submodules will be\n# discarded if `submodule.recurse=1` is configured\n# we choose this instead of `--no-recurse-submodules` because it works on\n# versions of git before that option was added to `git checkout`\n_CHECKOUT_CMD = ('git', '-c', 'submodule.recurse=0', 'checkout', '--', '.')\n\n\ndef _git_apply(patch: str) -> None:\n    args = ('apply', '--whitespace=nowarn', patch)\n    try:\n        cmd_output_b('git', *args)\n    except CalledProcessError:\n        # Retry with autocrlf=false -- see #570\n        cmd_output_b('git', '-c', 'core.autocrlf=false', *args)\n\n\n@contextlib.contextmanager\ndef _intent_to_add_cleared() -> Generator[None, None, None]:\n    intent_to_add = git.intent_to_add_files()\n    if intent_to_add:\n        logger.warning('Unstaged intent-to-add files detected.')\n\n        xargs(('git', 'rm', '--cached', '--'), intent_to_add)\n        try:\n            yield\n        finally:\n            xargs(('git', 'add', '--intent-to-add', '--'), intent_to_add)\n    else:\n        yield\n\n\n@contextlib.contextmanager\ndef _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:\n    tree = cmd_output('git', 'write-tree')[1].strip()\n    retcode, diff_stdout_binary, _ = cmd_output_b(\n        'git', 'diff-index', '--ignore-submodules', '--binary',\n        '--exit-code', '--no-color', '--no-ext-diff', tree, '--',\n        check=False,\n    )\n    if retcode and diff_stdout_binary.strip():\n        patch_filename = f'patch{int(time.time())}-{os.getpid()}'\n        patch_filename = os.path.join(patch_dir, patch_filename)\n        logger.warning('Unstaged files detected.')\n        logger.info(f'Stashing unstaged files to {patch_filename}.')\n        # Save the current unstaged changes as a patch\n        os.makedirs(patch_dir, exist_ok=True)\n        with open(patch_filename, 'wb') as patch_file:\n            patch_file.write(diff_stdout_binary)\n\n        # prevent recursive post-checkout hooks (#1418)\n        no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')\n\n        try:\n            cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)\n            yield\n        finally:\n            # Try to apply the patch we saved\n            try:\n                _git_apply(patch_filename)\n            except CalledProcessError:\n                logger.warning(\n                    'Stashed changes conflicted with hook auto-fixes... '\n                    'Rolling back fixes...',\n                )\n                # We failed to apply the patch, presumably due to fixes made\n                # by hooks.\n                # Roll back the changes made by hooks.\n                cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)\n                _git_apply(patch_filename)\n\n            logger.info(f'Restored changes from {patch_filename}.')\n    else:\n        # There weren't any staged files so we don't need to do anything\n        # special\n        yield\n\n\n@contextlib.contextmanager\ndef staged_files_only(patch_dir: str) -> Generator[None, None, None]:\n    \"\"\"Clear any unstaged changes from the git working directory inside this\n    context.\n    \"\"\"\n    with _intent_to_add_cleared(), _unstaged_changes_cleared(patch_dir):\n        yield\n", "path": "pre_commit/staged_files_only.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport contextlib\nimport logging\nimport os.path\nimport time\nfrom typing import Generator\n\nfrom pre_commit import git\nfrom pre_commit.errors import FatalError\nfrom pre_commit.util import CalledProcessError\nfrom pre_commit.util import cmd_output\nfrom pre_commit.util import cmd_output_b\nfrom pre_commit.xargs import xargs\n\n\nlogger = logging.getLogger('pre_commit')\n\n# without forcing submodule.recurse=0, changes in nested submodules will be\n# discarded if `submodule.recurse=1` is configured\n# we choose this instead of `--no-recurse-submodules` because it works on\n# versions of git before that option was added to `git checkout`\n_CHECKOUT_CMD = ('git', '-c', 'submodule.recurse=0', 'checkout', '--', '.')\n\n\ndef _git_apply(patch: str) -> None:\n    args = ('apply', '--whitespace=nowarn', patch)\n    try:\n        cmd_output_b('git', *args)\n    except CalledProcessError:\n        # Retry with autocrlf=false -- see #570\n        cmd_output_b('git', '-c', 'core.autocrlf=false', *args)\n\n\n@contextlib.contextmanager\ndef _intent_to_add_cleared() -> Generator[None, None, None]:\n    intent_to_add = git.intent_to_add_files()\n    if intent_to_add:\n        logger.warning('Unstaged intent-to-add files detected.')\n\n        xargs(('git', 'rm', '--cached', '--'), intent_to_add)\n        try:\n            yield\n        finally:\n            xargs(('git', 'add', '--intent-to-add', '--'), intent_to_add)\n    else:\n        yield\n\n\n@contextlib.contextmanager\ndef _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:\n    tree = cmd_output('git', 'write-tree')[1].strip()\n    diff_cmd = (\n        'git', 'diff-index', '--ignore-submodules', '--binary',\n        '--exit-code', '--no-color', '--no-ext-diff', tree, '--',\n    )\n    retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False)\n    if retcode == 0:\n        # There weren't any staged files so we don't need to do anything\n        # special\n        yield\n    elif retcode == 1 and diff_stdout.strip():\n        patch_filename = f'patch{int(time.time())}-{os.getpid()}'\n        patch_filename = os.path.join(patch_dir, patch_filename)\n        logger.warning('Unstaged files detected.')\n        logger.info(f'Stashing unstaged files to {patch_filename}.')\n        # Save the current unstaged changes as a patch\n        os.makedirs(patch_dir, exist_ok=True)\n        with open(patch_filename, 'wb') as patch_file:\n            patch_file.write(diff_stdout)\n\n        # prevent recursive post-checkout hooks (#1418)\n        no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')\n\n        try:\n            cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)\n            yield\n        finally:\n            # Try to apply the patch we saved\n            try:\n                _git_apply(patch_filename)\n            except CalledProcessError:\n                logger.warning(\n                    'Stashed changes conflicted with hook auto-fixes... '\n                    'Rolling back fixes...',\n                )\n                # We failed to apply the patch, presumably due to fixes made\n                # by hooks.\n                # Roll back the changes made by hooks.\n                cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)\n                _git_apply(patch_filename)\n\n            logger.info(f'Restored changes from {patch_filename}.')\n    else:  # pragma: win32 no cover\n        # some error occurred while requesting the diff\n        e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr)\n        raise FatalError(\n            f'pre-commit failed to diff -- perhaps due to permissions?\\n\\n{e}',\n        )\n\n\n@contextlib.contextmanager\ndef staged_files_only(patch_dir: str) -> Generator[None, None, None]:\n    \"\"\"Clear any unstaged changes from the git working directory inside this\n    context.\n    \"\"\"\n    with _intent_to_add_cleared(), _unstaged_changes_cleared(patch_dir):\n        yield\n", "path": "pre_commit/staged_files_only.py"}]}