Skip to content

Commit 192be60

Browse files
authored
Merge pull request #2774 from pre-commit/diff-must-succeed
only treat exit code 1 as a successful diff
2 parents 8ba9bc6 + cddc9cf commit 192be60

File tree

2 files changed

+67
-8
lines changed

2 files changed

+67
-8
lines changed

pre_commit/staged_files_only.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Generator
88

99
from pre_commit import git
10+
from pre_commit.errors import FatalError
1011
from pre_commit.util import CalledProcessError
1112
from pre_commit.util import cmd_output
1213
from pre_commit.util import cmd_output_b
@@ -49,20 +50,24 @@ def _intent_to_add_cleared() -> Generator[None, None, None]:
4950
@contextlib.contextmanager
5051
def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
5152
tree = cmd_output('git', 'write-tree')[1].strip()
52-
retcode, diff_stdout_binary, _ = cmd_output_b(
53+
diff_cmd = (
5354
'git', 'diff-index', '--ignore-submodules', '--binary',
5455
'--exit-code', '--no-color', '--no-ext-diff', tree, '--',
55-
check=False,
5656
)
57-
if retcode and diff_stdout_binary.strip():
57+
retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False)
58+
if retcode == 0:
59+
# There weren't any staged files so we don't need to do anything
60+
# special
61+
yield
62+
elif retcode == 1 and diff_stdout.strip():
5863
patch_filename = f'patch{int(time.time())}-{os.getpid()}'
5964
patch_filename = os.path.join(patch_dir, patch_filename)
6065
logger.warning('Unstaged files detected.')
6166
logger.info(f'Stashing unstaged files to {patch_filename}.')
6267
# Save the current unstaged changes as a patch
6368
os.makedirs(patch_dir, exist_ok=True)
6469
with open(patch_filename, 'wb') as patch_file:
65-
patch_file.write(diff_stdout_binary)
70+
patch_file.write(diff_stdout)
6671

6772
# prevent recursive post-checkout hooks (#1418)
6873
no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')
@@ -86,10 +91,12 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
8691
_git_apply(patch_filename)
8792

8893
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
94+
else: # pragma: win32 no cover
95+
# some error occurred while requesting the diff
96+
e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr)
97+
raise FatalError(
98+
f'pre-commit failed to diff -- perhaps due to permissions?\n\n{e}',
99+
)
93100

94101

95102
@contextlib.contextmanager

tests/staged_files_only_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
from __future__ import annotations
22

3+
import contextlib
34
import itertools
45
import os.path
56
import shutil
67

78
import pytest
9+
import re_assert
810

911
from pre_commit import git
12+
from pre_commit.errors import FatalError
1013
from pre_commit.staged_files_only import staged_files_only
1114
from pre_commit.util import cmd_output
1215
from testing.auto_namedtuple import auto_namedtuple
1316
from testing.fixtures import git_dir
1417
from testing.util import cwd
1518
from testing.util import get_resource_path
1619
from testing.util import git_commit
20+
from testing.util import xfailif_windows
1721

1822

1923
FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', ''))
@@ -382,3 +386,51 @@ def test_intent_to_add(in_git_dir, patch_dir):
382386
with staged_files_only(patch_dir):
383387
assert_no_diff()
384388
assert git.intent_to_add_files() == ['foo']
389+
390+
391+
@contextlib.contextmanager
392+
def _unreadable(f):
393+
orig = os.stat(f).st_mode
394+
os.chmod(f, 0o000)
395+
try:
396+
yield
397+
finally:
398+
os.chmod(f, orig)
399+
400+
401+
@xfailif_windows # pragma: win32 no cover
402+
def test_failed_diff_does_not_discard_changes(in_git_dir, patch_dir):
403+
# stage 3 files
404+
for i in range(3):
405+
with open(str(i), 'w') as f:
406+
f.write(str(i))
407+
cmd_output('git', 'add', '0', '1', '2')
408+
409+
# modify all of their contents
410+
for i in range(3):
411+
with open(str(i), 'w') as f:
412+
f.write('new contents')
413+
414+
with _unreadable('1'):
415+
with pytest.raises(FatalError) as excinfo:
416+
with staged_files_only(patch_dir):
417+
raise AssertionError('should have errored on enter')
418+
419+
# the diff command failed to produce a diff of `1`
420+
msg, = excinfo.value.args
421+
re_assert.Matches(
422+
r'^pre-commit failed to diff -- perhaps due to permissions\?\n\n'
423+
r'command: .*\n'
424+
r'return code: 128\n'
425+
r'stdout: \(none\)\n'
426+
r'stderr:\n'
427+
r' error: open\("1"\): Permission denied\n'
428+
r' fatal: cannot hash 1\n'
429+
# TODO: not sure why there's weird whitespace here
430+
r' $',
431+
).assert_matches(msg)
432+
433+
# even though it errored, the unstaged changes should still be present
434+
for i in range(3):
435+
with open(str(i)) as f:
436+
assert f.read() == 'new contents'

0 commit comments

Comments
 (0)