diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index fea9e306..6da6db25 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -49,7 +49,13 @@ MANIFEST_HOOK_DICT = schema.Map( 'files', schema.check_and(schema.check_string, schema.check_regex), '', ), + schema.Optional( + 'exclude', + schema.check_and(schema.check_string, schema.check_regex), + '^$', + ), schema.Optional('types', schema.check_array(check_type_tag), ['file']), + schema.Optional('exclude_types', schema.check_array(check_type_tag), []), schema.Optional( 'additional_dependencies', schema.check_array(schema.check_string), [], @@ -58,11 +64,6 @@ MANIFEST_HOOK_DICT = schema.Map( schema.Optional('always_run', schema.check_bool, False), schema.Optional('pass_filenames', schema.check_bool, True), schema.Optional('description', schema.check_string, ''), - schema.Optional( - 'exclude', - schema.check_and(schema.check_string, schema.check_regex), - '^$', - ), schema.Optional('language_version', schema.check_string, 'default'), schema.Optional('log_file', schema.check_string, ''), schema.Optional('minimum_pre_commit_version', schema.check_string, '0'), diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index b52ab39d..99d3a189 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -43,12 +43,14 @@ def get_changed_files(new, old): )[1].splitlines() -def filter_filenames_by_types(filenames, types): - types = frozenset(types) - return tuple( - filename for filename in filenames - if tags_from_path(filename) >= types - ) +def filter_filenames_by_types(filenames, types, exclude_types): + types, exclude_types = frozenset(types), frozenset(exclude_types) + ret = [] + for filename in filenames: + tags = tags_from_path(filename) + if tags >= types and not tags & exclude_types: + ret.append(filename) + return tuple(ret) def get_filenames(args, include_expr, exclude_expr): @@ -73,7 +75,9 @@ NO_FILES = '(no files to check)' def _run_single_hook(hook, repo, args, skips, cols): filenames = get_filenames(args, hook['files'], hook['exclude']) - filenames = filter_filenames_by_types(filenames, hook['types']) + filenames = filter_filenames_by_types( + filenames, hook['types'], hook['exclude_types'], + ) if hook['id'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), diff --git a/testing/resources/exclude_types_repo/.pre-commit-hooks.yaml b/testing/resources/exclude_types_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..ed8794fb --- /dev/null +++ b/testing/resources/exclude_types_repo/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: python-files + name: Python files + entry: bin/hook.sh + language: script + types: [python] + exclude_types: [python3] diff --git a/testing/resources/exclude_types_repo/bin/hook.sh b/testing/resources/exclude_types_repo/bin/hook.sh new file mode 100755 index 00000000..bdade513 --- /dev/null +++ b/testing/resources/exclude_types_repo/bin/hook.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo $@ +exit 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 87031419..1643cbb8 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -20,6 +20,7 @@ from pre_commit.commands.run import run from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd +from pre_commit.util import make_executable from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo @@ -43,7 +44,7 @@ def repo_with_failing_hook(tempdir_factory): def stage_a_file(filename='foo.py'): - cmd_output('touch', filename) + open(filename, 'a').close() cmd_output('git', 'add', filename) @@ -166,6 +167,22 @@ def test_types_hook_repository( assert b'bar.notpy' not in printed +def test_exclude_types_hook_repository( + cap_out, tempdir_factory, mock_out_store_directory, +): + git_path = make_consuming_repo(tempdir_factory, 'exclude_types_repo') + with cwd(git_path): + with io.open('exe', 'w') as exe: + exe.write('#!/usr/bin/env python3\n') + make_executable('exe') + cmd_output('git', 'add', 'exe') + stage_a_file('bar.py') + ret, printed = _do_run(cap_out, git_path, _get_opts()) + assert ret == 1 + assert b'bar.py' in printed + assert b'exe' not in printed + + def test_show_diff_on_failure( capfd, cap_out, tempdir_factory, mock_out_store_directory, ): diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 3a31a812..7db886c5 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -35,6 +35,7 @@ def test_manifest_contents(manifest): 'pass_filenames': True, 'stages': [], 'types': ['file'], + 'exclude_types': [], }] @@ -56,6 +57,7 @@ def test_hooks(manifest): 'pass_filenames': True, 'stages': [], 'types': ['file'], + 'exclude_types': [], }