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 --- .github/FUNDING.yml | 2 - .github/ISSUE_TEMPLATE/bug.yaml | 41 ++++++++++++++++ .pre-commit-config.yaml | 12 ++--- CHANGELOG.md | 30 ++++++++++++ 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 +- requirements-dev.txt | 2 +- setup.cfg | 5 +- testing/gen-languages-all | 2 +- testing/make-archives | 2 +- testing/zipapp/entry | 2 +- testing/zipapp/make | 2 +- testing/zipapp/python | 2 +- tests/clientlib_test.py | 64 +++++++++++++++++-------- tests/commands/autoupdate_test.py | 9 ++++ tests/commands/install_uninstall_test.py | 6 +-- tests/commands/run_test.py | 12 +++++ tests/git_test.py | 10 ++++ tests/repository_test.py | 16 +++---- tests/staged_files_only_test.py | 43 +++++++++++++++-- 34 files changed, 297 insertions(+), 122 deletions(-) delete mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug.yaml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 9408e44..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: asottile -open_collective: pre-commit diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 0000000..6cce5fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,41 @@ +name: bug report +description: something went wrong +body: + - type: markdown + attributes: + value: | + this is for issues for `pre-commit` (the framework). + if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues] + + [pre-commit.ci]: https://pre-commit.ci + [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues + - type: textarea + id: freeform + attributes: + label: describe your issue + placeholder: 'I was doing ... I ran ... I expected ... I got ...' + validations: + required: true + - type: input + id: version + attributes: + label: pre-commit --version + placeholder: pre-commit x.x.x + validations: + required: true + - type: textarea + id: configuration + attributes: + label: .pre-commit-config.yaml + description: (auto-rendered as yaml, no need for backticks) + placeholder: 'repos: ...' + render: yaml + validations: + required: true + - type: textarea + id: error-log + attributes: + label: '~/.cache/pre-commit/pre-commit.log (if present)' + placeholder: "### version information\n..." + validations: + required: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57466c7..49517c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] @@ -21,11 +21,11 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.15.0 + rev: v2.16.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.25.0 + rev: v2.29.1 hooks: - id: pyupgrade args: [--py36-plus] @@ -35,16 +35,16 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.1.0 + rev: v2.2.1 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.17.0 + rev: v1.20.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910 + rev: v0.910-1 hooks: - id: mypy additional_dependencies: [types-all] diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b93256..55f46d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +2.16.0 - 2021-11-30 +=================== + +### Features +- add warning for regexes containing `[\/]` or `[/\\]`. + - #2053 PR by @radek-sprta. + - #2043 issue by @asottile. +- move hook template back to `bash` resolving shebang-portability issues. + - #2065 PR by @asottile. +- add support for `fail_fast` at the individual hook level. + - #2097 PR by @colens3. + - #1143 issue by @potiuk. +- allow passthrough of `GIT_CONFIG_KEY_*`, `GIT_CONFIG_VALUE_*`, and + `GIT_CONFIG_COUNT`. + - #2136 PR by @emzeat. + +### Fixes +- fix pre-commit autoupdate for `core.useBuiltinFSMonitor=true` on windows. + - #2047 PR by @asottile. + - #2046 issue by @lcnittl. +- fix temporary file stashing with for `submodule.recurse=1`. + - #2071 PR by @asottile. + - #2063 issue by @a666. +- ban broken importlib-resources versions. + - #2098 PR by @asottile. +- replace `exit(...)` with `raise SystemExit(...)` for portability. + - #2103 PR by @asottile. + - #2104 PR by @asottile. + + 2.15.0 - 2021-09-02 =================== 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 (=2.1 coverage distlib pytest diff --git a/setup.cfg b/setup.cfg index c0f4f0e..02669c7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.15.0 +version = 2.16.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown @@ -17,6 +17,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy @@ -30,7 +31,7 @@ install_requires = toml virtualenv>=20.0.8 importlib-metadata;python_version<"3.8" - importlib-resources;python_version<"3.7" + importlib-resources<5.3;python_version<"3.7" python_requires = >=3.6.1 [options.packages.find] diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 51e4bce..c933c14 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -25,4 +25,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/make-archives b/testing/make-archives index cb0b0a4..707fd88 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -80,4 +80,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/zipapp/entry b/testing/zipapp/entry index f0a345e..87f9291 100755 --- a/testing/zipapp/entry +++ b/testing/zipapp/entry @@ -68,4 +68,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/zipapp/make b/testing/zipapp/make index 8740b2f..55b6d2c 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -106,4 +106,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/zipapp/python b/testing/zipapp/python index 97c5928..7184a1a 100755 --- a/testing/zipapp/python +++ b/testing/zipapp/python @@ -45,4 +45,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index da794e6..5427b1d 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -247,38 +247,64 @@ def test_warn_mutable_rev_conditional(): cfgv.validate(config_obj, CONFIG_REPO_DICT) -def test_validate_optional_sensible_regex_at_hook_level(caplog): +@pytest.mark.parametrize( + ('regex', 'warning'), + ( + ( + r'dir/*.py', + "The 'files' field in hook 'flake8' is a regex, not a glob -- " + "matching '/*' probably isn't what you want here", + ), + ( + r'dir[\/].*\.py', + r"pre-commit normalizes slashes in the 'files' field in hook " + r"'flake8' to forward slashes, so you can use / instead of [\/]", + ), + ( + r'dir[/\\].*\.py', + r"pre-commit normalizes slashes in the 'files' field in hook " + r"'flake8' to forward slashes, so you can use / instead of [/\\]", + ), + ), +) +def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): config_obj = { 'id': 'flake8', - 'files': 'dir/*.py', + 'files': regex, } cfgv.validate(config_obj, CONFIG_HOOK_DICT) - assert caplog.record_tuples == [ + assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] + + +@pytest.mark.parametrize( + ('regex', 'warning'), + ( ( - 'pre_commit', - logging.WARNING, - "The 'files' field in hook 'flake8' is a regex, not a glob -- " + r'dir/*.py', + "The top-level 'files' field is a regex, not a glob -- " "matching '/*' probably isn't what you want here", ), - ] - - -def test_validate_optional_sensible_regex_at_top_level(caplog): + ( + r'dir[\/].*\.py', + r"pre-commit normalizes the slashes in the top-level 'files' " + r'field to forward slashes, so you can use / instead of [\/]', + ), + ( + r'dir[/\\].*\.py', + r"pre-commit normalizes the slashes in the top-level 'files' " + r'field to forward slashes, so you can use / instead of [/\\]', + ), + ), +) +def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): config_obj = { - 'files': 'dir/*.py', + 'files': regex, 'repos': [], } cfgv.validate(config_obj, CONFIG_SCHEMA) - assert caplog.record_tuples == [ - ( - 'pre_commit', - logging.WARNING, - "The top-level 'files' field is a regex, not a glob -- matching " - "'/*' probably isn't what you want here", - ), - ] + assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] @pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index b2bad60..7316eb9 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -5,6 +5,7 @@ import pytest import yaml import pre_commit.constants as C +from pre_commit import envcontext from pre_commit import git from pre_commit import util from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev @@ -176,6 +177,14 @@ def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) +def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir, store): + # force the setting on "globally" for git + home = tmpdir.join('fakehome').ensure_dir() + home.join('.gitconfig').write('[core]\nuseBuiltinFSMonitor = true\n') + with envcontext.envcontext((('HOME', str(home)),)): + test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) + + def test_autoupdate_pure_yaml(out_of_date, tmpdir, store): with mock.patch.object(util, 'Dumper', yaml.SafeDumper): test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 3c07124..8339903 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -278,11 +278,7 @@ def test_environment_not_sourced(tempdir_factory, store): hook = os.path.join(path, '.git/hooks/pre-commit') with open(hook) as f: src = f.read() - src = re.sub( - '\nINSTALL_PYTHON =.*\n', - '\nINSTALL_PYTHON = "/dne"\n', - src, - ) + src = re.sub('\nINSTALL_PYTHON=.*\n', '\nINSTALL_PYTHON="/dne"\n', src) with open(hook, 'w') as f: f.write(src) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 8c15395..3a6fa2a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -985,6 +985,18 @@ def test_fail_fast(cap_out, store, repo_with_failing_hook): assert printed.count(b'Failing hook') == 1 +def test_fail_fast_per_hook(cap_out, store, repo_with_failing_hook): + with modify_config() as config: + # More than one hook + config['repos'][0]['hooks'] *= 2 + config['repos'][0]['hooks'][0]['fail_fast'] = True + stage_a_file() + + ret, printed = _do_run(cap_out, store, repo_with_failing_hook, run_opts()) + # it should have only run one hook + assert printed.count(b'Failing hook') == 1 + + def test_classifier_removes_dne(): classifier = Classifier(('this_file_does_not_exist',)) assert classifier.filenames == [] diff --git a/tests/git_test.py b/tests/git_test.py index aa21880..bcb3fd1 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -227,6 +227,11 @@ def test_no_git_env(): 'GIT_SSH': '/usr/bin/ssh', 'GIT_SSH_COMMAND': 'ssh -o', 'GIT_DIR': '/none/shall/pass', + 'GIT_CONFIG_KEY_0': 'user.name', + 'GIT_CONFIG_VALUE_0': 'anthony', + 'GIT_CONFIG_KEY_1': 'user.email', + 'GIT_CONFIG_VALUE_1': 'asottile@example.com', + 'GIT_CONFIG_COUNT': '2', } no_git_env = git.no_git_env(env) assert no_git_env == { @@ -234,6 +239,11 @@ def test_no_git_env(): 'GIT_EXEC_PATH': '/some/git/exec/path', 'GIT_SSH': '/usr/bin/ssh', 'GIT_SSH_COMMAND': 'ssh -o', + 'GIT_CONFIG_KEY_0': 'user.name', + 'GIT_CONFIG_VALUE_0': 'anthony', + 'GIT_CONFIG_KEY_1': 'user.email', + 'GIT_CONFIG_VALUE_1': 'asottile@example.com', + 'GIT_CONFIG_COUNT': '2', } diff --git a/tests/repository_test.py b/tests/repository_test.py index 4121fed..36268e1 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -111,8 +111,8 @@ def test_local_conda_additional_dependencies(store): 'name': 'local-conda', 'entry': 'python', 'language': 'conda', - 'args': ['-c', 'import tzdata; print("OK")'], - 'additional_dependencies': ['python-tzdata'], + 'args': ['-c', 'import botocore; print("OK")'], + 'additional_dependencies': ['botocore'], }], } hook = _get_hook(config, store, 'local-conda') @@ -164,7 +164,7 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) +def test_python_venv(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_venv_hooks_repo', 'foo', [os.devnull], @@ -245,7 +245,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', @@ -253,7 +252,6 @@ def test_run_a_node_hook(tempdir_factory, store): ) -@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where node is not # installed at the system @@ -263,7 +261,6 @@ def test_run_a_node_hook_default_version(tempdir_factory, store): test_run_a_node_hook(tempdir_factory, store) -@xfailif_windows # pragma: win32 no cover def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', @@ -271,7 +268,6 @@ def test_run_versioned_node_hook(tempdir_factory, store): ) -@xfailif_windows # pragma: win32 no cover def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): cfg = tmpdir.join('cfg') cfg.write('cache=/dne\n') @@ -653,7 +649,6 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'tins' in output -@xfailif_windows # pragma: win32 no cover def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) @@ -1007,6 +1002,7 @@ def test_manifest_hooks(tempdir_factory, store): types=['file'], types_or=[], verbose=False, + fail_fast=False, ) @@ -1025,13 +1021,13 @@ def test_local_perl_additional_dependencies(store): 'name': 'hello', 'entry': 'perltidy --version', 'language': 'perl', - 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20200110.tar.gz'], + 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20211029.tar.gz'], }], } hook = _get_hook(config, store, 'hello') ret, out = _hook_run(hook, (), color=False) assert ret == 0 - assert _norm_out(out).startswith(b'This is perltidy, v20200110') + assert _norm_out(out).startswith(b'This is perltidy, v20211029') @pytest.mark.parametrize( diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index ddb9574..2e3f620 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -181,9 +181,11 @@ def test_img_conflict(img_staged, patch_dir): @pytest.fixture -def submodule_with_commits(tempdir_factory): +def repo_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): + open('foo', 'a+').close() + cmd_output('git', 'add', 'foo') git_commit() rev1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() git_commit() @@ -196,18 +198,21 @@ def checkout_submodule(rev): @pytest.fixture -def sub_staged(submodule_with_commits, tempdir_factory): +def sub_staged(repo_with_commits, tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): + open('bar', 'a+').close() + cmd_output('git', 'add', 'bar') + git_commit() cmd_output( - 'git', 'submodule', 'add', submodule_with_commits.path, 'sub', + 'git', 'submodule', 'add', repo_with_commits.path, 'sub', ) - checkout_submodule(submodule_with_commits.rev1) + checkout_submodule(repo_with_commits.rev1) cmd_output('git', 'add', 'sub') yield auto_namedtuple( path=path, sub_path=os.path.join(path, 'sub'), - submodule=submodule_with_commits, + submodule=repo_with_commits, ) @@ -242,6 +247,34 @@ def test_sub_something_unstaged(sub_staged, patch_dir): _test_sub_state(sub_staged, 'rev2', 'AM') +def test_submodule_does_not_discard_changes(sub_staged, patch_dir): + with open('bar', 'w') as f: + f.write('unstaged changes') + + foo_path = os.path.join(sub_staged.sub_path, 'foo') + with open(foo_path, 'w') as f: + f.write('foo contents') + + with staged_files_only(patch_dir): + with open('bar') as f: + assert f.read() == '' + + with open(foo_path) as f: + assert f.read() == 'foo contents' + + with open('bar') as f: + assert f.read() == 'unstaged changes' + + with open(foo_path) as f: + assert f.read() == 'foo contents' + + +def test_submodule_does_not_discard_changes_recurse(sub_staged, patch_dir): + cmd_output('git', 'config', 'submodule.recurse', '1', cwd=sub_staged.path) + + test_submodule_does_not_discard_changes(sub_staged, patch_dir) + + def test_stage_utf8_changes(foo_staged, patch_dir): contents = '\u2603' with open('foo', 'w', encoding='UTF-8') as foo_file: -- cgit v1.2.3