Skip to content

Commit 928938a

Browse files
domodwyerasottile
authored andcommitted
fix hooks firing during staged_files_only
1 parent 0670e0b commit 928938a

File tree

5 files changed

+53
-5
lines changed

5 files changed

+53
-5
lines changed

pre_commit/commands/run.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,12 @@ def run(
324324
f'`--hook-stage {args.hook_stage}`',
325325
)
326326
return 1
327+
# prevent recursive post-checkout hooks (#1418)
328+
if (
329+
args.hook_stage == 'post-checkout' and
330+
environ.get('_PRE_COMMIT_SKIP_POST_CHECKOUT')
331+
):
332+
return 0
327333

328334
# Expose from-ref / to-ref as environment variables for hooks to consume
329335
if args.from_ref and args.to_ref:

pre_commit/staged_files_only.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
5656
with open(patch_filename, 'wb') as patch_file:
5757
patch_file.write(diff_stdout_binary)
5858

59-
# Clear the working directory of unstaged changes
60-
cmd_output_b('git', 'checkout', '--', '.')
59+
# prevent recursive post-checkout hooks (#1418)
60+
no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')
61+
cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env)
62+
6163
try:
6264
yield
6365
finally:
@@ -72,8 +74,9 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
7274
# We failed to apply the patch, presumably due to fixes made
7375
# by hooks.
7476
# Roll back the changes made by hooks.
75-
cmd_output_b('git', 'checkout', '--', '.')
77+
cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env)
7678
_git_apply(patch_filename)
79+
7780
logger.info(f'Restored changes from {patch_filename}.')
7881
else:
7982
# There weren't any staged files so we don't need to do anything

testing/util.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,12 @@ def cwd(path):
103103
os.chdir(original_cwd)
104104

105105

106-
def git_commit(*args, fn=cmd_output, msg='commit!', **kwargs):
106+
def git_commit(*args, fn=cmd_output, msg='commit!', all_files=True, **kwargs):
107107
kwargs.setdefault('stderr', subprocess.STDOUT)
108108

109-
cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', '-a') + args
109+
cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', *args)
110+
if all_files: # allow skipping `-a` with `all_files=False`
111+
cmd += ('-a',)
110112
if msg is not None: # allow skipping `-m` with `msg=None`
111113
cmd += ('-m', msg)
112114
ret, out, _ = fn(*cmd, **kwargs)

tests/commands/install_uninstall_test.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,37 @@ def test_post_checkout_integration(tempdir_factory, store):
789789
assert 'some_file' not in stderr
790790

791791

792+
def test_skips_post_checkout_unstaged_changes(tempdir_factory, store):
793+
path = git_dir(tempdir_factory)
794+
config = {
795+
'repo': 'local',
796+
'hooks': [{
797+
'id': 'fail',
798+
'name': 'fail',
799+
'entry': 'fail',
800+
'language': 'fail',
801+
'always_run': True,
802+
'stages': ['post-checkout'],
803+
}],
804+
}
805+
write_config(path, config)
806+
with cwd(path):
807+
cmd_output('git', 'add', '.')
808+
_get_commit_output(tempdir_factory)
809+
810+
install(C.CONFIG_FILE, store, hook_types=['pre-commit'])
811+
install(C.CONFIG_FILE, store, hook_types=['post-checkout'])
812+
813+
# make an unstaged change so staged_files_only fires
814+
open('file', 'a').close()
815+
cmd_output('git', 'add', 'file')
816+
with open('file', 'w') as f:
817+
f.write('unstaged changes')
818+
819+
retc, out = _get_commit_output(tempdir_factory, all_files=False)
820+
assert retc == 0
821+
822+
792823
def test_prepare_commit_msg_integration_failing(
793824
failing_prepare_commit_msg_repo, tempdir_factory, store,
794825
):

tests/commands/run_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,3 +1022,9 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook):
10221022
run_opts(hook='do_not_commit'),
10231023
)
10241024
assert b'identity-copy' not in printed
1025+
1026+
1027+
def test_skipped_without_any_setup_for_post_checkout(in_git_dir, store):
1028+
environ = {'_PRE_COMMIT_SKIP_POST_CHECKOUT': '1'}
1029+
opts = run_opts(hook_stage='post-checkout')
1030+
assert run(C.CONFIG_FILE, store, opts, environ=environ) == 0

0 commit comments

Comments
 (0)