1
0
Fork 0

Merging upstream version 2.21.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 21:35:37 +01:00
parent 08d9b01ff9
commit 54d13e9018
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
48 changed files with 534 additions and 210 deletions

View file

@ -12,7 +12,7 @@ body:
- type: input - type: input
id: search id: search
attributes: attributes:
label: search tried in the issue tracker label: search you tried in the issue tracker
placeholder: ... placeholder: ...
validations: validations:
required: true required: true

View file

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0 rev: v4.4.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@ -10,35 +10,35 @@ repos:
- id: name-tests-test - id: name-tests-test
- id: requirements-txt-fixer - id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.20.1 rev: v2.2.0
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder_python_imports - repo: https://github.com/asottile/reorder_python_imports
rev: v3.3.0 rev: v3.9.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/)
args: [--py37-plus, --add-import, 'from __future__ import annotations'] args: [--py37-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/asottile/add-trailing-comma - repo: https://github.com/asottile/add-trailing-comma
rev: v2.2.3 rev: v2.4.0
hooks: hooks:
- id: add-trailing-comma - id: add-trailing-comma
args: [--py36-plus] args: [--py36-plus]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.34.0 rev: v3.3.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]
- repo: https://github.com/pre-commit/mirrors-autopep8 - repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.6.0 rev: v2.0.1
hooks: hooks:
- id: autopep8 - id: autopep8
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 4.0.1 rev: 6.0.0
hooks: hooks:
- id: flake8 - id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.961 rev: v0.991
hooks: hooks:
- id: mypy - id: mypy
additional_dependencies: [types-all] additional_dependencies: [types-all]

View file

@ -1,3 +1,43 @@
2.21.0 - 2022-12-25
===================
### Features
- Require new-enough virtualenv to prevent 3.10 breakage
- #2467 PR by @asottile.
- Respect aliases with `SKIP` for environment install.
- #2480 PR by @kmARC.
- #2478 issue by @kmARC.
- Allow `pre-commit run --files` against unmerged paths.
- #2484 PR by @asottile.
- Also apply regex warnings to `repo: local` hooks.
- #2524 PR by @chrisRedwine.
- #2521 issue by @asottile.
- `rust` is now a "first class" language -- supporting `language_version` and
installation when not present.
- #2534 PR by @Holzhaus.
- `r` now uses more-reliable binary installation.
- #2460 PR by @lorenzwalthert.
- `GIT_ALLOW_PROTOCOL` is now passed through for git operations.
- #2555 PR by @asottile.
- `GIT_ASKPASS` is now passed through for git operations.
- #2564 PR by @mattp-.
- Remove `toml` dependency by using `cargo add` directly.
- #2568 PR by @m-rsha.
- Support `dotnet` hooks which have dotted prefixes.
- #2641 PR by @rkm.
- #2629 issue by @rkm.
### Fixes
- Properly adjust `--commit-msg-filename` if run from a sub directory.
- #2459 PR by @asottile.
- Simplify `--intent-to-add` detection by using `git diff`.
- #2580 PR by @m-rsha.
- Fix `R.exe` selection on windows.
- #2605 PR by @lorenzwalthert.
- #2599 issue by @SInginc.
- Skip default `nuget` source when installing `dotnet` packages.
- #2642 PR by @rkm.
2.20.0 - 2022-07-10 2.20.0 - 2022-07-10
=================== ===================

View file

@ -5,7 +5,6 @@
- The complete test suite depends on having at least the following installed - The complete test suite depends on having at least the following installed
(possibly not a complete list) (possibly not a complete list)
- git (Version 2.24.0 or above is required to run pre-merge-commit tests) - git (Version 2.24.0 or above is required to run pre-merge-commit tests)
- python2 (Required by a test which checks different python versions)
- python3 (Required by a test which checks different python versions) - python3 (Required by a test which checks different python versions)
- tox (or virtualenv) - tox (or virtualenv)
- ruby + gem - ruby + gem
@ -65,9 +64,9 @@ to implement. The current implemented languages are at varying levels:
- 0th class - pre-commit does not require any dependencies for these languages - 0th class - pre-commit does not require any dependencies for these languages
as they're not actually languages (current examples: fail, pygrep) as they're not actually languages (current examples: fail, pygrep)
- 1st class - pre-commit will bootstrap a full interpreter requiring nothing to - 1st class - pre-commit will bootstrap a full interpreter requiring nothing to
be installed globally (current examples: node, ruby) be installed globally (current examples: node, ruby, rust)
- 2nd class - pre-commit requires the user to install the language globally but - 2nd class - pre-commit requires the user to install the language globally but
will install tools in an isolated fashion (current examples: python, go, rust, will install tools in an isolated fashion (current examples: python, go,
swift, docker). swift, docker).
- 3rd class - pre-commit requires the user to install both the tool and the - 3rd class - pre-commit requires the user to install both the tool and the
language globally (current examples: script, system) language globally (current examples: script, system)

View file

@ -17,6 +17,8 @@ jobs:
parameters: parameters:
toxenvs: [py37] toxenvs: [py37]
os: windows os: windows
additional_variables:
TEMP: C:\Temp
pre_test: pre_test:
- task: UseRubyVersion@0 - task: UseRubyVersion@0
- powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts"

View file

