From 8df11ee7aaa53c6055d5b22bdd8ef82afb5be6d7 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Mon, 23 Oct 2017 14:29:08 +0200 Subject: [PATCH] Implement check-useless-excludes meta hook --- pre_commit/meta_hooks/__init__.py | 0 .../meta_hooks/check_useless_excludes.py | 41 +++++++++++ pre_commit/repository.py | 14 ++-- tests/commands/run_test.py | 71 ++++++++++++++++++- 4 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 pre_commit/meta_hooks/__init__.py create mode 100644 pre_commit/meta_hooks/check_useless_excludes.py diff --git a/pre_commit/meta_hooks/__init__.py b/pre_commit/meta_hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py new file mode 100644 index 00000000..8e891bc1 --- /dev/null +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -0,0 +1,41 @@ +import re +import sys + +import pre_commit.constants as C +from pre_commit.clientlib import load_config +from pre_commit.git import get_all_files + + +def exclude_matches_any(filenames, include, exclude): + include_re, exclude_re = re.compile(include), re.compile(exclude) + for filename in filenames: + if include_re.search(filename) and exclude_re.search(filename): + return True + return False + + +def check_useless_excludes(config_file=None): + config = load_config(config_file or C.CONFIG_FILE) + files = get_all_files() + useless_excludes = False + + exclude = config.get('exclude') + if exclude != '^$' and not exclude_matches_any(files, '', exclude): + print('The global exclude pattern does not match any files') + useless_excludes = True + + for repo in config['repos']: + for hook in repo['hooks']: + include, exclude = hook.get('files', ''), hook.get('exclude') + if exclude and not exclude_matches_any(files, include, exclude): + print( + 'The exclude pattern for {} does not match any files' + .format(hook['id']) + ) + useless_excludes = True + + return useless_excludes + + +if __name__ == '__main__': + sys.exit(check_useless_excludes()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index b0858ba9..cb53fc85 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -4,6 +4,7 @@ import io import json import logging import os +import pipes import shutil import sys from collections import defaultdict @@ -247,13 +248,16 @@ class LocalRepository(Repository): class MetaRepository(LocalRepository): + # Note: the hook `entry` is passed through `shlex.split()` by the command + # runner, so to prevent issues with spaces and backslashes (on Windows) it + # must be quoted here. meta_hooks = { - 'test-hook': { - 'name': 'Test Hook', - 'files': '', + 'check-useless-excludes': { + 'name': 'Check for useless excludes', + 'files': '.pre-commit-config.yaml', 'language': 'system', - 'entry': 'echo "Hello World!"', - 'always_run': True, + 'entry': pipes.quote(sys.executable), + 'args': ['-m', 'pre_commit.meta_hooks.check_useless_excludes'], }, } diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e52716fa..27fa9eea 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -653,7 +653,7 @@ def test_meta_hook_passes( ( 'hooks', ( OrderedDict(( - ('id', 'test-hook'), + ('id', 'check-useless-excludes'), )), ), ), @@ -663,13 +663,78 @@ def test_meta_hook_passes( _test_run( cap_out, repo_with_passing_hook, - opts={'verbose': True}, - expected_outputs=[b'Hello World!'], + opts={}, + expected_outputs=[b'Check for useless excludes'], expected_ret=0, stage=False, ) +def test_useless_exclude_global( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('exclude', 'foo'), + ( + 'repos', [ + OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + )), + ), + ), + )), + ], + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={'all_files': True}, + expected_outputs=[ + b'Check for useless excludes', + b'The global exclude pattern does not match any files', + ], + expected_ret=1, + stage=False, + ) + + +def test_useless_exclude_for_hook( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude', 'foo'), + )), + ), + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={'all_files': True}, + expected_outputs=[ + b'Check for useless excludes', + b'The exclude pattern for check-useless-excludes ' + b'does not match any files', + ], + expected_ret=1, + stage=False, + ) + + @pytest.yield_fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: