From 18fa0042541b9fdbf65f6ea6285e4ef5a19e796f Mon Sep 17 00:00:00 2001 From: Andrew Hare Date: Thu, 20 Feb 2020 02:21:29 -0700 Subject: [PATCH] Add post-checkout --- pre_commit/commands/hook_impl.py | 7 +++++++ pre_commit/commands/run.py | 3 +++ pre_commit/constants.py | 2 +- pre_commit/main.py | 20 +++++++++++++++--- testing/util.py | 2 ++ tests/commands/hook_impl_test.py | 10 +++++++++ tests/commands/install_uninstall_test.py | 26 ++++++++++++++++++++++++ tests/commands/run_test.py | 10 +++++++++ tests/repository_test.py | 2 +- 9 files changed, 77 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 0916c02b..890cedb5 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -74,6 +74,7 @@ def _ns( remote_name: Optional[str] = None, remote_url: Optional[str] = None, commit_msg_filename: Optional[str] = None, + checkout_type: Optional[str] = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -84,6 +85,7 @@ def _ns( remote_url=remote_url, commit_msg_filename=commit_msg_filename, all_files=all_files, + checkout_type=checkout_type, files=(), hook=None, verbose=False, @@ -157,6 +159,11 @@ def _run_ns( return _ns(hook_type, color, commit_msg_filename=args[0]) elif hook_type in {'pre-merge-commit', 'pre-commit'}: return _ns(hook_type, color) + elif hook_type == 'post-checkout': + return _ns( + hook_type, color, source=args[0], origin=args[1], + checkout_type=args[2], + ) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95f8ab41..30970efd 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -316,6 +316,9 @@ def run( environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url + if args.checkout_type: + environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type + with contextlib.ExitStack() as exit_stack: if stash: exit_stack.enter_context(staged_files_only(store.directory)) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 23622ecb..e2b8e3ac 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -18,7 +18,7 @@ VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 STAGES = ( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'manual', - 'push', + 'post-checkout', 'push', ) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index 1d849c05..47dd73a5 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -79,7 +79,7 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', choices=( 'pre-commit', 'pre-merge-commit', 'pre-push', - 'prepare-commit-msg', 'commit-msg', + 'prepare-commit-msg', 'commit-msg', 'post-checkout', ), action=AppendReplaceDefault, default=['pre-commit'], @@ -92,11 +92,17 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument('--verbose', '-v', action='store_true', default=False) parser.add_argument( '--origin', '-o', - help="The origin branch's commit_id when using `git push`.", + help=( + "The origin branch's commit_id when using `git push`. " + 'The ref of the previous HEAD when using `git checkout`.' + ), ) parser.add_argument( '--source', '-s', - help="The remote branch's commit_id when using `git push`.", + help=( + "The remote branch's commit_id when using `git push`. " + 'The ref of the new HEAD when using `git checkout`.' + ), ) parser.add_argument( '--commit-msg-filename', @@ -123,6 +129,14 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--files', nargs='*', default=[], help='Specific filenames to run hooks on.', ) + parser.add_argument( + '--checkout-type', + help=( + 'Indicates whether the checkout was a branch checkout ' + '(changing branches, flag=1) or a file checkout (retrieving a ' + 'file from the index, flag=0).' + ), + ) def _adjust_args_and_chdir(args: argparse.Namespace) -> None: diff --git a/testing/util.py b/testing/util.py index ce3206eb..2875993c 100644 --- a/testing/util.py +++ b/testing/util.py @@ -72,6 +72,7 @@ def run_opts( hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', + checkout_type='', ): # These are mutually exclusive assert not (all_files and files) @@ -88,6 +89,7 @@ def run_opts( hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, + checkout_type=checkout_type, ) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 8fdbd0fa..556ea363 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -104,6 +104,16 @@ def test_run_ns_commit_msg(): assert ns.commit_msg_filename == '.git/COMMIT_MSG' +def test_run_ns_post_checkout(): + ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') + assert ns is not None + assert ns.hook_stage == 'post-checkout' + assert ns.color is True + assert ns.source == 'a' + assert ns.origin == 'b' + assert ns.checkout_type == 'c' + + @pytest.fixture def push_example(tempdir_factory): src = git_dir(tempdir_factory) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index e8e72616..c76c303c 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -20,6 +20,7 @@ from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import remove_config_from_repo +from testing.fixtures import write_config from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit @@ -725,6 +726,31 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): assert second_line.startswith('Must have "Signed off by:"...') +def test_post_checkout_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-checkout', + 'name': 'Post checkout', + 'entry': 'bash -c "echo ${PRE_COMMIT_ORIGIN}"', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-checkout'], + }], + } + write_config(path, config) + with cwd(path): + cmd_output('git', 'add', '.') + git_commit() + install(C.CONFIG_FILE, store, hook_types=['post-checkout']) + retc, _, stderr = cmd_output('git', 'checkout', '-b', 'feature') + assert retc == 0 + _, head, _ = cmd_output('git', 'rev-parse', 'HEAD') + assert head in str(stderr) + + def test_prepare_commit_msg_integration_failing( failing_prepare_commit_msg_repo, tempdir_factory, store, ): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 87eef2ec..06ec2f3d 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -18,6 +18,7 @@ from pre_commit.commands.run import Classifier from pre_commit.commands.run import filter_by_include_exclude from pre_commit.commands.run import run from pre_commit.util import cmd_output +from pre_commit.util import EnvironT from pre_commit.util import make_executable from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo @@ -466,6 +467,15 @@ def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): assert b'Specify both --origin and --source.' not in printed +def test_checkout_type(cap_out, store, repo_with_passing_hook): + args = run_opts(origin='', source='', checkout_type='1') + environ: EnvironT = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_CHECKOUT_TYPE'] == '1' + + def test_has_unmerged_paths(in_merge_conflict): assert _has_unmerged_paths() is True cmd_output('git', 'add', '.') diff --git a/tests/repository_test.py b/tests/repository_test.py index b745a9aa..2d36df88 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -871,7 +871,7 @@ def test_manifest_hooks(tempdir_factory, store): require_serial=False, stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'manual', 'push', + 'manual', 'post-checkout', 'push', ), types=['file'], verbose=False,