@ -298,6 +298,14 @@ CONFIG_HOOK_DICT = cfgv.Map(
OptionalSensibleRegexAtHook('files', cfgv.check_string), OptionalSensibleRegexAtHook('files', cfgv.check_string),
OptionalSensibleRegexAtHook('exclude', cfgv.check_string), OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
) )
LOCAL_HOOK_DICT = cfgv.Map(
'Hook', 'id',
*MANIFEST_HOOK_DICT.items,
OptionalSensibleRegexAtHook('files', cfgv.check_string),
OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
)
CONFIG_REPO_DICT = cfgv.Map( CONFIG_REPO_DICT = cfgv.Map(
'Repository', 'repo', 'Repository', 'repo',
@ -308,7 +316,7 @@ CONFIG_REPO_DICT = cfgv.Map(
'repo', cfgv.NotIn(LOCAL, META), 'repo', cfgv.NotIn(LOCAL, META),
), ),
cfgv.ConditionalRecurse( cfgv.ConditionalRecurse(
'hooks', cfgv.Array(MANIFEST_HOOK_DICT), 'hooks', cfgv.Array(LOCAL_HOOK_DICT),
'repo', LOCAL, 'repo', LOCAL,
), ),
cfgv.ConditionalRecurse( cfgv.ConditionalRecurse(

View file

@ -263,7 +263,7 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]:
def _get_diff() -> bytes: def _get_diff() -> bytes:
_, out, _ = cmd_output_b( _, out, _ = cmd_output_b(
'git', 'diff', '--no-ext-diff', '--ignore-submodules', retcode=None, 'git', 'diff', '--no-ext-diff', '--ignore-submodules', check=False,
) )
return out return out
@ -318,7 +318,7 @@ def _has_unmerged_paths() -> bool:
def _has_unstaged_config(config_file: str) -> bool: def _has_unstaged_config(config_file: str) -> bool:
retcode, _, _ = cmd_output_b( retcode, _, _ = cmd_output_b(
'git', 'diff', '--no-ext-diff', '--exit-code', config_file, 'git', 'diff', '--no-ext-diff', '--exit-code', config_file,
retcode=None, check=False,
) )
# be explicit, other git errors don't mean it has an unstaged config. # be explicit, other git errors don't mean it has an unstaged config.
return retcode == 1 return retcode == 1
@ -333,7 +333,7 @@ def run(
stash = not args.all_files and not args.files stash = not args.all_files and not args.files
# Check if we have unresolved merge conflict files and fail fast. # Check if we have unresolved merge conflict files and fail fast.
if _has_unmerged_paths(): if stash and _has_unmerged_paths():
logger.error('Unmerged files. Resolve before committing.') logger.error('Unmerged files. Resolve before committing.')
return 1 return 1
if bool(args.from_ref) != bool(args.to_ref): if bool(args.from_ref) != bool(args.to_ref):
@ -420,7 +420,11 @@ def run(
return 1 return 1
skips = _get_skips(environ) skips = _get_skips(environ)
to_install = [hook for hook in hooks if hook.id not in skips] to_install = [
hook
for hook in hooks
if hook.id not in skips and hook.alias not in skips
]
install_hook_envs(to_install, store) install_hook_envs(to_install, store)
return _run_hooks(config, hooks, skips, args) return _run_hooks(config, hooks, skips, args)

View file

@ -1,7 +1,3 @@
# TODO: maybe `git ls-remote git://github.com/pre-commit/pre-commit-hooks` to
# determine the latest revision? This adds ~200ms from my tests (and is
# significantly faster than https:// or http://). For now, periodically
# manually updating the revision is fine.
from __future__ import annotations from __future__ import annotations
SAMPLE_CONFIG = '''\ SAMPLE_CONFIG = '''\
# See https://pre-commit.com for more information # See https://pre-commit.com for more information

View file

@ -25,7 +25,7 @@ def _log_and_exit(
error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc)
output.write_line_b(error_msg) output.write_line_b(error_msg)
_, git_version_b, _ = cmd_output_b('git', '--version', retcode=None) _, git_version_b, _ = cmd_output_b('git', '--version', check=False)
git_version = git_version_b.decode(errors='backslashreplace').rstrip() git_version = git_version_b.decode(errors='backslashreplace').rstrip()
storedir = Store().directory storedir = Store().directory

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
import os.path import os.path
import sys import sys
from typing import MutableMapping from typing import Mapping
from pre_commit.errors import FatalError from pre_commit.errors import FatalError
from pre_commit.util import CalledProcessError from pre_commit.util import CalledProcessError
@ -24,9 +24,7 @@ def zsplit(s: str) -> list[str]:
return [] return []
def no_git_env( def no_git_env(_env: Mapping[str, str] | None = None) -> dict[str, str]:
_env: MutableMapping[str, str] | None = None,
) -> dict[str, str]:
# Too many bugs dealing with environment variables and GIT: # Too many bugs dealing with environment variables and GIT:
# https://github.com/pre-commit/pre-commit/issues/300 # https://github.com/pre-commit/pre-commit/issues/300
# In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running
@ -44,6 +42,8 @@ def no_git_env(
'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO',
'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT',
'GIT_HTTP_PROXY_AUTHMETHOD', 'GIT_HTTP_PROXY_AUTHMETHOD',
'GIT_ALLOW_PROTOCOL',
'GIT_ASKPASS',
} }
} }
@ -150,18 +150,10 @@ def get_staged_files(cwd: str | None = None) -> list[str]:
def intent_to_add_files() -> list[str]: def intent_to_add_files() -> list[str]:
_, stdout, _ = cmd_output( _, stdout, _ = cmd_output(
'git', 'status', '--ignore-submodules', '--porcelain', '-z', 'git', 'diff', '--no-ext-diff', '--ignore-submodules',
'--diff-filter=A', '--name-only', '-z',
) )
parts = list(reversed(zsplit(stdout))) return zsplit(stdout)
intent_to_add = []
while parts:
line = parts.pop()
status, filename = line[:3], line[3:]
if status[0] in {'C', 'R'}: # renames / moves have an additional arg
parts.pop()
if status[1] == 'A':
intent_to_add.append(filename)
return intent_to_add
def get_all_files() -> list[str]: def get_all_files() -> list[str]:
@ -187,11 +179,11 @@ def head_rev(remote: str) -> str:
def has_diff(*args: str, repo: str = '.') -> bool: def has_diff(*args: str, repo: str = '.') -> bool:
cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args) cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args)
return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1 return cmd_output_b(*cmd, cwd=repo, check=False)[0] == 1
def has_core_hookpaths_set() -> bool: def has_core_hookpaths_set() -> bool:
_, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', retcode=None) _, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', check=False)
return bool(out.strip()) return bool(out.strip())

View file

@ -2,6 +2,10 @@ from __future__ import annotations
import contextlib import contextlib
import os.path import os.path
import re
import tempfile
import xml.etree.ElementTree
import zipfile
from typing import Generator from typing import Generator
from typing import Sequence from typing import Sequence
@ -35,6 +39,22 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]:
yield yield
@contextlib.contextmanager
def _nuget_config_no_sources() -> Generator[str, None, None]:
with tempfile.TemporaryDirectory() as tmpdir:
nuget_config = os.path.join(tmpdir, 'nuget.config')
with open(nuget_config, 'w') as f:
f.write(
'<?xml version="1.0" encoding="utf-8"?>'
'<configuration>'
' <packageSources>'
' <clear />'
' </packageSources>'
'</configuration>',
)
yield nuget_config
def install_environment( def install_environment(
prefix: Prefix, prefix: Prefix,
version: str, version: str,
@ -57,19 +77,40 @@ def install_environment(
), ),
) )
# Determine tool from the packaged file <tool_name>.<version>.nupkg nupkg_dir = prefix.path(build_dir)
build_outputs = os.listdir(os.path.join(prefix.prefix_dir, build_dir)) nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')]
for output in build_outputs:
tool_name = output.split('.')[0] if not nupkgs:
raise AssertionError('could not find any build outputs to install')
for nupkg in nupkgs:
with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f:
nuspec, = (x for x in f.namelist() if x.endswith('.nuspec'))
with f.open(nuspec) as spec:
tree = xml.etree.ElementTree.parse(spec)
namespace = re.match(r'{.*}', tree.getroot().tag)
if not namespace:
raise AssertionError('could not parse namespace from nuspec')
tool_id_element = tree.find(f'.//{namespace[0]}id')
if tool_id_element is None:
raise AssertionError('expected to find an "id" element')
tool_id = tool_id_element.text
if not tool_id:
raise AssertionError('"id" element missing tool name')
# Install to bin dir # Install to bin dir
with _nuget_config_no_sources() as nuget_config:
helpers.run_setup_cmd( helpers.run_setup_cmd(
prefix, prefix,
( (
'dotnet', 'tool', 'install', 'dotnet', 'tool', 'install',
'--configfile', nuget_config,
'--tool-path', os.path.join(envdir, BIN_DIR), '--tool-path', os.path.join(envdir, BIN_DIR),
'--add-source', build_dir, '--add-source', build_dir,
tool_name, tool_id,
), ),
) )

View file

