From 91f0344b081a68661ac84936d09325f29d3e8478 Mon Sep 17 00:00:00 2001 From: Konrad Weihmann Date: Fri, 22 Apr 2022 09:59:41 +0200 Subject: [PATCH] hook: enable calling out of tree entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit in the following setup ├── common │   ├── .pre-commit-config.yaml │   ├── script1.sh │   └── script-config ├── repo1 │   └── code_file └── repo2 └── code_file a common configuration from 'common' folder will be installed using an absolute path to repo1 and repo2. As local tools needs to be run (script1.sh with its configuration script-config) we need to pass the path to common somehow into the hook cmdline. Add support for a template variable %CONFIG_BASEPATH% which is automatically expanded to the configuration directory when pre-commit run is called This helps to create a single source of truth setup, while still keeping the possibility to call non public tools and configurations Signed-off-by: Konrad Weihmann --- pre_commit/commands/run.py | 2 +- pre_commit/hook.py | 12 ++++++++++++ tests/commands/run_test.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 37f989b5..49b96c8e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -397,7 +397,7 @@ def run( config = load_config(config_file) hooks = [ - hook + hook.expand(args, config_file) for hook in all_hooks(config, store) if not args.hook or hook.id == args.hook or hook.alias == args.hook if args.hook_stage in hook.stages diff --git a/pre_commit/hook.py b/pre_commit/hook.py index 202abb35..d4cbb421 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -1,6 +1,8 @@ from __future__ import annotations +import argparse import logging +import os import shlex from typing import Any from typing import NamedTuple @@ -61,5 +63,15 @@ class Hook(NamedTuple): ) return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) + def expand(self, args: argparse.Namespace, config_file: str) -> Hook: + map_ = { + '%CONFIG_BASEPATH%': os.path.dirname(os.path.abspath(config_file)), + } + values = self._asdict() + for k, v in map_.items(): + values['entry'] = values['entry'].replace(k, v) + values['args'] = [x.replace(k, v) for x in values['args']] + return Hook(**values) + _KEYS = frozenset(set(Hook._fields) - {'src', 'prefix'}) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 085b063f..dc06f523 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -265,6 +265,39 @@ def test_global_exclude(cap_out, store, in_git_dir): assert printed.endswith(b'\n\n.pre-commit-config.yaml\nbar.py\n\n') +def test_global_hook_expand(cap_out, store, in_git_dir): + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [ + { + 'id': 'foo', + 'language': 'script', + 'name': 'bar', + 'entry': '%CONFIG_BASEPATH%/script', + 'args': ['%CONFIG_BASEPATH%'], + }, + ], + }, + ], + } + write_config('.', config) + + open('foo.py', 'a').close() + cmd_output('git', 'add', '.') + opts = run_opts(verbose=False) + ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) + # script doesn't exist so this needs to fail + assert ret != 0 + + # %CONFIG_BASEPATH% is properly expanded + assert b'%CONFIG_BASEPATH%' not in printed + if sys.platform != 'win32': + # on windows path separator would look different + assert f'{in_git_dir}/script'.encode() in printed + + def test_global_files(cap_out, store, in_git_dir): config = { 'files': r'^bar\.py$',