diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 2fa7b153..44599ea6 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -36,6 +36,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), cfgv.Required('language', cfgv.check_one_of(all_languages)), + cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional( 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f2ff7b38..d9280460 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -77,7 +77,7 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): 'replacement.'.format(hook['id'], repo.repo_config['repo']), ) - if hook['id'] in skips: + if hook['id'] in skips or hook['alias'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), end_msg=SKIPPED, @@ -257,8 +257,15 @@ def run(config_file, store, args, environ=os.environ): for repo in repositories(config, store): for _, hook in repo.hooks: if ( - (not args.hook or hook['id'] == args.hook) and - (not hook['stages'] or args.hook_stage in hook['stages']) + ( + not args.hook or + hook['id'] == args.hook or + hook['alias'] == args.hook + ) and + ( + not hook['stages'] or + args.hook_stage in hook['stages'] + ) ): repo_hooks.append((repo, hook)) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index bb233f28..bc891c0c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -42,6 +42,18 @@ def repo_with_failing_hook(tempdir_factory): yield git_path +@pytest.fixture +def aliased_repo(tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(git_path): + with modify_config() as config: + config['repos'][0]['hooks'].append( + {'id': 'bash_hook', 'alias': 'foo_bash'}, + ) + stage_a_file() + yield git_path + + def stage_a_file(filename='foo.py'): open(filename, 'a').close() cmd_output('git', 'add', filename) @@ -388,6 +400,18 @@ def test_skip_hook(cap_out, store, repo_with_passing_hook): assert msg in printed +def test_skip_aliased_hook(cap_out, store, aliased_repo): + ret, printed = _do_run( + cap_out, store, aliased_repo, + run_opts(hook='foo_bash'), + {'SKIP': 'foo_bash'}, + ) + assert ret == 0 + # Only the aliased hook runs and is skipped + for msg in (b'Bash hook', b'Skipped'): + assert printed.count(msg) == 1 + + def test_hook_id_not_in_non_verbose_output( cap_out, store, repo_with_passing_hook, ): @@ -416,6 +440,24 @@ def test_multiple_hooks_same_id(cap_out, store, repo_with_passing_hook): assert output.count(b'Bash hook') == 2 +def test_aliased_hook_run(cap_out, store, aliased_repo): + ret, output = _do_run( + cap_out, store, aliased_repo, + run_opts(verbose=True, hook='bash_hook'), + ) + assert ret == 0 + # Both hooks will run since they share the same ID + assert output.count(b'Bash hook') == 2 + + ret, output = _do_run( + cap_out, store, aliased_repo, + run_opts(verbose=True, hook='foo_bash'), + ) + assert ret == 0 + # Only the aliased hook runs + assert output.count(b'Bash hook') == 1 + + def test_non_ascii_hook_id(repo_with_passing_hook, tempdir_factory): with cwd(repo_with_passing_hook): _, stdout, _ = cmd_output_mocked_pre_commit_home( diff --git a/tests/repository_test.py b/tests/repository_test.py index f1b0f6e0..4d851f59 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -831,6 +831,7 @@ def test_manifest_hooks(tempdir_factory, store): 'exclude': '^$', 'files': '', 'id': 'bash_hook', + 'alias': '', 'language': 'script', 'language_version': 'default', 'log_file': '',