@ -75,7 +75,7 @@ def in_env(
def health_check(prefix: Prefix, language_version: str) -> str | None: def health_check(prefix: Prefix, language_version: str) -> str | None:
with in_env(prefix, language_version): with in_env(prefix, language_version):
retcode, _, _ = cmd_output_b('node', '--version', retcode=None) retcode, _, _ = cmd_output_b('node', '--version', check=False)
if retcode != 0: # pragma: win32 no cover if retcode != 0: # pragma: win32 no cover
return f'`node --version` returned {retcode}' return f'`node --version` returned {retcode}'
else: else:

View file

@ -15,6 +15,7 @@ from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
from pre_commit.util import win_exe
ENVIRONMENT_DIR = 'renv' ENVIRONMENT_DIR = 'renv'
RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
@ -63,7 +64,7 @@ def _rscript_exec() -> str:
if r_home is None: if r_home is None:
return 'Rscript' return 'Rscript'
else: else:
return os.path.join(r_home, 'bin', 'Rscript') return os.path.join(r_home, 'bin', win_exe('Rscript'))
def _entry_validate(entry: Sequence[str]) -> None: def _entry_validate(entry: Sequence[str]) -> None:
@ -158,7 +159,7 @@ def _inline_r_setup(code: str) -> str:
only be configured via R options once R has started. These are set here. only be configured via R options once R has started. These are set here.
""" """
with_option = f"""\ with_option = f"""\
options(install.packages.compile.from.source = "never") options(install.packages.compile.from.source = "never", pkgType = "binary")
{code} {code}
""" """
return with_option return with_option

View file

@ -1,13 +1,17 @@
from __future__ import annotations from __future__ import annotations
import contextlib import contextlib
import functools
import os.path import os.path
import shutil
import sys
import tempfile
import urllib.request
from typing import Generator from typing import Generator
from typing import Sequence from typing import Sequence
import toml
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import parse_shebang
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
@ -16,40 +20,105 @@ from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
from pre_commit.util import make_executable
from pre_commit.util import win_exe
ENVIRONMENT_DIR = 'rustenv' ENVIRONMENT_DIR = 'rustenv'
get_default_version = helpers.basic_get_default_version
health_check = helpers.basic_health_check health_check = helpers.basic_health_check
def get_env_patch(target_dir: str) -> PatchesT: @functools.lru_cache(maxsize=1)
def get_default_version() -> str:
# If rust is already installed, we can save a bunch of setup time by
# using the installed version.
#
# Just detecting the executable does not suffice, because if rustup is
# installed but no toolchain is available, then `cargo` exists but
# cannot be used without installing a toolchain first.
if cmd_output_b('cargo', '--version', check=False)[0] == 0:
return 'system'
else:
return C.DEFAULT
def _rust_toolchain(language_version: str) -> str:
"""Transform the language version into a rust toolchain version."""
if language_version == C.DEFAULT:
return 'stable'
else:
return language_version
def _envdir(prefix: Prefix, version: str) -> str:
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
return prefix.path(directory)
def get_env_patch(target_dir: str, version: str) -> PatchesT:
return ( return (
('CARGO_HOME', target_dir),
('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))),
# Only set RUSTUP_TOOLCHAIN if we don't want use the system's default
# toolchain
*(
(('RUSTUP_TOOLCHAIN', _rust_toolchain(version)),)
if version != 'system' else ()
),
) )
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix) -> Generator[None, None, None]: def in_env(
target_dir = prefix.path( prefix: Prefix,
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), language_version: str,
) ) -> Generator[None, None, None]:
with envcontext(get_env_patch(target_dir)): with envcontext(
get_env_patch(_envdir(prefix, language_version), language_version),
):
yield yield
def _add_dependencies( def _add_dependencies(
cargo_toml_path: str, prefix: Prefix,
additional_dependencies: set[str], additional_dependencies: set[str],
) -> None: ) -> None:
with open(cargo_toml_path, 'r+') as f: crates = []
cargo_toml = toml.load(f)
cargo_toml.setdefault('dependencies', {})
for dep in additional_dependencies: for dep in additional_dependencies:
name, _, spec = dep.partition(':') name, _, spec = dep.partition(':')
cargo_toml['dependencies'][name] = spec or '*' crate = f'{name}@{spec or "*"}'
f.seek(0) crates.append(crate)
toml.dump(cargo_toml, f)
f.truncate() helpers.run_setup_cmd(prefix, ('cargo', 'add', *crates))
def install_rust_with_toolchain(toolchain: str) -> None:
with tempfile.TemporaryDirectory() as rustup_dir:
with envcontext((('RUSTUP_HOME', rustup_dir),)):
# acquire `rustup` if not present
if parse_shebang.find_executable('rustup') is None:
# We did not detect rustup and need to download it first.
if sys.platform == 'win32': # pragma: win32 cover
url = 'https://win.rustup.rs/x86_64'
else: # pragma: win32 no cover
url = 'https://sh.rustup.rs'
resp = urllib.request.urlopen(url)
rustup_init = os.path.join(rustup_dir, win_exe('rustup-init'))
with open(rustup_init, 'wb') as f:
shutil.copyfileobj(resp, f)
make_executable(rustup_init)
# install rustup into `$CARGO_HOME/bin`
cmd_output_b(
rustup_init, '-y', '--quiet', '--no-modify-path',
'--default-toolchain', 'none',
)
cmd_output_b(
'rustup', 'toolchain', 'install', '--no-self-update',
toolchain,
)
def install_environment( def install_environment(
@ -57,10 +126,7 @@ def install_environment(
version: str, version: str,
additional_dependencies: Sequence[str], additional_dependencies: Sequence[str],
) -> None: ) -> None:
helpers.assert_version_default('rust', version) directory = _envdir(prefix, version)
directory = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
# There are two cases where we might want to specify more dependencies: # There are two cases where we might want to specify more dependencies:
# as dependencies for the library being built, and as binary packages # as dependencies for the library being built, and as binary packages
@ -77,19 +143,23 @@ def install_environment(
} }
lib_deps = set(additional_dependencies) - cli_deps lib_deps = set(additional_dependencies) - cli_deps
if len(lib_deps) > 0:
_add_dependencies(prefix.path('Cargo.toml'), lib_deps)
with clean_path_on_failure(directory): with clean_path_on_failure(directory):
packages_to_install: set[tuple[str, ...]] = {('--path', '.')} packages_to_install: set[tuple[str, ...]] = {('--path', '.')}
for cli_dep in cli_deps: for cli_dep in cli_deps:
cli_dep = cli_dep[len('cli:'):] cli_dep = cli_dep[len('cli:'):]
package, _, version = cli_dep.partition(':') package, _, crate_version = cli_dep.partition(':')
if version != '': if crate_version != '':
packages_to_install.add((package, '--version', version)) packages_to_install.add((package, '--version', crate_version))
else: else:
packages_to_install.add((package,)) packages_to_install.add((package,))
with in_env(prefix, version):
if version != 'system':
install_rust_with_toolchain(_rust_toolchain(version))
if len(lib_deps) > 0:
_add_dependencies(prefix, lib_deps)
for args in packages_to_install: for args in packages_to_install:
cmd_output_b( cmd_output_b(
'cargo', 'install', '--bins', '--root', directory, *args, 'cargo', 'install', '--bins', '--root', directory, *args,
@ -102,5 +172,5 @@ def run_hook(
file_args: Sequence[str], file_args: Sequence[str],
color: bool, color: bool,
) -> tuple[int, bytes]: ) -> tuple[int, bytes]:
with in_env(hook.prefix): with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color) return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -155,6 +155,10 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None:
args.config = os.path.abspath(args.config) args.config = os.path.abspath(args.config)
if args.command in {'run', 'try-repo'}: if args.command in {'run', 'try-repo'}:
args.files = [os.path.abspath(filename) for filename in args.files] args.files = [os.path.abspath(filename) for filename in args.files]
if args.commit_msg_filename is not None:
args.commit_msg_filename = os.path.abspath(
args.commit_msg_filename,
)
if args.command == 'try-repo' and os.path.exists(args.repo): if args.command == 'try-repo' and os.path.exists(args.repo):
args.repo = os.path.abspath(args.repo) args.repo = os.path.abspath(args.repo)
@ -164,6 +168,10 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None:
args.config = os.path.relpath(args.config) args.config = os.path.relpath(args.config)
if args.command in {'run', 'try-repo'}: if args.command in {'run', 'try-repo'}:
args.files = [os.path.relpath(filename) for filename in args.files] args.files = [os.path.relpath(filename) for filename in args.files]
if args.commit_msg_filename is not None:
args.commit_msg_filename = os.path.relpath(
args.commit_msg_filename,
)
if args.command == 'try-repo' and os.path.exists(args.repo): if args.command == 'try-repo' and os.path.exists(args.repo):
args.repo = os.path.relpath(args.repo) args.repo = os.path.relpath(args.repo)

