hook: enable calling out of tree entries

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 <kweihmann@outlook.com>
This commit is contained in:
Konrad Weihmann 2022-04-22 09:59:41 +02:00
parent e1ce4c0bf3
commit 91f0344b08
3 changed files with 46 additions and 1 deletions

View file

@ -397,7 +397,7 @@ def run(
config = load_config(config_file) config = load_config(config_file)
hooks = [ hooks = [
hook hook.expand(args, config_file)
for hook in all_hooks(config, store) for hook in all_hooks(config, store)
if not args.hook or hook.id == args.hook or hook.alias == args.hook if not args.hook or hook.id == args.hook or hook.alias == args.hook
if args.hook_stage in hook.stages if args.hook_stage in hook.stages

View file

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import argparse
import logging import logging
import os
import shlex import shlex
from typing import Any from typing import Any
from typing import NamedTuple from typing import NamedTuple
@ -61,5 +63,15 @@ class Hook(NamedTuple):
) )
return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) 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'}) _KEYS = frozenset(set(Hook._fields) - {'src', 'prefix'})

View file

@ -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') 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): def test_global_files(cap_out, store, in_git_dir):
config = { config = {
'files': r'^bar\.py$', 'files': r'^bar\.py$',