From fc1651ef0b7ccedc604b28d5893f5cd486cd4091 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 Dec 2021 04:57:54 +0100 Subject: Merging upstream version 2.16.0. Signed-off-by: Daniel Baumann --- pre_commit/__main__.py | 2 +- pre_commit/clientlib.py | 25 +++++++++++++ pre_commit/commands/autoupdate.py | 22 ++++++++--- pre_commit/commands/install_uninstall.py | 13 +++---- pre_commit/commands/run.py | 2 +- pre_commit/constants.py | 6 +-- pre_commit/git.py | 12 ++++-- pre_commit/hook.py | 1 + pre_commit/languages/pygrep.py | 2 +- pre_commit/main.py | 2 +- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- pre_commit/meta_hooks/check_useless_excludes.py | 2 +- pre_commit/meta_hooks/identity.py | 2 +- pre_commit/resources/hook-tmpl | 50 +++++++------------------ pre_commit/staged_files_only.py | 10 ++++- pre_commit/util.py | 4 +- 16 files changed, 90 insertions(+), 67 deletions(-) (limited to 'pre_commit') diff --git a/pre_commit/__main__.py b/pre_commit/__main__.py index 5414068..83bd93c 100644 --- a/pre_commit/__main__.py +++ b/pre_commit/__main__.py @@ -2,4 +2,4 @@ from pre_commit.main import main if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index bc7274a..6377a8b 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -70,6 +70,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( ), cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []), cfgv.Optional('always_run', cfgv.check_bool, False), + cfgv.Optional('fail_fast', cfgv.check_bool, False), cfgv.Optional('pass_filenames', cfgv.check_bool, True), cfgv.Optional('description', cfgv.check_string, ''), cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT), @@ -143,6 +144,18 @@ class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): f"regex, not a glob -- matching '/*' probably isn't what you " f'want here', ) + if r'[\/]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes slashes in the {self.key!r} field ' + fr'in hook {dct.get("id")!r} to forward slashes, so you ' + fr'can use / instead of [\/]', + ) + if r'[/\\]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes slashes in the {self.key!r} field ' + fr'in hook {dct.get("id")!r} to forward slashes, so you ' + fr'can use / instead of [/\\]', + ) class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): @@ -154,6 +167,18 @@ class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): f'The top-level {self.key!r} field is a regex, not a glob -- ' f"matching '/*' probably isn't what you want here", ) + if r'[\/]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes the slashes in the top-level ' + fr'{self.key!r} field to forward slashes, so you can use / ' + fr'instead of [\/]', + ) + if r'[/\\]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes the slashes in the top-level ' + fr'{self.key!r} field to forward slashes, so you can use / ' + fr'instead of [/\\]', + ) class MigrateShaToRev: diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 33a3473..5cb978e 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -36,24 +36,36 @@ class RevInfo(NamedTuple): return cls(config['repo'], config['rev'], None) def update(self, tags_only: bool, freeze: bool) -> 'RevInfo': + git_cmd = ('git', *git.NO_FS_MONITOR) + if tags_only: - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') + tag_cmd = ( + *git_cmd, 'describe', + 'FETCH_HEAD', '--tags', '--abbrev=0', + ) else: - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact') + tag_cmd = ( + *git_cmd, 'describe', + 'FETCH_HEAD', '--tags', '--exact', + ) with tmpdir() as tmp: git.init_repo(tmp, self.repo) - cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=tmp) + cmd_output_b( + *git_cmd, 'fetch', 'origin', 'HEAD', '--tags', + cwd=tmp, + ) try: rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip() except CalledProcessError: - cmd = ('git', 'rev-parse', 'FETCH_HEAD') + cmd = (*git_cmd, 'rev-parse', 'FETCH_HEAD') rev = cmd_output(*cmd, cwd=tmp)[1].strip() frozen = None if freeze: - exact = cmd_output('git', 'rev-parse', rev, cwd=tmp)[1].strip() + exact_rev_cmd = (*git_cmd, 'rev-parse', rev) + exact = cmd_output(*exact_rev_cmd, cwd=tmp)[1].strip() if exact != rev: rev, frozen = exact, rev return self._replace(rev=rev, frozen=frozen) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 73c8d60..7974423 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -1,6 +1,7 @@ import itertools import logging import os.path +import shlex import shutil import sys from typing import Optional @@ -100,19 +101,17 @@ def _install_hook_script( args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}'] if skip_on_missing_config: args.append('--skip-on-missing-config') - params = {'INSTALL_PYTHON': sys.executable, 'ARGS': args} with open(hook_path, 'w') as hook_file: contents = resource_text('hook-tmpl') before, rest = contents.split(TEMPLATE_START) - to_template, after = rest.split(TEMPLATE_END) - - before = before.replace('#!/usr/bin/env python3', shebang()) + _, after = rest.split(TEMPLATE_END) hook_file.write(before + TEMPLATE_START) - for line in to_template.splitlines(): - var = line.split()[0] - hook_file.write(f'{var} = {params[var]!r}\n') + hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n') + # TODO: python3.8+: shlex.join + args_s = ' '.join(shlex.quote(part) for part in args) + hook_file.write(f'ARGS=({args_s})\n') hook_file.write(TEMPLATE_END + after) make_executable(hook_path) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95ad5e9..2714faf 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -290,7 +290,7 @@ def _run_hooks( verbose=args.verbose, use_color=args.color, ) retval |= current_retval - if retval and config['fail_fast']: + if retval and (config['fail_fast'] or hook.fail_fast): break if retval and args.show_diff_on_failure and prior_diff: if args.all_files: diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 1a69c90..d2f9363 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,9 +1,9 @@ import sys -if sys.version_info < (3, 8): # pragma: no cover (= (3, 8): # pragma: >=3.8 cover import importlib.metadata as importlib_metadata +else: # pragma: <3.8 cover + import importlib_metadata CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' diff --git a/pre_commit/git.py b/pre_commit/git.py index 6264529..e9ec601 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -12,9 +12,11 @@ from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b - logger = logging.getLogger(__name__) +# see #2046 +NO_FS_MONITOR = ('-c', 'core.useBuiltinFSMonitor=false') + def zsplit(s: str) -> List[str]: s = s.strip('\0') @@ -39,9 +41,10 @@ def no_git_env( return { k: v for k, v in _env.items() if not k.startswith('GIT_') or + k.startswith(('GIT_CONFIG_KEY_', 'GIT_CONFIG_VALUE_')) or k in { 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', - 'GIT_SSL_NO_VERIFY', + 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', } } @@ -185,10 +188,11 @@ def init_repo(path: str, remote: str) -> None: if os.path.isdir(remote): remote = os.path.abspath(remote) + git = ('git', *NO_FS_MONITOR) env = no_git_env() # avoid the user's template so that hooks do not recurse - cmd_output_b('git', 'init', '--template=', path, env=env) - cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) + cmd_output_b(*git, 'init', '--template=', path, env=env) + cmd_output_b(*git, 'remote', 'add', 'origin', remote, cwd=path, env=env) def commit(repo: str = '.') -> None: diff --git a/pre_commit/hook.py b/pre_commit/hook.py index ea77394..82e99c5 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -27,6 +27,7 @@ class Hook(NamedTuple): additional_dependencies: Sequence[str] args: Sequence[str] always_run: bool + fail_fast: bool pass_filenames: bool description: str language_version: str diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index c80d679..a713c3f 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -124,4 +124,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/main.py b/pre_commit/main.py index 2b50c91..f1e8d03 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -411,4 +411,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index a1e9352..a6eb0e0 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -39,4 +39,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 6116597..60870f8 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -77,4 +77,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index 730d0ec..12eb02f 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -13,4 +13,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 299144e..53d29f9 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,44 +1,20 @@ -#!/usr/bin/env python3 +#!/usr/bin/env bash # File generated by pre-commit: https://pre-commit.com # ID: 138fd403232d2ddd5efb44317e38bf03 -import os -import sys - -# we try our best, but the shebang of this script is difficult to determine: -# - macos doesn't ship with python3 -# - windows executables are almost always `python.exe` -# therefore we continue to support python2 for this small script -if sys.version_info < (3, 3): - from distutils.spawn import find_executable as which -else: - from shutil import which - -# work around https://github.com/Homebrew/homebrew-core/issues/30445 -os.environ.pop('__PYVENV_LAUNCHER__', None) # start templated -INSTALL_PYTHON = '' -ARGS = ['hook-impl'] +INSTALL_PYTHON='' +ARGS=(hook-impl) # end templated -ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__)))) -ARGS.append('--') -ARGS.extend(sys.argv[1:]) - -DNE = '`pre-commit` not found. Did you forget to activate your virtualenv?' -if os.access(INSTALL_PYTHON, os.X_OK): - CMD = [INSTALL_PYTHON, '-mpre_commit'] -elif which('pre-commit'): - CMD = ['pre-commit'] -else: - raise SystemExit(DNE) -CMD.extend(ARGS) -if sys.platform == 'win32': # https://bugs.python.org/issue19124 - import subprocess +HERE="$(cd "$(dirname "$0")" && pwd)" +ARGS+=(--hook-dir "$HERE" -- "$@") - if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - raise SystemExit(subprocess.Popen(CMD).wait()) - else: - raise SystemExit(subprocess.call(CMD)) -else: - os.execvp(CMD[0], CMD) +if [ -x "$INSTALL_PYTHON" ]; then + exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}" +elif command -v pre-commit > /dev/null; then + exec pre-commit "${ARGS[@]}" +else + echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 + exit 1 +fi diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 48cc102..bad004c 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -13,6 +13,12 @@ from pre_commit.xargs import xargs logger = logging.getLogger('pre_commit') +# without forcing submodule.recurse=0, changes in nested submodules will be +# discarded if `submodule.recurse=1` is configured +# we choose this instead of `--no-recurse-submodules` because it works on +# versions of git before that option was added to `git checkout` +_CHECKOUT_CMD = ('git', '-c', 'submodule.recurse=0', 'checkout', '--', '.') + def _git_apply(patch: str) -> None: args = ('apply', '--whitespace=nowarn', patch) @@ -58,7 +64,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # prevent recursive post-checkout hooks (#1418) no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') - cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) + cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) try: yield @@ -74,7 +80,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) + cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) _git_apply(patch_filename) logger.info(f'Restored changes from {patch_filename}.') diff --git a/pre_commit/util.py b/pre_commit/util.py index 6bf8ae7..6977acb 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -21,10 +21,10 @@ import yaml from pre_commit import parse_shebang -if sys.version_info >= (3, 7): # pragma: no cover (PY37+) +if sys.version_info >= (3, 7): # pragma: >=3.7 cover from importlib.resources import open_binary from importlib.resources import read_text -else: # pragma: no cover (