View file

@ -52,7 +52,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
retcode, diff_stdout_binary, _ = cmd_output_b( retcode, diff_stdout_binary, _ = cmd_output_b(
'git', 'diff-index', '--ignore-submodules', '--binary', 'git', 'diff-index', '--ignore-submodules', '--binary',
'--exit-code', '--no-color', '--no-ext-diff', tree, '--', '--exit-code', '--no-color', '--no-ext-diff', tree, '--',
retcode=None, check=False,
) )
if retcode and diff_stdout_binary.strip(): if retcode and diff_stdout_binary.strip():
patch_filename = f'patch{int(time.time())}-{os.getpid()}' patch_filename = f'patch{int(time.time())}-{os.getpid()}'

View file

@ -83,14 +83,12 @@ class CalledProcessError(RuntimeError):
self, self,
returncode: int, returncode: int,
cmd: tuple[str, ...], cmd: tuple[str, ...],
expected_returncode: int,
stdout: bytes, stdout: bytes,
stderr: bytes | None, stderr: bytes | None,
) -> None: ) -> None:
super().__init__(returncode, cmd, expected_returncode, stdout, stderr) super().__init__(returncode, cmd, stdout, stderr)
self.returncode = returncode self.returncode = returncode
self.cmd = cmd self.cmd = cmd
self.expected_returncode = expected_returncode
self.stdout = stdout self.stdout = stdout
self.stderr = stderr self.stderr = stderr
@ -104,7 +102,6 @@ class CalledProcessError(RuntimeError):
return b''.join(( return b''.join((
f'command: {self.cmd!r}\n'.encode(), f'command: {self.cmd!r}\n'.encode(),
f'return code: {self.returncode}\n'.encode(), f'return code: {self.returncode}\n'.encode(),
f'expected return code: {self.expected_returncode}\n'.encode(),
b'stdout:', _indent_or_none(self.stdout), b'\n', b'stdout:', _indent_or_none(self.stdout), b'\n',
b'stderr:', _indent_or_none(self.stderr), b'stderr:', _indent_or_none(self.stderr),
)) ))
@ -124,7 +121,7 @@ def _oserror_to_output(e: OSError) -> tuple[int, bytes, None]:
def cmd_output_b( def cmd_output_b(
*cmd: str, *cmd: str,
retcode: int | None = 0, check: bool = True,
**kwargs: Any, **kwargs: Any,
) -> tuple[int, bytes, bytes | None]: ) -> tuple[int, bytes, bytes | None]:
_setdefault_kwargs(kwargs) _setdefault_kwargs(kwargs)
@ -142,8 +139,8 @@ def cmd_output_b(
stdout_b, stderr_b = proc.communicate() stdout_b, stderr_b = proc.communicate()
returncode = proc.returncode returncode = proc.returncode
if retcode is not None and retcode != returncode: if check and returncode:
raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b) raise CalledProcessError(returncode, cmd, stdout_b, stderr_b)
return returncode, stdout_b, stderr_b return returncode, stdout_b, stderr_b
@ -196,10 +193,10 @@ if os.name != 'nt': # pragma: win32 no cover
def cmd_output_p( def cmd_output_p(
*cmd: str, *cmd: str,
retcode: int | None = 0, check: bool = True,
**kwargs: Any, **kwargs: Any,
) -> tuple[int, bytes, bytes | None]: ) -> tuple[int, bytes, bytes | None]:
assert retcode is None assert check is False
assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr']
_setdefault_kwargs(kwargs) _setdefault_kwargs(kwargs)

View file

@ -154,7 +154,7 @@ def xargs(
run_cmd: tuple[str, ...], run_cmd: tuple[str, ...],
) -> tuple[int, bytes, bytes | None]: ) -> tuple[int, bytes, bytes | None]:
return cmd_fn( return cmd_fn(
*run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs, *run_cmd, check=False, stderr=subprocess.STDOUT, **kwargs,
) )
threads = min(len(partitions), target_concurrency) threads = min(len(partitions), target_concurrency)

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = pre_commit name = pre_commit
version = 2.20.0 version = 2.21.0
description = A framework for managing and maintaining multi-language pre-commit hooks. description = A framework for managing and maintaining multi-language pre-commit hooks.
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown
@ -13,10 +13,6 @@ classifiers =
License :: OSI Approved :: MIT License License :: OSI Approved :: MIT License
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 :: Only
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 :: CPython
Programming Language :: Python :: Implementation :: PyPy Programming Language :: Python :: Implementation :: PyPy
@ -27,8 +23,7 @@ install_requires =
identify>=1.0.0 identify>=1.0.0
nodeenv>=0.11.1 nodeenv>=0.11.1
pyyaml>=5.1 pyyaml>=5.1
toml virtualenv>=20.10.0
virtualenv>=20.0.8
importlib-metadata;python_version<"3.8" importlib-metadata;python_version<"3.8"
python_requires = >=3.7 python_requires = >=3.7
@ -61,7 +56,6 @@ check_untyped_defs = true
disallow_any_generics = true disallow_any_generics = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
disallow_untyped_defs = true disallow_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true warn_redundant_casts = true
warn_unused_ignores = true warn_unused_ignores = true

View file

@ -3,9 +3,9 @@
set -euo pipefail set -euo pipefail
. /etc/lsb-release . /etc/lsb-release
if [ "$DISTRIB_CODENAME" = "focal" ]; then if [ "$DISTRIB_CODENAME" = "jammy" ]; then
SWIFT_URL='https://download.swift.org/swift-5.6.1-release/ubuntu2004/swift-5.6.1-RELEASE/swift-5.6.1-RELEASE-ubuntu20.04.tar.gz' SWIFT_URL='https://download.swift.org/swift-5.7.1-release/ubuntu2204/swift-5.7.1-RELEASE/swift-5.7.1-RELEASE-ubuntu22.04.tar.gz'
SWIFT_HASH='2b4f22d4a8b59fe8e050f0b7f020f8d8f12553cbda56709b2340a4a3bb90cfea' SWIFT_HASH='7f60291f5088d3e77b0c2364beaabd29616ee7b37260b7b06bdbeb891a7fe161'
else else
echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2 echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2
exit 1 exit 1

View file

