prompt: 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()
  
  ```
  
golden_diff: 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
  
verification_info: {"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"}]}