diff options
38 files changed, 396 insertions, 59 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c06de5..57466c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,11 +21,11 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.14.0 + rev: v2.15.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.23.1 + rev: v2.25.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/CHANGELOG.md b/CHANGELOG.md index eaeaa27..6b93256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +2.15.0 - 2021-09-02 +=================== + +### Features +- add support for hooks written in `dart`. + - #2027 PR by @asottile. +- add support for `post-rewrite` hooks. + - #2036 PR by @uSpike. + - #2035 issue by @uSpike. + +### Fixes +- fix `check-useless-excludes` with exclude matching broken symlink. + - #2029 PR by @asottile. + - #2019 issue by @pkoch. +- eliminate duplicate mutable sha warning messages for `pre-commit autoupdate`. + - #2030 PR by @asottile. + - #2010 issue by @graingert. + +2.14.1 - 2021-08-28 +=================== + +### Fixes +- fix force-push of disparate histories using git>=2.28. + - #2005 PR by @asottile. + - #2002 issue by @bogusfocused. +- fix `check-useless-excludes` and `check-hooks-apply` matching non-root + `.pre-commit-config.yaml`. + - #2026 PR by @asottile. + - pre-commit-ci/issues#84 issue by @billsioros. + 2.14.0 - 2021-08-06 =================== diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 58dee74..a468e8a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -26,9 +26,9 @@ jobs: Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin" Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin" displayName: Add strawberry perl to PATH - - task: PowerShell@2 - inputs: - filePath: "testing/get-r.ps1" + - bash: testing/get-dart.sh + displayName: install dart + - powershell: testing/get-r.ps1 displayName: install R - template: job--python-tox.yml@asottile parameters: @@ -38,13 +38,11 @@ jobs: pre_test: - task: UseRubyVersion@0 - template: step--git-install.yml - - bash: | - testing/get-coursier.sh - echo '##vso[task.prependpath]/tmp/coursier' + - bash: testing/get-coursier.sh displayName: install coursier - - bash: | - testing/get-swift.sh - echo '##vso[task.prependpath]/tmp/swift/usr/bin' + - bash: testing/get-dart.sh + displayName: install dart + - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh displayName: install R @@ -54,13 +52,11 @@ jobs: os: linux pre_test: - task: UseRubyVersion@0 - - bash: | - testing/get-coursier.sh - echo '##vso[task.prependpath]/tmp/coursier' + - bash: testing/get-coursier.sh displayName: install coursier - - bash: | - testing/get-swift.sh - echo '##vso[task.prependpath]/tmp/swift/usr/bin' + - bash: testing/get-dart.sh + displayName: install dart + - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh displayName: install R diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 962c7fa..bc7274a 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -216,14 +216,14 @@ _meta = ( ( 'check-hooks-apply', ( ('name', 'Check hooks apply to the repository'), - ('files', C.CONFIG_FILE), + ('files', f'^{re.escape(C.CONFIG_FILE)}$'), ('entry', _entry('check_hooks_apply')), ), ), ( 'check-useless-excludes', ( ('name', 'Check for useless excludes'), - ('files', C.CONFIG_FILE), + ('files', f'^{re.escape(C.CONFIG_FILE)}$'), ('entry', _entry('check_useless_excludes')), ), ), diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index c544167..90bb33b 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -78,6 +78,7 @@ def _ns( commit_msg_filename: Optional[str] = None, checkout_type: Optional[str] = None, is_squash_merge: Optional[str] = None, + rewrite_command: Optional[str] = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -92,6 +93,7 @@ def _ns( all_files=all_files, checkout_type=checkout_type, is_squash_merge=is_squash_merge, + rewrite_command=rewrite_command, files=(), hook=None, verbose=False, @@ -166,6 +168,7 @@ _EXPECTED_ARG_LENGTH_BY_HOOK = { 'pre-commit': 0, 'pre-merge-commit': 0, 'post-merge': 1, + 'post-rewrite': 1, 'pre-push': 2, } @@ -209,6 +212,8 @@ def _run_ns( ) elif hook_type == 'post-merge': return _ns(hook_type, color, is_squash_merge=args[0]) + elif hook_type == 'post-rewrite': + return _ns(hook_type, color, rewrite_command=args[0]) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index a155f6b..fef14cd 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -3,7 +3,6 @@ import textwrap import yaml -from pre_commit.clientlib import load_config from pre_commit.util import yaml_load @@ -40,9 +39,6 @@ def _migrate_sha_to_rev(contents: str) -> str: def migrate_config(config_file: str, quiet: bool = False) -> int: - # ensure that the configuration is a valid pre-commit configuration - load_config(config_file) - with open(config_file) as f: orig_contents = contents = f.read() diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index d906d5b..95ad5e9 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -245,7 +245,9 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: # these hooks do not operate on files - if args.hook_stage in {'post-checkout', 'post-commit', 'post-merge'}: + if args.hook_stage in { + 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', + }: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) @@ -386,6 +388,9 @@ def run( if args.is_squash_merge: environ['PRE_COMMIT_IS_SQUASH_MERGE'] = args.is_squash_merge + if args.rewrite_command: + environ['PRE_COMMIT_REWRITE_COMMAND'] = args.rewrite_command + # Set pre_commit flag environ['PRE_COMMIT'] = '1' diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3dcbbac..1a69c90 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -19,6 +19,7 @@ VERSION = importlib_metadata.version('pre_commit') STAGES = ( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', + 'post-rewrite', ) DEFAULT = 'default' diff --git a/pre_commit/git.py b/pre_commit/git.py index 4bf2823..6264529 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -155,12 +155,15 @@ def get_all_files() -> List[str]: def get_changed_files(old: str, new: str) -> List[str]: - return zsplit( - cmd_output( - 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - f'{old}...{new}', - )[1], - ) + diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z') + try: + _, out, _ = cmd_output(*diff_cmd, f'{old}...{new}') + except CalledProcessError: # pragma: no cover (new git) + # on newer git where old and new do not have a merge base git fails + # so we try a full diff (this is what old git did for us!) + _, out, _ = cmd_output(*diff_cmd, f'{old}..{new}') + + return zsplit(out) def head_rev(remote: str) -> str: diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index fde6000..d8a364c 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -7,6 +7,7 @@ from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import conda from pre_commit.languages import coursier +from pre_commit.languages import dart from pre_commit.languages import docker from pre_commit.languages import docker_image from pre_commit.languages import dotnet @@ -44,6 +45,7 @@ languages = { # BEGIN GENERATED (testing/gen-languages-all) 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, healthy=coursier.healthy, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 + 'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, healthy=dart.healthy, install_environment=dart.install_environment, run_hook=dart.run_hook), # noqa: E501 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py new file mode 100644 index 0000000..16e7554 --- /dev/null +++ b/pre_commit/languages/dart.py @@ -0,0 +1,109 @@ +import contextlib +import os.path +import shutil +import tempfile +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import win_exe +from pre_commit.util import yaml_load + +ENVIRONMENT_DIR = 'dartenv' + +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env(prefix: Prefix) -> Generator[None, None, None]: + directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('dart', version) + + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + bin_dir = os.path.join(envdir, 'bin') + + def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: + dart_env = {**os.environ, 'PUB_CACHE': pub_cache} + + with open(prefix_p.path('pubspec.yaml')) as f: + pubspec_contents = yaml_load(f) + + helpers.run_setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) + + for executable in pubspec_contents['executables']: + helpers.run_setup_cmd( + prefix_p, + ( + 'dart', 'compile', 'exe', + '--output', os.path.join(bin_dir, win_exe(executable)), + prefix_p.path('bin', f'{executable}.dart'), + ), + env=dart_env, + ) + + with clean_path_on_failure(envdir): + os.makedirs(bin_dir) + + with tempfile.TemporaryDirectory() as tmp: + _install_dir(prefix, tmp) + + for dep_s in additional_dependencies: + with tempfile.TemporaryDirectory() as dep_tmp: + dep, _, version = dep_s.partition(':') + if version: + dep_cmd: Tuple[str, ...] = (dep, '--version', version) + else: + dep_cmd = (dep,) + + helpers.run_setup_cmd( + prefix, + ('dart', 'pub', 'cache', 'add', *dep_cmd), + env={**os.environ, 'PUB_CACHE': dep_tmp}, + ) + + # try and find the 'pubspec.yaml' that just got added + for root, _, filenames in os.walk(dep_tmp): + if 'pubspec.yaml' in filenames: + with tempfile.TemporaryDirectory() as copied: + pkg = os.path.join(copied, 'pkg') + shutil.copytree(root, pkg) + _install_dir(Prefix(pkg), dep_tmp) + break + else: + raise AssertionError( + f'could not find pubspec.yaml for {dep_s}', + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 29138fd..276ce16 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -48,8 +48,8 @@ def exe_exists(exe: str) -> bool: ) -def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: - cmd_output_b(*cmd, cwd=prefix.prefix_dir) +def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...], **kwargs: Any) -> None: + cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) @overload diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 43b7280..faa6029 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -21,6 +21,7 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +from pre_commit.util import win_exe ENVIRONMENT_DIR = 'py_env' @@ -172,7 +173,7 @@ def healthy(prefix: Prefix, language_version: str) -> bool: if not os.path.exists(pyvenv_cfg): return False - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + exe_name = win_exe('python') py_exe = prefix.path(bin_dir(envdir), exe_name) cfg = _read_pyvenv_cfg(pyvenv_cfg) diff --git a/pre_commit/main.py b/pre_commit/main.py index ad3d873..2b50c91 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -69,6 +69,7 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: '-t', '--hook-type', choices=( 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', + 'post-rewrite', ), action=AppendReplaceDefault, default=['pre-commit'], @@ -146,6 +147,13 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: 'squash merge' ), ) + parser.add_argument( + '--rewrite-command', + help=( + 'During a post-rewrite hook, specifies the command that invoked ' + 'the rewrite' + ), + ) def _adjust_args_and_chdir(args: argparse.Namespace) -> None: diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 12be03f..6116597 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -43,6 +43,9 @@ def check_useless_excludes(config_file: str) -> int: for repo in config['repos']: for hook in repo['hooks']: + # the default of manifest hooks is `types: [file]` but we may + # be configuring a symlink hook while there's a broken symlink + hook.setdefault('types', []) # Not actually a manifest dict, but this more accurately reflects # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) diff --git a/pre_commit/resources/empty_template_pubspec.yaml b/pre_commit/resources/empty_template_pubspec.yaml new file mode 100644 index 0000000..3be6ffe --- /dev/null +++ b/pre_commit/resources/empty_template_pubspec.yaml @@ -0,0 +1,4 @@ +name: pre_commit_empty_pubspec +environment: + sdk: '>=2.10.0' +executables: {} diff --git a/pre_commit/store.py b/pre_commit/store.py index 0fd5e62..fc3bc51 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -189,7 +189,7 @@ class Store: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', 'package.json', 'pre_commit_placeholder_package.gemspec', 'setup.py', - 'environment.yml', 'Makefile.PL', + 'environment.yml', 'Makefile.PL', 'pubspec.yaml', 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', ) diff --git a/pre_commit/util.py b/pre_commit/util.py index b5f40ad..6bf8ae7 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -268,3 +268,7 @@ def rmtree(path: str) -> None: def parse_version(s: str) -> Tuple[int, ...]: """poor man's version comparison""" return tuple(int(p) for p in s.split('.')) + + +def win_exe(s: str) -> str: + return s if sys.platform != 'win32' else f'{s}.exe' @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.14.0 +version = 2.15.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown diff --git a/testing/gen-languages-all b/testing/gen-languages-all index eb7cd70..51e4bce 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,9 +2,9 @@ import sys LANGUAGES = [ - 'conda', 'coursier', 'docker', 'docker_image', 'dotnet', 'fail', 'golang', - 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script', - 'swift', 'system', + 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', + 'golang', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', + 'script', 'swift', 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh index 760c6c1..4c5e955 100755 --- a/testing/get-coursier.sh +++ b/testing/get-coursier.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a script used in CI to install coursier -set -euxo pipefail +set -euo pipefail COURSIER_URL="https://github.com/coursier/coursier/releases/download/v2.0.0/cs-x86_64-pc-linux" COURSIER_HASH="e2e838b75bc71b16bcb77ce951ad65660c89bda7957c79a0628ec7146d35122f" @@ -11,3 +11,5 @@ rm -f "$ARTIFACT" curl --location --silent --output "$ARTIFACT" "$COURSIER_URL" echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check chmod ugo+x /tmp/coursier/cs + +echo '##vso[task.prependpath]/tmp/coursier' diff --git a/testing/get-dart.sh b/testing/get-dart.sh new file mode 100755 index 0000000..b655e1a --- /dev/null +++ b/testing/get-dart.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION=2.13.4 + +if [ "$OSTYPE" = msys ]; then + URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" + echo "##vso[task.prependpath]$(cygpath -w /tmp/dart-sdk/bin)" +else + URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-linux-x64-release.zip" + echo '##vso[task.prependpath]/tmp/dart-sdk/bin' +fi + +curl --silent --location --output /tmp/dart.zip "$URL" + +unzip -q -d /tmp /tmp/dart.zip +rm /tmp/dart.zip diff --git a/testing/get-swift.sh b/testing/get-swift.sh index e205d44..a05b7b9 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a script used in CI to install swift -set -euxo pipefail +set -euo pipefail . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "bionic" ]; then @@ -25,3 +25,5 @@ fi mkdir -p /tmp/swift tar -xf "$TGZ" --strip 1 --directory /tmp/swift + +echo '##vso[task.prependpath]/tmp/swift/usr/bin' diff --git a/testing/resources/dart_repo/.pre-commit-hooks.yaml b/testing/resources/dart_repo/.pre-commit-hooks.yaml new file mode 100644 index 0000000..e0dc5a2 --- /dev/null +++ b/testing/resources/dart_repo/.pre-commit-hooks.yaml @@ -0,0 +1,4 @@ +- id: hello-world-dart + name: hello world dart + entry: hello-world-dart + language: dart diff --git a/testing/resources/dart_repo/bin/hello-world-dart.dart b/testing/resources/dart_repo/bin/hello-world-dart.dart new file mode 100644 index 0000000..5d8d6a6 --- /dev/null +++ b/testing/resources/dart_repo/bin/hello-world-dart.dart @@ -0,0 +1,6 @@ +import 'package:ansicolor/ansicolor.dart'; + +void main() { + AnsiPen pen = new AnsiPen()..red(); + print("hello hello " + pen("world")); +} diff --git a/testing/resources/dart_repo/pubspec.yaml b/testing/resources/dart_repo/pubspec.yaml new file mode 100644 index 0000000..bc719d0 --- /dev/null +++ b/testing/resources/dart_repo/pubspec.yaml @@ -0,0 +1,10 @@ +environment: + sdk: '>=2.10.0 <3.0.0' + +name: hello_world_dart + +executables: + hello-world-dart: + +dependencies: + ansicolor: ^2.0.1 diff --git a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml index d005a74..0f514c1 100644 --- a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml +++ b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml @@ -1,4 +1,4 @@ -- id: dotnet example hook +- id: dotnet-example-hook name: dotnet example hook entry: testeroni language: dotnet diff --git a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml index d005a74..0f514c1 100644 --- a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml +++ b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml @@ -1,4 +1,4 @@ -- id: dotnet example hook +- id: dotnet-example-hook name: dotnet example hook entry: testeroni language: dotnet diff --git a/testing/util.py b/testing/util.py index 12f22b5..791a2b9 100644 --- a/testing/util.py +++ b/testing/util.py @@ -72,6 +72,7 @@ def run_opts( commit_msg_filename='', checkout_type='', is_squash_merge='', + rewrite_command='', ): # These are mutually exclusive assert not (all_files and files) @@ -92,6 +93,7 @@ def run_opts( commit_msg_filename=commit_msg_filename, checkout_type=checkout_type, is_squash_merge=is_squash_merge, + rewrite_command=rewrite_command, ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 09bdb3e..da794e6 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,4 +1,5 @@ import logging +import re import cfgv import pytest @@ -10,6 +11,7 @@ from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION from pre_commit.clientlib import MANIFEST_SCHEMA +from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main @@ -392,6 +394,15 @@ def test_meta_hook_invalid(config_repo): cfgv.validate(config_repo, CONFIG_REPO_DICT) +def test_meta_check_hooks_apply_only_at_top_level(): + cfg = {'id': 'check-hooks-apply'} + cfg = cfgv.apply_defaults(cfg, META_HOOK_DICT) + + files_re = re.compile(cfg['files']) + assert files_re.search('.pre-commit-config.yaml') + assert not files_re.search('foo/.pre-commit-config.yaml') + + @pytest.mark.parametrize( 'mapping', ( diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index c38b9ca..37b78bc 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -99,6 +99,7 @@ def test_run_legacy_recursive(tmpdir): ('post-commit', []), ('post-merge', ['1']), ('post-checkout', ['old_head', 'new_head', '1']), + ('post-rewrite', ['amend']), # multiple choices for commit-editmsg ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']), ('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'message']), @@ -166,6 +167,14 @@ def test_run_ns_post_merge(): assert ns.is_squash_merge == '1' +def test_run_ns_post_rewrite(): + ns = hook_impl._run_ns('post-rewrite', True, ('amend',), b'') + assert ns is not None + assert ns.hook_stage == 'post-rewrite' + assert ns.color is True + assert ns.rewrite_command == 'amend' + + def test_run_ns_post_checkout(): ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 314b8b9..3c07124 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -817,6 +817,35 @@ def test_post_merge_integration(tempdir_factory, store): assert os.path.exists('post-merge.tmp') +def test_post_rewrite_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-rewrite', + 'name': 'Post rewrite', + 'entry': 'touch post-rewrite.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-rewrite'], + }], + }, + ] + write_config(path, config) + with cwd(path): + open('init', 'a').close() + cmd_output('git', 'add', '.') + install(C.CONFIG_FILE, store, hook_types=['post-rewrite']) + git_commit() + + assert not os.path.exists('post-rewrite.tmp') + + git_commit('--amend', '-m', 'ammended message') + assert os.path.exists('post-rewrite.tmp') + + def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = [ @@ -948,7 +977,7 @@ def test_pre_merge_commit_integration(tempdir_factory, store): output_pattern = re_assert.Matches( r'^\[INFO\] Initializing environment for .+\n' r'Bash hook\.+Passed\n' - r"Merge made by the 'recursive' strategy.\n" + r"Merge made by the '(ort|recursive)' strategy.\n" r' foo \| 0\n' r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\n' r' create mode 100644 foo\n$', diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index f5c89d0..f5eddd3 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,7 +1,4 @@ -import pytest - import pre_commit.constants as C -from pre_commit.clientlib import InvalidConfigError from pre_commit.commands.migrate_config import migrate_config @@ -130,13 +127,3 @@ def test_migrate_config_sha_to_rev(tmpdir): ' rev: v1.2.0\n' ' hooks: []\n' ) - - -@pytest.mark.parametrize('contents', ('', '\n')) -def test_migrate_config_invalid_configuration(tmpdir, contents): - cfg = tmpdir.join(C.CONFIG_FILE) - cfg.write(contents) - with tmpdir.as_cwd(), pytest.raises(InvalidConfigError): - migrate_config(C.CONFIG_FILE) - # even though the config is invalid, this should be a noop - assert cfg.read() == contents diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index da7569e..8c15395 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -504,6 +504,15 @@ def test_is_squash_merge(cap_out, store, repo_with_passing_hook): assert environ['PRE_COMMIT_IS_SQUASH_MERGE'] == '1' +def test_rewrite_command(cap_out, store, repo_with_passing_hook): + args = run_opts(rewrite_command='amend') + environ: MutableMapping[str, str] = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_REWRITE_COMMAND'] == 'amend' + + def test_checkout_type(cap_out, store, repo_with_passing_hook): args = run_opts(from_ref='', to_ref='', checkout_type='1') environ: MutableMapping[str, str] = {} diff --git a/tests/git_test.py b/tests/git_test.py index 51d5f8c..aa21880 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -139,6 +139,24 @@ def test_get_changed_files(in_git_dir): assert files == [] +def test_get_changed_files_disparate_histories(in_git_dir): + """in modern versions of git, `...` does not fall back to full diff""" + git_commit() + in_git_dir.join('a.txt').ensure() + cmd_output('git', 'add', '.') + git_commit() + cmd_output('git', 'branch', '-m', 'branch1') + + cmd_output('git', 'checkout', '--orphan', 'branch2') + cmd_output('git', 'rm', '-rf', '.') + in_git_dir.join('a.txt').ensure() + in_git_dir.join('b.txt').ensure() + cmd_output('git', 'add', '.') + git_commit() + + assert git.get_changed_files('branch1', 'branch2') == ['b.txt'] + + @pytest.mark.parametrize( ('s', 'expected'), ( diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 90d1036..8324cac 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -9,6 +9,7 @@ from pre_commit.envcontext import envcontext from pre_commit.languages import python from pre_commit.prefix import Prefix from pre_commit.util import make_executable +from pre_commit.util import win_exe def test_read_pyvenv_cfg(tmpdir): @@ -112,7 +113,7 @@ def test_unhealthy_python_goes_missing(python_dir): python.install_environment(prefix, C.DEFAULT, ()) - exe_name = 'python' if sys.platform != 'win32' else 'python.exe' + exe_name = win_exe('python') py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) os.remove(py_exe) @@ -158,7 +159,7 @@ def test_unhealthy_then_replaced(python_dir): python.install_environment(prefix, C.DEFAULT, ()) # simulate an exe which returns an old version - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + exe_name = win_exe('python') py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) os.rename(py_exe, f'{py_exe}.tmp') diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index d261e81..703bd25 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -1,5 +1,10 @@ +from pre_commit import git from pre_commit.meta_hooks import check_useless_excludes +from pre_commit.util import cmd_output from testing.fixtures import add_config_to_repo +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_repo +from testing.util import xfailif_windows def test_useless_exclude_global(capsys, in_git_dir): @@ -113,3 +118,20 @@ def test_valid_exclude(capsys, in_git_dir): out, _ = capsys.readouterr() assert out == '' + + +@xfailif_windows # pragma: win32 no cover +def test_useless_excludes_broken_symlink(capsys, in_git_dir, tempdir_factory): + path = make_repo(tempdir_factory, 'script_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['exclude'] = 'broken-symlink' + add_config_to_repo(in_git_dir.strpath, config) + + in_git_dir.join('broken-symlink').mksymlinkto('DNE') + cmd_output('git', 'add', 'broken-symlink') + git.commit() + + assert check_useless_excludes.main(('.pre-commit-config.yaml',)) == 0 + + out, _ = capsys.readouterr() + assert out == '' diff --git a/tests/repository_test.py b/tests/repository_test.py index af829c2..4121fed 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1002,6 +1002,7 @@ def test_manifest_hooks(tempdir_factory, store): stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', + 'post-rewrite', ), types=['file'], types_or=[], @@ -1043,10 +1044,50 @@ def test_local_perl_additional_dependencies(store): def test_dotnet_hook(tempdir_factory, store, repo): _test_hook_repo( tempdir_factory, store, repo, - 'dotnet example hook', [], b'Hello from dotnet!\n', + 'dotnet-example-hook', [], b'Hello from dotnet!\n', ) +def test_dart_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'dart_repo', + 'hello-world-dart', [], b'hello hello world\n', + ) + + +def test_local_dart_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-dart', + 'name': 'local-dart', + 'entry': 'hello-world-dart', + 'language': 'dart', + 'additional_dependencies': ['hello_world_dart'], + }], + } + hook = _get_hook(config, store, 'local-dart') + ret, out = _hook_run(hook, (), color=False) + assert (ret, _norm_out(out)) == (0, b'hello hello world\n') + + +def test_local_dart_additional_dependencies_versioned(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-dart', + 'name': 'local-dart', + 'entry': 'secure-random -l 4 -b 16', + 'language': 'dart', + 'additional_dependencies': ['encrypt:5.0.0'], + }], + } + hook = _get_hook(config, store, 'local-dart') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + re_assert.Matches('^[a-f0-9]{8}\r?\n$').assert_matches(out.decode()) + + def test_non_installable_hook_error_for_language_version(store, caplog): config = { 'repo': 'local', |