@ -17,7 +17,7 @@ from typing import Sequence
REPOS = ( REPOS = (
('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'),
('ruby-build', 'https://github.com/rbenv/ruby-build', '2004fd7'), ('ruby-build', 'https://github.com/rbenv/ruby-build', '98c0337'),
( (
'ruby-download', 'ruby-download',
'https://github.com/garnieretienne/rvm-download', 'https://github.com/garnieretienne/rvm-download',

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6</TargetFramework>
<PackAsTool>true</PackAsTool> <PackAsTool>true</PackAsTool>
<ToolCommandName>proj1</ToolCommandName> <ToolCommandName>proj1</ToolCommandName>

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6</TargetFramework>
<PackAsTool>true</PackAsTool> <PackAsTool>true</PackAsTool>
<ToolCommandName>proj2</ToolCommandName> <ToolCommandName>proj2</ToolCommandName>

View file

@ -0,0 +1,3 @@
bin/
obj/
nupkg/

View file

@ -0,0 +1,5 @@
- id: dotnet-example-hook
name: dotnet example hook
entry: testeroni.tool
language: dotnet
files: ''

View file

@ -0,0 +1,12 @@
using System;
namespace dotnet_hooks_repo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello from dotnet!");
}
}
}

View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<PackAsTool>true</PackAsTool>
<ToolCommandName>testeroni.tool</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
</PropertyGroup>
</Project>

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net6</TargetFramework>
<PackAsTool>true</PackAsTool> <PackAsTool>true</PackAsTool>
<ToolCommandName>testeroni</ToolCommandName> <ToolCommandName>testeroni</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath> <PackageOutputPath>./nupkg</PackageOutputPath>

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net6</TargetFramework>
<PackAsTool>true</PackAsTool> <PackAsTool>true</PackAsTool>
<ToolCommandName>testeroni</ToolCommandName> <ToolCommandName>testeroni</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath> <PackageOutputPath>./nupkg</PackageOutputPath>

View file

@ -2,4 +2,5 @@
name: Python 3 Hook name: Python 3 Hook
entry: python3-hook entry: python3-hook
language: python language: python
language_version: python3
files: \.py$ files: \.py$

View file

@ -2,5 +2,5 @@
name: Ruby Hook name: Ruby Hook
entry: ruby_hook entry: ruby_hook
language: ruby language: ruby
language_version: 2.5.1 language_version: 3.1.0
files: \.rb$ files: \.rb$

View file

@ -15,6 +15,8 @@ from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION
from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import MANIFEST_SCHEMA
from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import META_HOOK_DICT
from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import MigrateShaToRev
from pre_commit.clientlib import OptionalSensibleRegexAtHook
from pre_commit.clientlib import OptionalSensibleRegexAtTop
from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_config_main
from pre_commit.clientlib import validate_manifest_main from pre_commit.clientlib import validate_manifest_main
from testing.fixtures import sample_local_config from testing.fixtures import sample_local_config
@ -261,6 +263,27 @@ def test_warn_mutable_rev_conditional():
cfgv.validate(config_obj, CONFIG_REPO_DICT) cfgv.validate(config_obj, CONFIG_REPO_DICT)
@pytest.mark.parametrize(
'validator_cls',
(
OptionalSensibleRegexAtHook,
OptionalSensibleRegexAtTop,
),
)
def test_sensible_regex_validators_dont_pass_none(validator_cls):
key = 'files'
with pytest.raises(cfgv.ValidationError) as excinfo:
validator = validator_cls(key, cfgv.check_string)
validator.check({key: None})
assert str(excinfo.value) == (
'\n'
f'==> At key: {key}'
'\n'
'=====> Expected string got NoneType'
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
('regex', 'warning'), ('regex', 'warning'),
( (
@ -296,6 +319,22 @@ def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning):
assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)]
def test_validate_optional_sensible_regex_at_local_hook(caplog):
config_obj = sample_local_config()
config_obj['hooks'][0]['files'] = 'dir/*.py'
cfgv.validate(config_obj, CONFIG_REPO_DICT)
assert caplog.record_tuples == [
(
'pre_commit',
logging.WARNING,
"The 'files' field in hook 'do_not_commit' is a regex, not a glob "
"-- matching '/*' probably isn't what you want here",
),
]
@pytest.mark.parametrize( @pytest.mark.parametrize(
('regex', 'warning'), ('regex', 'warning'),
( (

View file

@ -135,7 +135,7 @@ def test_init_templatedir_skip_on_missing_config(
retcode, output = git_commit( retcode, output = git_commit(
fn=cmd_output_mocked_pre_commit_home, fn=cmd_output_mocked_pre_commit_home,
tempdir_factory=tempdir_factory, tempdir_factory=tempdir_factory,
retcode=None, check=False,
) )
assert retcode == commit_retcode assert retcode == commit_retcode

View file

@ -126,7 +126,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs):
cmd_output('git', 'add', touch_file) cmd_output('git', 'add', touch_file)
return git_commit( return git_commit(
fn=cmd_output_mocked_pre_commit_home, fn=cmd_output_mocked_pre_commit_home,
retcode=None, check=False,
tempdir_factory=tempdir_factory, tempdir_factory=tempdir_factory,
**kwargs, **kwargs,
) )
@ -286,7 +286,7 @@ def test_environment_not_sourced(tempdir_factory, store):
'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], 'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'],
'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], 'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'],
}, },
retcode=None, check=False,
) )
assert ret == 1 assert ret == 1
assert out == ( assert out == (
@ -551,7 +551,7 @@ def _get_push_output(tempdir_factory, remote='origin', opts=()):
return cmd_output_mocked_pre_commit_home( return cmd_output_mocked_pre_commit_home(
'git', 'push', remote, 'HEAD:new_branch', *opts, 'git', 'push', remote, 'HEAD:new_branch', *opts,
tempdir_factory=tempdir_factory, tempdir_factory=tempdir_factory,
retcode=None, check=False,
)[:2] )[:2]

View file

@ -536,6 +536,13 @@ def test_merge_conflict(cap_out, store, in_merge_conflict):
assert b'Unmerged files. Resolve before committing.' in printed assert b'Unmerged files. Resolve before committing.' in printed
def test_files_during_merge_conflict(cap_out, store, in_merge_conflict):
opts = run_opts(files=['placeholder'])
ret, printed = _do_run(cap_out, store, in_merge_conflict, opts)
assert ret == 0
assert b'Bash hook' in printed
def test_merge_conflict_modified(cap_out, store, in_merge_conflict): def test_merge_conflict_modified(cap_out, store, in_merge_conflict):
# Touch another file so we have unstaged non-conflicting things # Touch another file so we have unstaged non-conflicting things
assert os.path.exists('placeholder') assert os.path.exists('placeholder')
@ -635,6 +642,32 @@ def test_skip_bypasses_installation(cap_out, store, repo_with_passing_hook):
assert ret == 0 assert ret == 0
def test_skip_alias_bypasses_installation(
cap_out, store, repo_with_passing_hook,
):
config = {
'repo': 'local',
'hooks': [
{
'id': 'skipme',
'name': 'skipme-1',
'alias': 'skipme-1',
'entry': 'skipme',
'language': 'python',
'additional_dependencies': ['/pre-commit-does-not-exist'],
},
],
}
add_config_to_repo(repo_with_passing_hook, config)
ret, printed = _do_run(
cap_out, store, repo_with_passing_hook,
run_opts(all_files=True),
{'SKIP': 'skipme-1'},
)
assert ret == 0
def test_hook_id_not_in_non_verbose_output( def test_hook_id_not_in_non_verbose_output(
cap_out, store, repo_with_passing_hook, cap_out, store, repo_with_passing_hook,
): ):
@ -685,7 +718,7 @@ def test_non_ascii_hook_id(repo_with_passing_hook, tempdir_factory):
with cwd(repo_with_passing_hook): with cwd(repo_with_passing_hook):
_, stdout, _ = cmd_output_mocked_pre_commit_home( _, stdout, _ = cmd_output_mocked_pre_commit_home(
sys.executable, '-m', 'pre_commit.main', 'run', '', sys.executable, '-m', 'pre_commit.main', 'run', '',
retcode=None, tempdir_factory=tempdir_factory, check=False, tempdir_factory=tempdir_factory,
) )
assert 'UnicodeDecodeError' not in stdout assert 'UnicodeDecodeError' not in stdout
# Doesn't actually happen, but a reasonable assertion # Doesn't actually happen, but a reasonable assertion
@ -704,7 +737,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory):
_, out = git_commit( _, out = git_commit(
fn=cmd_output_mocked_pre_commit_home, fn=cmd_output_mocked_pre_commit_home,
tempdir_factory=tempdir_factory, tempdir_factory=tempdir_factory,
retcode=None, check=False,
) )
assert 'UnicodeEncodeError' not in out assert 'UnicodeEncodeError' not in out
# Doesn't actually happen, but a reasonable assertion # Doesn't actually happen, but a reasonable assertion

View file

@ -68,7 +68,7 @@ def _make_conflict():
bar_only_file.write('bar') bar_only_file.write('bar')
cmd_output('git', 'add', 'bar_only_file') cmd_output('git', 'add', 'bar_only_file')
git_commit(msg=_make_conflict.__name__) git_commit(msg=_make_conflict.__name__)
cmd_output('git', 'merge', 'foo', retcode=None) cmd_output('git', 'merge', 'foo', check=False)
@pytest.fixture @pytest.fixture

View file

@ -162,7 +162,7 @@ def test_error_handler_non_ascii_exception(mock_store_dir):
def test_error_handler_non_utf8_exception(mock_store_dir): def test_error_handler_non_utf8_exception(mock_store_dir):
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
with error_handler.error_handler(): with error_handler.error_handler():
raise CalledProcessError(1, ('exe',), 0, b'error: \xa0\xe1', b'') raise CalledProcessError(1, ('exe',), b'error: \xa0\xe1', b'')
def test_error_handler_non_stringable_exception(mock_store_dir): def test_error_handler_non_stringable_exception(mock_store_dir):
@ -183,10 +183,11 @@ def test_error_handler_no_tty(tempdir_factory):
'from pre_commit.error_handler import error_handler\n' 'from pre_commit.error_handler import error_handler\n'
'with error_handler():\n' 'with error_handler():\n'
' raise ValueError("\\u2603")\n', ' raise ValueError("\\u2603")\n',
retcode=3, check=False,
tempdir_factory=tempdir_factory, tempdir_factory=tempdir_factory,
pre_commit_home=pre_commit_home, pre_commit_home=pre_commit_home,
) )
assert ret == 3
log_file = os.path.join(pre_commit_home, 'pre-commit.log') log_file = os.path.join(pre_commit_home, 'pre-commit.log')
out_lines = out.splitlines() out_lines = out.splitlines()
assert out_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' assert out_lines[-2] == 'An unexpected error has occurred: ValueError: ☃'

View file

@ -104,7 +104,7 @@ def test_is_in_merge_conflict_submodule(in_conflicting_submodule):
def test_cherry_pick_conflict(in_merge_conflict): def test_cherry_pick_conflict(in_merge_conflict):
cmd_output('git', 'merge', '--abort') cmd_output('git', 'merge', '--abort')
foo_ref = cmd_output('git', 'rev-parse', 'foo')[1].strip() foo_ref = cmd_output('git', 'rev-parse', 'foo')[1].strip()
cmd_output('git', 'cherry-pick', foo_ref, retcode=None) cmd_output('git', 'cherry-pick', foo_ref, check=False)
assert git.is_in_merge_conflict() is False assert git.is_in_merge_conflict() is False

View file

@ -178,6 +178,6 @@ def test_get_docker_path_in_docker_windows(in_docker):
def test_get_docker_path_in_docker_docker_in_docker(in_docker): def test_get_docker_path_in_docker_docker_in_docker(in_docker):
# won't be able to discover "self" container in true docker-in-docker # won't be able to discover "self" container in true docker-in-docker
err = CalledProcessError(1, (), 0, b'', b'') err = CalledProcessError(1, (), b'', b'')
with mock.patch.object(docker, 'cmd_output_b', side_effect=err): with mock.patch.object(docker, 'cmd_output_b', side_effect=err):
assert docker._get_docker_path('/project') == '/project' assert docker._get_docker_path('/project') == '/project'

View file

@ -6,6 +6,7 @@ import pytest
from pre_commit import envcontext from pre_commit import envcontext
from pre_commit.languages import r from pre_commit.languages import r
from pre_commit.util import win_exe
from testing.fixtures import make_config_from_repo from testing.fixtures import make_config_from_repo
from testing.fixtures import make_repo from testing.fixtures import make_repo
from tests.repository_test import _get_hook_no_install from tests.repository_test import _get_hook_no_install
@ -133,7 +134,7 @@ def test_r_parsing_file_local(tempdir_factory, store):
def test_rscript_exec_relative_to_r_home(): def test_rscript_exec_relative_to_r_home():
expected = os.path.join('r_home_dir', 'bin', 'Rscript') expected = os.path.join('r_home_dir', 'bin', win_exe('Rscript'))
with envcontext.envcontext((('R_HOME', 'r_home_dir'),)): with envcontext.envcontext((('R_HOME', 'r_home_dir'),)):
assert r._rscript_exec() == expected assert r._rscript_exec() == expected

View file

@ -71,10 +71,10 @@ def test_install_ruby_default(fake_gem_prefix):
@xfailif_windows # pragma: win32 no cover @xfailif_windows # pragma: win32 no cover
def test_install_ruby_with_version(fake_gem_prefix): def test_install_ruby_with_version(fake_gem_prefix):
ruby.install_environment(fake_gem_prefix, '2.7.2', ()) ruby.install_environment(fake_gem_prefix, '3.1.0', ())
# Should be able to activate and use rbenv install # Should be able to activate and use rbenv install
with ruby.in_env(fake_gem_prefix, '2.7.2'): with ruby.in_env(fake_gem_prefix, '3.1.0'):
cmd_output('rbenv', 'install', '--help') cmd_output('rbenv', 'install', '--help')

View file

@ -0,0 +1,90 @@
from __future__ import annotations
from unittest import mock
import pytest
import pre_commit.constants as C
from pre_commit import parse_shebang
from pre_commit.languages import rust
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output
ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__
@pytest.fixture
def cmd_output_b_mck():
with mock.patch.object(rust, 'cmd_output_b') as mck:
yield mck
def test_sets_system_when_rust_is_available(cmd_output_b_mck):
cmd_output_b_mck.return_value = (0, b'', b'')
assert ACTUAL_GET_DEFAULT_VERSION() == 'system'
def test_uses_default_when_rust_is_not_available(cmd_output_b_mck):
cmd_output_b_mck.return_value = (127, b'', b'error: not found')
assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
@pytest.mark.parametrize('language_version', (C.DEFAULT, '1.56.0'))
def test_installs_with_bootstrapped_rustup(tmpdir, language_version):
tmpdir.join('src', 'main.rs').ensure().write(
'fn main() {\n'
' println!("Hello, world!");\n'
'}\n',
)
tmpdir.join('Cargo.toml').ensure().write(
'[package]\n'
'name = "hello_world"\n'
'version = "0.1.0"\n'
'edition = "2021"\n',
)
prefix = Prefix(str(tmpdir))
find_executable_exes = []
original_find_executable = parse_shebang.find_executable
def mocked_find_executable(exe: str) -> str | None:
"""
Return `None` the first time `find_executable` is called to ensure
that the bootstrapping code is executed, then just let the function
work as normal.
Also log the arguments to ensure that everything works as expected.
"""
find_executable_exes.append(exe)
if len(find_executable_exes) == 1:
return None
return original_find_executable(exe)
with mock.patch.object(parse_shebang, 'find_executable') as find_exe_mck:
find_exe_mck.side_effect = mocked_find_executable
rust.install_environment(prefix, language_version, ())
assert find_executable_exes == ['rustup', 'rustup', 'cargo']
with rust.in_env(prefix, language_version):
assert cmd_output('hello_world')[1] == 'Hello, world!\n'
def test_installs_with_existing_rustup(tmpdir):
tmpdir.join('src', 'main.rs').ensure().write(
'fn main() {\n'
' println!("Hello, world!");\n'
'}\n',
)
tmpdir.join('Cargo.toml').ensure().write(
'[package]\n'
'name = "hello_world"\n'
'version = "0.1.0"\n'
'edition = "2021"\n',
)
prefix = Prefix(str(tmpdir))
assert parse_shebang.find_executable('rustup') is not None
rust.install_environment(prefix, '1.56.0', ())
with rust.in_env(prefix, '1.56.0'):
assert cmd_output('hello_world')[1] == 'Hello, world!\n'

View file

@ -17,6 +17,8 @@ from testing.util import cwd
def _args(**kwargs): def _args(**kwargs):
kwargs.setdefault('command', 'help') kwargs.setdefault('command', 'help')
kwargs.setdefault('config', C.CONFIG_FILE) kwargs.setdefault('config', C.CONFIG_FILE)
if kwargs['command'] in {'run', 'try-repo'}:
kwargs.setdefault('commit_msg_filename', None)
return argparse.Namespace(**kwargs) return argparse.Namespace(**kwargs)
@ -35,13 +37,24 @@ def test_adjust_args_and_chdir_noop(in_git_dir):
def test_adjust_args_and_chdir_relative_things(in_git_dir): def test_adjust_args_and_chdir_relative_things(in_git_dir):
in_git_dir.join('foo/cfg.yaml').ensure() in_git_dir.join('foo/cfg.yaml').ensure()
in_git_dir.join('foo').chdir() with in_git_dir.join('foo').as_cwd():
args = _args(command='run', files=['f1', 'f2'], config='cfg.yaml') args = _args(command='run', files=['f1', 'f2'], config='cfg.yaml')
main._adjust_args_and_chdir(args) main._adjust_args_and_chdir(args)
assert os.getcwd() == in_git_dir assert os.getcwd() == in_git_dir
assert args.config == os.path.join('foo', 'cfg.yaml') assert args.config == os.path.join('foo', 'cfg.yaml')
assert args.files == [os.path.join('foo', 'f1'), os.path.join('foo', 'f2')] assert args.files == [
os.path.join('foo', 'f1'),
os.path.join('foo', 'f2'),
]
def test_adjust_args_and_chdir_relative_commit_msg(in_git_dir):
in_git_dir.join('foo/cfg.yaml').ensure()
with in_git_dir.join('foo').as_cwd():
args = _args(command='run', files=[], commit_msg_filename='t.txt')
main._adjust_args_and_chdir(args)
assert os.getcwd() == in_git_dir
assert args.commit_msg_filename == os.path.join('foo', 't.txt')
@pytest.mark.skipif(os.name != 'nt', reason='windows feature') @pytest.mark.skipif(os.name != 'nt', reason='windows feature')
@ -56,8 +69,7 @@ def test_install_on_subst(in_git_dir, store): # pragma: posix no cover
def test_adjust_args_and_chdir_non_relative_config(in_git_dir): def test_adjust_args_and_chdir_non_relative_config(in_git_dir):
in_git_dir.join('foo').ensure_dir().chdir() with in_git_dir.join('foo').ensure_dir().as_cwd():
args = _args() args = _args()
main._adjust_args_and_chdir(args) main._adjust_args_and_chdir(args)
assert os.getcwd() == in_git_dir assert os.getcwd() == in_git_dir
@ -65,8 +77,7 @@ def test_adjust_args_and_chdir_non_relative_config(in_git_dir):
def test_adjust_args_try_repo_repo_relative(in_git_dir): def test_adjust_args_try_repo_repo_relative(in_git_dir):
in_git_dir.join('foo').ensure_dir().chdir() with in_git_dir.join('foo').ensure_dir().as_cwd():
args = _args(command='try-repo', repo='../foo', files=[]) args = _args(command='try-repo', repo='../foo', files=[])
assert args.repo is not None assert args.repo is not None
assert os.path.exists(args.repo) assert os.path.exists(args.repo)

View file

@ -173,24 +173,14 @@ def test_python_venv(tempdir_factory, store):
) )
@xfailif_windows # pragma: win32 no cover # no python 2 in GHA def test_language_versioned_python_hook(tempdir_factory, store):
def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): # we patch this force virtualenv executing with `-p` since we can't
# We're using the python3 repo because it prints the python version # reliably have multiple pythons available in CI
path = make_repo(tempdir_factory, 'python3_hooks_repo') with mock.patch.object(
python,
def run_on_version(version, expected_output): '_sys_executable_matches',
config = make_config_from_repo(path) return_value=False,
config['hooks'][0]['language_version'] = version ):
hook = _get_hook(config, store, 'python3-hook')
ret, out = _hook_run(hook, [], color=False)
assert ret == 0
assert _norm_out(out) == expected_output
run_on_version('python2', b'2\n[]\nHello World\n')
run_on_version('python3', b'3\n[]\nHello World\n')
def test_versioned_python_hook(tempdir_factory, store):
_test_hook_repo( _test_hook_repo(
tempdir_factory, store, 'python3_hooks_repo', tempdir_factory, store, 'python3_hooks_repo',
'python3-hook', 'python3-hook',
@ -345,7 +335,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store):
tempdir_factory, store, 'ruby_versioned_hooks_repo', tempdir_factory, store, 'ruby_versioned_hooks_repo',
'ruby_hook', 'ruby_hook',
[os.devnull], [os.devnull],
b'2.5.1\nHello world from a ruby hook\n', b'3.1.0\nHello world from a ruby hook\n',
) )
@ -367,7 +357,7 @@ def test_run_ruby_hook_with_disable_shared_gems(
tempdir_factory, store, 'ruby_versioned_hooks_repo', tempdir_factory, store, 'ruby_versioned_hooks_repo',
'ruby_hook', 'ruby_hook',
[os.devnull], [os.devnull],
b'2.5.1\nHello world from a ruby hook\n', b'3.1.0\nHello world from a ruby hook\n',
) )
@ -471,7 +461,7 @@ def test_additional_rust_cli_dependencies_installed(
hook = _get_hook(config, store, 'rust-hook') hook = _get_hook(config, store, 'rust-hook')
binaries = os.listdir( binaries = os.listdir(
hook.prefix.path( hook.prefix.path(
helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', helpers.environment_dir(rust.ENVIRONMENT_DIR, 'system'), 'bin',
), ),
) )
# normalize for windows # normalize for windows
@ -485,12 +475,12 @@ def test_additional_rust_lib_dependencies_installed(
path = make_repo(tempdir_factory, 'rust_hooks_repo') path = make_repo(tempdir_factory, 'rust_hooks_repo')
config = make_config_from_repo(path) config = make_config_from_repo(path)
# A small rust package with no dependencies. # A small rust package with no dependencies.
deps = ['shellharden:3.1.0'] deps = ['shellharden:3.1.0', 'git-version']
config['hooks'][0]['additional_dependencies'] = deps config['hooks'][0]['additional_dependencies'] = deps
hook = _get_hook(config, store, 'rust-hook') hook = _get_hook(config, store, 'rust-hook')
binaries = os.listdir( binaries = os.listdir(
hook.prefix.path( hook.prefix.path(
helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', helpers.environment_dir(rust.ENVIRONMENT_DIR, 'system'), 'bin',
), ),
) )
# normalize for windows # normalize for windows
@ -883,7 +873,7 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store):
@pytest.fixture @pytest.fixture
def local_python_config(): def local_python_config():
# Make a "local" hooks repo that just installs our other hooks repo # Make a "local" hooks repo that just installs our other hooks repo
repo_path = get_resource_path('python3_hooks_repo') repo_path = get_resource_path('python_hooks_repo')
manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE))
hooks = [ hooks = [
dict(hook, additional_dependencies=[repo_path]) for hook in manifest dict(hook, additional_dependencies=[repo_path]) for hook in manifest
@ -892,23 +882,12 @@ def local_python_config():
def test_local_python_repo(store, local_python_config): def test_local_python_repo(store, local_python_config):
hook = _get_hook(local_python_config, store, 'python3-hook') hook = _get_hook(local_python_config, store, 'foo')
# language_version should have been adjusted to the interpreter version # language_version should have been adjusted to the interpreter version
assert hook.language_version != C.DEFAULT assert hook.language_version != C.DEFAULT
ret, out = _hook_run(hook, ('filename',), color=False) ret, out = _hook_run(hook, ('filename',), color=False)
assert ret == 0 assert ret == 0
assert _norm_out(out) == b"3\n['filename']\nHello World\n" assert _norm_out(out) == b"['filename']\nHello World\n"
@xfailif_windows # pragma: win32 no cover # no python2 in GHA
def test_local_python_repo_python2(store, local_python_config):
local_python_config['hooks'][0]['language_version'] = 'python2'
hook = _get_hook(local_python_config, store, 'python3-hook')
# language_version should have been adjusted to the interpreter version
assert hook.language_version != C.DEFAULT
ret, out = _hook_run(hook, ('filename',), color=False)
assert ret == 0
assert _norm_out(out) == b"2\n['filename']\nHello World\n"
def test_default_language_version(store, local_python_config): def test_default_language_version(store, local_python_config):
@ -1052,6 +1031,7 @@ def test_local_perl_additional_dependencies(store):
'dotnet_hooks_csproj_repo', 'dotnet_hooks_csproj_repo',
'dotnet_hooks_sln_repo', 'dotnet_hooks_sln_repo',
'dotnet_hooks_combo_repo', 'dotnet_hooks_combo_repo',
'dotnet_hooks_csproj_prefix_repo',
), ),
) )
def test_dotnet_hook(tempdir_factory, store, repo): def test_dotnet_hook(tempdir_factory, store, repo):

View file

@ -127,7 +127,7 @@ def test_clone_shallow_failure_fallback_to_complete(
# Force shallow clone failure # Force shallow clone failure
def fake_shallow_clone(self, *args, **kwargs): def fake_shallow_clone(self, *args, **kwargs):
raise CalledProcessError(1, (), 0, b'', None) raise CalledProcessError(1, (), b'', None)
store._shallow_clone = fake_shallow_clone store._shallow_clone = fake_shallow_clone
ret = store.clone(path, rev) ret = store.clone(path, rev)

View file

@ -18,11 +18,10 @@ from pre_commit.util import tmpdir
def test_CalledProcessError_str(): def test_CalledProcessError_str():
error = CalledProcessError(1, ('exe',), 0, b'output', b'errors') error = CalledProcessError(1, ('exe',), b'output', b'errors')
assert str(error) == ( assert str(error) == (
"command: ('exe',)\n" "command: ('exe',)\n"
'return code: 1\n' 'return code: 1\n'
'expected return code: 0\n'
'stdout:\n' 'stdout:\n'
' output\n' ' output\n'
'stderr:\n' 'stderr:\n'
@ -31,11 +30,10 @@ def test_CalledProcessError_str():
def test_CalledProcessError_str_nooutput(): def test_CalledProcessError_str_nooutput():
error = CalledProcessError(1, ('exe',), 0, b'', b'') error = CalledProcessError(1, ('exe',), b'', b'')
assert str(error) == ( assert str(error) == (
"command: ('exe',)\n" "command: ('exe',)\n"
'return code: 1\n' 'return code: 1\n'
'expected return code: 0\n'
'stdout: (none)\n' 'stdout: (none)\n'
'stderr: (none)' 'stderr: (none)'
) )
@ -83,14 +81,14 @@ def test_tmpdir():
def test_cmd_output_exe_not_found(): def test_cmd_output_exe_not_found():
ret, out, _ = cmd_output('dne', retcode=None) ret, out, _ = cmd_output('dne', check=False)
assert ret == 1 assert ret == 1
assert out == 'Executable `dne` not found' assert out == 'Executable `dne` not found'
@pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p)) @pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p))
def test_cmd_output_exe_not_found_bytes(fn): def test_cmd_output_exe_not_found_bytes(fn):
ret, out, _ = fn('dne', retcode=None, stderr=subprocess.STDOUT) ret, out, _ = fn('dne', check=False, stderr=subprocess.STDOUT)
assert ret == 1 assert ret == 1
assert out == b'Executable `dne` not found' assert out == b'Executable `dne` not found'
@ -101,7 +99,7 @@ def test_cmd_output_no_shebang(tmpdir, fn):
make_executable(f) make_executable(f)
# previously this raised `OSError` -- the output is platform specific # previously this raised `OSError` -- the output is platform specific
ret, out, _ = fn(str(f), retcode=None, stderr=subprocess.STDOUT) ret, out, _ = fn(str(f), check=False, stderr=subprocess.STDOUT)
assert ret == 1 assert ret == 1
assert isinstance(out, bytes) assert isinstance(out, bytes)
assert out.endswith(b'\n') assert out.endswith(b'\n')

View file

@ -3,7 +3,7 @@ envlist = py37,py38,pypy3,pre-commit
[testenv] [testenv]
deps = -rrequirements-dev.txt deps = -rrequirements-dev.txt
passenv = APPDATA HOME LOCALAPPDATA PROGRAMFILES RUSTUP_HOME passenv = *
commands = commands =
coverage erase coverage erase
coverage run -m pytest {posargs:tests} coverage run -m pytest {posargs:tests}
@ -23,5 +23,6 @@ env =
GIT_COMMITTER_NAME=test GIT_COMMITTER_NAME=test
GIT_AUTHOR_EMAIL=test@example.com GIT_AUTHOR_EMAIL=test@example.com
GIT_COMMITTER_EMAIL=test@example.com GIT_COMMITTER_EMAIL=test@example.com
GIT_ALLOW_PROTOCOL=file
VIRTUALENV_NO_DOWNLOAD=1 VIRTUALENV_NO_DOWNLOAD=1
PRE_COMMIT_NO_CONCURRENCY=1 PRE_COMMIT_NO_CONCURRENCY=1