1
0
Fork 0

Adding upstream version 3.2.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 21:39:47 +01:00
parent 1a4d9cedd3
commit 346ef73c17
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
23 changed files with 357 additions and 102 deletions

View file

@ -30,7 +30,7 @@ repos:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
- repo: https://github.com/pre-commit/mirrors-autopep8 - repo: https://github.com/pre-commit/mirrors-autopep8
rev: v2.0.1 rev: v2.0.2
hooks: hooks:
- id: autopep8 - id: autopep8
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
@ -38,7 +38,7 @@ repos:
hooks: hooks:
- id: flake8 - id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.0.1 rev: v1.1.1
hooks: hooks:
- id: mypy - id: mypy
additional_dependencies: [types-all] additional_dependencies: [types-all]

View file

@ -1,3 +1,18 @@
3.2.0 - 2023-03-17
==================
### Features
- Allow `pre-commit`, `pre-push`, and `pre-merge-commit` as `stages`.
- #2732 issue by @asottile.
- #2808 PR by @asottile.
- Add `pre-rebase` hook support.
- #2582 issue by @BrutalSimplicity.
- #2725 PR by @mgaligniana.
### Fixes
- Remove bulky cargo cache from `language: rust` installs.
- #2820 PR by @asottile.
3.1.1 - 2023-02-27 3.1.1 - 2023-02-27
================== ==================

View file

@ -6,6 +6,7 @@ import re
import shlex import shlex
import sys import sys
from typing import Any from typing import Any
from typing import NamedTuple
from typing import Sequence from typing import Sequence
import cfgv import cfgv
@ -20,6 +21,21 @@ logger = logging.getLogger('pre_commit')
check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex) check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex)
HOOK_TYPES = (
'commit-msg',
'post-checkout',
'post-commit',
'post-merge',
'post-rewrite',
'pre-commit',
'pre-merge-commit',
'pre-push',
'pre-rebase',
'prepare-commit-msg',
)
# `manual` is not invoked by any installed git hook. See #719
STAGES = (*HOOK_TYPES, 'manual')
def check_type_tag(tag: str) -> None: def check_type_tag(tag: str) -> None:
if tag not in ALL_TAGS: if tag not in ALL_TAGS:
@ -43,6 +59,46 @@ def check_min_version(version: str) -> None:
) )
_STAGES = {
'commit': 'pre-commit',
'merge-commit': 'pre-merge-commit',
'push': 'pre-push',
}
def transform_stage(stage: str) -> str:
return _STAGES.get(stage, stage)
class StagesMigrationNoDefault(NamedTuple):
key: str
default: Sequence[str]
def check(self, dct: dict[str, Any]) -> None:
if self.key not in dct:
return
val = dct[self.key]
cfgv.check_array(cfgv.check_any)(val)
val = [transform_stage(v) for v in val]
cfgv.check_array(cfgv.check_one_of(STAGES))(val)
def apply_default(self, dct: dict[str, Any]) -> None:
if self.key not in dct:
return
dct[self.key] = [transform_stage(v) for v in dct[self.key]]
def remove_default(self, dct: dict[str, Any]) -> None:
raise NotImplementedError
class StagesMigration(StagesMigrationNoDefault):
def apply_default(self, dct: dict[str, Any]) -> None:
dct.setdefault(self.key, self.default)
super().apply_default(dct)
MANIFEST_HOOK_DICT = cfgv.Map( MANIFEST_HOOK_DICT = cfgv.Map(
'Hook', 'id', 'Hook', 'id',
@ -70,7 +126,7 @@ MANIFEST_HOOK_DICT = cfgv.Map(
cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('log_file', cfgv.check_string, ''),
cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'),
cfgv.Optional('require_serial', cfgv.check_bool, False), cfgv.Optional('require_serial', cfgv.check_bool, False),
cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []), StagesMigration('stages', []),
cfgv.Optional('verbose', cfgv.check_bool, False), cfgv.Optional('verbose', cfgv.check_bool, False),
) )
MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT) MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT)
@ -241,7 +297,9 @@ CONFIG_HOOK_DICT = cfgv.Map(
cfgv.OptionalNoDefault(item.key, item.check_fn) cfgv.OptionalNoDefault(item.key, item.check_fn)
for item in MANIFEST_HOOK_DICT.items for item in MANIFEST_HOOK_DICT.items
if item.key != 'id' if item.key != 'id'
if item.key != 'stages'
), ),
StagesMigrationNoDefault('stages', []),
OptionalSensibleRegexAtHook('files', cfgv.check_string), OptionalSensibleRegexAtHook('files', cfgv.check_string),
OptionalSensibleRegexAtHook('exclude', cfgv.check_string), OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
) )
@ -290,17 +348,13 @@ CONFIG_SCHEMA = cfgv.Map(
cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
cfgv.Optional( cfgv.Optional(
'default_install_hook_types', 'default_install_hook_types',
cfgv.check_array(cfgv.check_one_of(C.HOOK_TYPES)), cfgv.check_array(cfgv.check_one_of(HOOK_TYPES)),
['pre-commit'], ['pre-commit'],
), ),
cfgv.OptionalRecurse( cfgv.OptionalRecurse(
'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, 'default_language_version', DEFAULT_LANGUAGE_VERSION, {},
), ),
cfgv.Optional( StagesMigration('default_stages', STAGES),
'default_stages',
cfgv.check_array(cfgv.check_one_of(C.STAGES)),
C.STAGES,
),
cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('files', check_string_regex, ''),
cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('exclude', check_string_regex, '^$'),
cfgv.Optional('fail_fast', cfgv.check_bool, False), cfgv.Optional('fail_fast', cfgv.check_bool, False),

View file

@ -73,6 +73,8 @@ def _ns(
local_branch: str | None = None, local_branch: str | None = None,
from_ref: str | None = None, from_ref: str | None = None,
to_ref: str | None = None, to_ref: str | None = None,
pre_rebase_upstream: str | None = None,
pre_rebase_branch: str | None = None,
remote_name: str | None = None, remote_name: str | None = None,
remote_url: str | None = None, remote_url: str | None = None,
commit_msg_filename: str | None = None, commit_msg_filename: str | None = None,
@ -84,11 +86,13 @@ def _ns(
) -> argparse.Namespace: ) -> argparse.Namespace:
return argparse.Namespace( return argparse.Namespace(
color=color, color=color,
hook_stage=hook_type.replace('pre-', ''), hook_stage=hook_type,
remote_branch=remote_branch, remote_branch=remote_branch,
local_branch=local_branch, local_branch=local_branch,
from_ref=from_ref, from_ref=from_ref,
to_ref=to_ref, to_ref=to_ref,
pre_rebase_upstream=pre_rebase_upstream,
pre_rebase_branch=pre_rebase_branch,
remote_name=remote_name, remote_name=remote_name,
remote_url=remote_url, remote_url=remote_url,
commit_msg_filename=commit_msg_filename, commit_msg_filename=commit_msg_filename,
@ -185,6 +189,12 @@ def _check_args_length(hook_type: str, args: Sequence[str]) -> None:
f'hook-impl for {hook_type} expected 1, 2, or 3 arguments ' f'hook-impl for {hook_type} expected 1, 2, or 3 arguments '
f'but got {len(args)}: {args}', f'but got {len(args)}: {args}',
) )
elif hook_type == 'pre-rebase':
if len(args) < 1 or len(args) > 2:
raise SystemExit(
f'hook-impl for {hook_type} expected 1 or 2 arguments '
f'but got {len(args)}: {args}',
)
elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK: elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK:
expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type] expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type]
if len(args) != expected: if len(args) != expected:
@ -231,6 +241,13 @@ def _run_ns(
return _ns(hook_type, color, is_squash_merge=args[0]) return _ns(hook_type, color, is_squash_merge=args[0])
elif hook_type == 'post-rewrite': elif hook_type == 'post-rewrite':
return _ns(hook_type, color, rewrite_command=args[0]) return _ns(hook_type, color, rewrite_command=args[0])
elif hook_type == 'pre-rebase' and len(args) == 1:
return _ns(hook_type, color, pre_rebase_upstream=args[0])
elif hook_type == 'pre-rebase' and len(args) == 2:
return _ns(
hook_type, color, pre_rebase_upstream=args[0],
pre_rebase_branch=args[1],
)
else: else:
raise AssertionError(f'unexpected hook type: {hook_type}') raise AssertionError(f'unexpected hook type: {hook_type}')

View file

@ -254,6 +254,7 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]:
# these hooks do not operate on files # these hooks do not operate on files
if args.hook_stage in { if args.hook_stage in {
'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite',
'pre-rebase',
}: }:
return () return ()
elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}:
@ -389,6 +390,10 @@ def run(
environ['PRE_COMMIT_FROM_REF'] = args.from_ref environ['PRE_COMMIT_FROM_REF'] = args.from_ref
environ['PRE_COMMIT_TO_REF'] = args.to_ref environ['PRE_COMMIT_TO_REF'] = args.to_ref
if args.pre_rebase_upstream and args.pre_rebase_branch:
environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] = args.pre_rebase_upstream
environ['PRE_COMMIT_PRE_REBASE_BRANCH'] = args.pre_rebase_branch
if ( if (
args.remote_name and args.remote_url and args.remote_name and args.remote_url and
args.remote_branch and args.local_branch args.remote_branch and args.local_branch

View file

@ -10,17 +10,4 @@ LOCAL_REPO_VERSION = '1'
VERSION = importlib.metadata.version('pre_commit') VERSION = importlib.metadata.version('pre_commit')
# `manual` is not invoked by any installed git hook. See #719
STAGES = (
'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg',
'post-commit', 'manual', 'post-checkout', 'push', 'post-merge',
'post-rewrite',
)
HOOK_TYPES = (
'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg',
'commit-msg', 'post-commit', 'post-checkout', 'post-merge',
'post-rewrite',
)
DEFAULT = 'default' DEFAULT = 'default'

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import contextlib import contextlib
import os import os
import sys
from typing import Generator from typing import Generator
from typing import Sequence from typing import Sequence
@ -26,7 +27,7 @@ def get_env_patch(env: str) -> PatchesT:
# $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only
# seems to be used for python.exe. # seems to be used for python.exe.
path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))
if os.name == 'nt': # pragma: no cover (platform specific) if sys.platform == 'win32': # pragma: win32 cover
path = (env, os.pathsep, *path) path = (env, os.pathsep, *path)
path = (os.path.join(env, 'Scripts'), os.pathsep, *path) path = (os.path.join(env, 'Scripts'), os.pathsep, *path)
path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path) path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path)

View file

@ -48,7 +48,7 @@ def _read_pyvenv_cfg(filename: str) -> dict[str, str]:
def bin_dir(venv: str) -> str: def bin_dir(venv: str) -> str:
"""On windows there's a different directory for the virtualenv""" """On windows there's a different directory for the virtualenv"""
bin_part = 'Scripts' if os.name == 'nt' else 'bin' bin_part = 'Scripts' if sys.platform == 'win32' else 'bin'
return os.path.join(venv, bin_part) return os.path.join(venv, bin_part)
@ -137,7 +137,7 @@ def norm_version(version: str) -> str | None:
elif _sys_executable_matches(version): # virtualenv defaults to our exe elif _sys_executable_matches(version): # virtualenv defaults to our exe
return None return None
if os.name == 'nt': # pragma: no cover (windows) if sys.platform == 'win32': # pragma: no cover (windows)
version_exec = _find_by_py_launcher(version) version_exec = _find_by_py_launcher(version)
if version_exec: if version_exec:
return version_exec return version_exec

View file

@ -50,7 +50,6 @@ def _rust_toolchain(language_version: str) -> str:
def get_env_patch(target_dir: str, version: str) -> PatchesT: 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 # Only set RUSTUP_TOOLCHAIN if we don't want use the system's default
# toolchain # toolchain

View file

@ -7,6 +7,7 @@ import sys
from typing import Sequence from typing import Sequence
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import clientlib
from pre_commit import git from pre_commit import git
from pre_commit.color import add_color_option from pre_commit.color import add_color_option
from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import autoupdate
@ -52,7 +53,7 @@ def _add_config_option(parser: argparse.ArgumentParser) -> None:
def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
'-t', '--hook-type', '-t', '--hook-type',
choices=C.HOOK_TYPES, action='append', dest='hook_types', choices=clientlib.HOOK_TYPES, action='append', dest='hook_types',
) )
@ -73,7 +74,10 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
help='When hooks fail, run `git diff` directly afterward.', help='When hooks fail, run `git diff` directly afterward.',
) )
parser.add_argument( parser.add_argument(
'--hook-stage', choices=C.STAGES, default='commit', '--hook-stage',
choices=clientlib.STAGES,
type=clientlib.transform_stage,
default='pre-commit',
help='The stage during which the hook is fired. One of %(choices)s', help='The stage during which the hook is fired. One of %(choices)s',
) )
parser.add_argument( parser.add_argument(
@ -103,6 +107,17 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
'now checked out.' 'now checked out.'
), ),
) )
parser.add_argument(
'--pre-rebase-upstream', help=(
'The upstream from which the series was forked.'
),
)
parser.add_argument(
'--pre-rebase-branch', help=(
'The branch being rebased, and is not set when '
'rebasing the current branch.'
),
)
parser.add_argument( parser.add_argument(
'--commit-msg-filename', '--commit-msg-filename',
help='Filename to check when running during `commit-msg`', help='Filename to check when running during `commit-msg`',

View file

@ -119,7 +119,7 @@ def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]:
return returncode, stdout, stderr return returncode, stdout, stderr
if os.name != 'nt': # pragma: win32 no cover if sys.platform != 'win32': # pragma: win32 no cover
from os import openpty from os import openpty
import termios import termios

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = pre_commit name = pre_commit
version = 3.1.1 version = 3.2.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

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import contextlib import contextlib
import os.path import os.path
import subprocess import subprocess
import sys
import pytest import pytest
@ -30,7 +31,7 @@ def cmd_output_mocked_pre_commit_home(
return ret, out.replace('\r\n', '\n'), None return ret, out.replace('\r\n', '\n'), None
xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') xfailif_windows = pytest.mark.xfail(sys.platform == 'win32', reason='windows')
def run_opts( def run_opts(
@ -43,9 +44,11 @@ def run_opts(
local_branch='', local_branch='',
from_ref='', from_ref='',
to_ref='', to_ref='',
pre_rebase_upstream='',
pre_rebase_branch='',
remote_name='', remote_name='',
remote_url='', remote_url='',
hook_stage='commit', hook_stage='pre-commit',
show_diff_on_failure=False, show_diff_on_failure=False,
commit_msg_filename='', commit_msg_filename='',
prepare_commit_message_source='', prepare_commit_message_source='',
@ -66,6 +69,8 @@ def run_opts(
local_branch=local_branch, local_branch=local_branch,
from_ref=from_ref, from_ref=from_ref,
to_ref=to_ref, to_ref=to_ref,
pre_rebase_upstream=pre_rebase_upstream,
pre_rebase_branch=pre_rebase_branch,
remote_name=remote_name, remote_name=remote_name,
remote_url=remote_url, remote_url=remote_url,
hook_stage=hook_stage, hook_stage=hook_stage,

View file

@ -12,6 +12,7 @@ from pre_commit.clientlib import CONFIG_HOOK_DICT
from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_REPO_DICT
from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import CONFIG_SCHEMA
from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION
from pre_commit.clientlib import MANIFEST_HOOK_DICT
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 OptionalSensibleRegexAtHook from pre_commit.clientlib import OptionalSensibleRegexAtHook
@ -416,3 +417,50 @@ def test_warn_additional(schema):
x for x in schema.items if isinstance(x, cfgv.WarnAdditionalKeys) x for x in schema.items if isinstance(x, cfgv.WarnAdditionalKeys)
) )
assert allowed_keys == set(warn_additional.keys) assert allowed_keys == set(warn_additional.keys)
def test_stages_migration_for_default_stages():
cfg = {
'default_stages': ['commit-msg', 'push', 'commit', 'merge-commit'],
'repos': [],
}
cfgv.validate(cfg, CONFIG_SCHEMA)
cfg = cfgv.apply_defaults(cfg, CONFIG_SCHEMA)
assert cfg['default_stages'] == [
'commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit',
]
def test_manifest_stages_defaulting():
dct = {
'id': 'fake-hook',
'name': 'fake-hook',
'entry': 'fake-hook',
'language': 'system',
'stages': ['commit-msg', 'push', 'commit', 'merge-commit'],
}
cfgv.validate(dct, MANIFEST_HOOK_DICT)
dct = cfgv.apply_defaults(dct, MANIFEST_HOOK_DICT)
assert dct['stages'] == [
'commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit',
]
def test_config_hook_stages_defaulting_missing():
dct = {'id': 'fake-hook'}
cfgv.validate(dct, CONFIG_HOOK_DICT)
dct = cfgv.apply_defaults(dct, CONFIG_HOOK_DICT)
assert dct == {'id': 'fake-hook'}
def test_config_hook_stages_defaulting():
dct = {
'id': 'fake-hook',
'stages': ['commit-msg', 'push', 'commit', 'merge-commit'],
}
cfgv.validate(dct, CONFIG_HOOK_DICT)
dct = cfgv.apply_defaults(dct, CONFIG_HOOK_DICT)
assert dct == {
'id': 'fake-hook',
'stages': ['commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit'],
}

View file

@ -100,6 +100,8 @@ def test_run_legacy_recursive(tmpdir):
('commit-msg', ['.git/COMMIT_EDITMSG']), ('commit-msg', ['.git/COMMIT_EDITMSG']),
('post-commit', []), ('post-commit', []),
('post-merge', ['1']), ('post-merge', ['1']),
('pre-rebase', ['main', 'topic']),
('pre-rebase', ['main']),
('post-checkout', ['old_head', 'new_head', '1']), ('post-checkout', ['old_head', 'new_head', '1']),
('post-rewrite', ['amend']), ('post-rewrite', ['amend']),
# multiple choices for commit-editmsg # multiple choices for commit-editmsg
@ -139,13 +141,36 @@ def test_check_args_length_prepare_commit_msg_error():
) )
def test_check_args_length_pre_rebase_error():
with pytest.raises(SystemExit) as excinfo:
hook_impl._check_args_length('pre-rebase', [])
msg, = excinfo.value.args
assert msg == 'hook-impl for pre-rebase expected 1 or 2 arguments but got 0: []' # noqa: E501
def test_run_ns_pre_commit(): def test_run_ns_pre_commit():
ns = hook_impl._run_ns('pre-commit', True, (), b'') ns = hook_impl._run_ns('pre-commit', True, (), b'')
assert ns is not None assert ns is not None
assert ns.hook_stage == 'commit' assert ns.hook_stage == 'pre-commit'
assert ns.color is True assert ns.color is True
def test_run_ns_pre_rebase():
ns = hook_impl._run_ns('pre-rebase', True, ('main', 'topic'), b'')
assert ns is not None
assert ns.hook_stage == 'pre-rebase'
assert ns.color is True
assert ns.pre_rebase_upstream == 'main'
assert ns.pre_rebase_branch == 'topic'
ns = hook_impl._run_ns('pre-rebase', True, ('main',), b'')
assert ns is not None
assert ns.hook_stage == 'pre-rebase'
assert ns.color is True
assert ns.pre_rebase_upstream == 'main'
assert ns.pre_rebase_branch is None
def test_run_ns_commit_msg(): def test_run_ns_commit_msg():
ns = hook_impl._run_ns('commit-msg', False, ('.git/COMMIT_MSG',), b'') ns = hook_impl._run_ns('commit-msg', False, ('.git/COMMIT_MSG',), b'')
assert ns is not None assert ns is not None
@ -245,7 +270,7 @@ def test_run_ns_pre_push_updating_branch(push_example):
ns = hook_impl._run_ns('pre-push', False, args, stdin) ns = hook_impl._run_ns('pre-push', False, args, stdin)
assert ns is not None assert ns is not None
assert ns.hook_stage == 'push' assert ns.hook_stage == 'pre-push'
assert ns.color is False assert ns.color is False
assert ns.remote_name == 'origin' assert ns.remote_name == 'origin'
assert ns.remote_url == src assert ns.remote_url == src

View file

@ -810,6 +810,46 @@ def test_post_merge_integration(tempdir_factory, store):
assert os.path.exists('post-merge.tmp') assert os.path.exists('post-merge.tmp')
def test_pre_rebase_integration(tempdir_factory, store):
path = git_dir(tempdir_factory)
config = {
'repos': [
{
'repo': 'local',
'hooks': [{
'id': 'pre-rebase',
'name': 'Pre rebase',
'entry': 'touch pre-rebase.tmp',
'language': 'system',
'always_run': True,
'verbose': True,
'stages': ['pre-rebase'],
}],
},
],
}
write_config(path, config)
with cwd(path):
install(C.CONFIG_FILE, store, hook_types=['pre-rebase'])
open('foo', 'a').close()
cmd_output('git', 'add', '.')
git_commit()
cmd_output('git', 'checkout', '-b', 'branch')
open('bar', 'a').close()
cmd_output('git', 'add', '.')
git_commit()
cmd_output('git', 'checkout', 'master')
open('baz', 'a').close()
cmd_output('git', 'add', '.')
git_commit()
cmd_output('git', 'checkout', 'branch')
cmd_output('git', 'rebase', 'master', 'branch')
assert os.path.exists('pre-rebase.tmp')
def test_post_rewrite_integration(tempdir_factory, store): def test_post_rewrite_integration(tempdir_factory, store):
path = git_dir(tempdir_factory) path = git_dir(tempdir_factory)
config = { config = {

View file

@ -354,13 +354,13 @@ def test_show_diff_on_failure(
({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True),
( (
{'hook': 'nope'}, {'hook': 'nope'},
(b'No hook with id `nope` in stage `commit`',), (b'No hook with id `nope` in stage `pre-commit`',),
1, 1,
True, True,
), ),
( (
{'hook': 'nope', 'hook_stage': 'push'}, {'hook': 'nope', 'hook_stage': 'pre-push'},
(b'No hook with id `nope` in stage `push`',), (b'No hook with id `nope` in stage `pre-push`',),
1, 1,
True, True,
), ),
@ -563,6 +563,16 @@ def test_merge_conflict_resolved(cap_out, store, in_merge_conflict):
assert msg in printed assert msg in printed
def test_rebase(cap_out, store, repo_with_passing_hook):
args = run_opts(pre_rebase_upstream='master', pre_rebase_branch='topic')
environ: MutableMapping[str, str] = {}
ret, printed = _do_run(
cap_out, store, repo_with_passing_hook, args, environ,
)
assert environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] == 'master'
assert environ['PRE_COMMIT_PRE_REBASE_BRANCH'] == 'topic'
@pytest.mark.parametrize( @pytest.mark.parametrize(
('hooks', 'expected'), ('hooks', 'expected'),
( (
@ -818,7 +828,7 @@ def test_stages(cap_out, store, repo_with_passing_hook):
'language': 'pygrep', 'language': 'pygrep',
'stages': [stage], 'stages': [stage],
} }
for i, stage in enumerate(('commit', 'push', 'manual'), 1) for i, stage in enumerate(('pre-commit', 'pre-push', 'manual'), 1)
], ],
} }
add_config_to_repo(repo_with_passing_hook, config) add_config_to_repo(repo_with_passing_hook, config)
@ -833,8 +843,8 @@ def test_stages(cap_out, store, repo_with_passing_hook):
assert printed.count(b'hook ') == 1 assert printed.count(b'hook ') == 1
return printed return printed
assert _run_for_stage('commit').startswith(b'hook 1...') assert _run_for_stage('pre-commit').startswith(b'hook 1...')
assert _run_for_stage('push').startswith(b'hook 2...') assert _run_for_stage('pre-push').startswith(b'hook 2...')
assert _run_for_stage('manual').startswith(b'hook 3...') assert _run_for_stage('manual').startswith(b'hook 3...')
@ -1173,7 +1183,7 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook):
), ),
'language': 'system', 'language': 'system',
'files': r'\.py$', 'files': r'\.py$',
'stages': ['commit'], 'stages': ['pre-commit'],
}, },
{ {
'id': 'do_not_commit', 'id': 'do_not_commit',

View file

@ -36,10 +36,10 @@ def test_read_pyvenv_cfg_non_utf8(tmpdir):
def test_norm_version_expanduser(): def test_norm_version_expanduser():
home = os.path.expanduser('~') home = os.path.expanduser('~')
if os.name == 'nt': # pragma: nt cover if sys.platform == 'win32': # pragma: win32 cover
path = r'~\python343' path = r'~\python343'
expected_path = fr'{home}\python343' expected_path = fr'{home}\python343'
else: # pragma: nt no cover else: # pragma: win32 no cover
path = '~/.pyenv/versions/3.4.3/bin/python' path = '~/.pyenv/versions/3.4.3/bin/python'
expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python' expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python'
result = python.norm_version(path) result = python.norm_version(path)
@ -233,3 +233,54 @@ setup(
return_value=False, return_value=False,
): ):
assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n') assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n')
def _make_hello_hello(tmp_path):
setup_py = '''\
from setuptools import setup
setup(
name='socks',
version='0.0.0',
py_modules=['socks'],
entry_points={'console_scripts': ['socks = socks:main']},
)
'''
main_py = '''\
import sys
def main():
print(repr(sys.argv[1:]))
print('hello hello')
return 0
'''
tmp_path.joinpath('setup.py').write_text(setup_py)
tmp_path.joinpath('socks.py').write_text(main_py)
def test_simple_python_hook(tmp_path):
_make_hello_hello(tmp_path)
ret = run_language(tmp_path, python, 'socks', [os.devnull])
assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode())
def test_simple_python_hook_default_version(tmp_path):
# make sure that this continues to work for platforms where default
# language detection does not work
with mock.patch.object(
python,
'get_default_version',
return_value=C.DEFAULT,
):
test_simple_python_hook(tmp_path)
def test_python_hook_weird_setup_cfg(tmp_path):
_make_hello_hello(tmp_path)
setup_cfg = '[install]\ninstall_scripts=/usr/sbin'
tmp_path.joinpath('setup.cfg').write_text(setup_cfg)
ret = run_language(tmp_path, python, 'socks', [os.devnull])
assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode())

View file

@ -216,3 +216,9 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir):
'Is it installed, and are you in a Git repository directory?' 'Is it installed, and are you in a Git repository directory?'
) )
assert cap_out_lines[-1] == f'Check the log at {log_file}' assert cap_out_lines[-1] == f'Check the log at {log_file}'
def test_hook_stage_migration(mock_store_dir):
with mock.patch.object(main, 'run') as mck:
main.main(('run', '--hook-stage', 'commit'))
assert mck.call_args[0][2].hook_stage == 'pre-commit'

View file

@ -94,7 +94,7 @@ def test_normexe_does_not_exist_sep():
assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',) assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',)
@pytest.mark.xfail(os.name == 'nt', reason='posix only') @pytest.mark.xfail(sys.platform == 'win32', reason='posix only')
def test_normexe_not_executable(tmpdir): # pragma: win32 no cover def test_normexe_not_executable(tmpdir): # pragma: win32 no cover
tmpdir.join('exe').ensure() tmpdir.join('exe').ensure()
with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo: with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo:

View file

@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import annotations
import os.path import os.path
import shlex
import shutil import shutil
import sys
from typing import Any from typing import Any
from unittest import mock from unittest import mock
@ -16,6 +18,7 @@ from pre_commit.clientlib import CONFIG_SCHEMA
from pre_commit.clientlib import load_manifest from pre_commit.clientlib import load_manifest
from pre_commit.hook import Hook from pre_commit.hook import Hook
from pre_commit.languages import python from pre_commit.languages import python
from pre_commit.languages import system
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.repository import _hook_installed from pre_commit.repository import _hook_installed
from pre_commit.repository import all_hooks from pre_commit.repository import all_hooks
@ -79,51 +82,6 @@ def _test_hook_repo(
assert out == expected assert out == expected
def test_python_hook(tempdir_factory, store):
_test_hook_repo(
tempdir_factory, store, 'python_hooks_repo',
'foo', [os.devnull],
f'[{os.devnull!r}]\nHello World\n'.encode(),
)
def test_python_hook_default_version(tempdir_factory, store):
# make sure that this continues to work for platforms where default
# language detection does not work
with mock.patch.object(
python,
'get_default_version',
return_value=C.DEFAULT,
):
test_python_hook(tempdir_factory, store)
def test_python_hook_args_with_spaces(tempdir_factory, store):
_test_hook_repo(
tempdir_factory, store, 'python_hooks_repo',
'foo',
[],
b"['i have spaces', 'and\"\\'quotes', '$and !this']\n"
b'Hello World\n',
config_kwargs={
'hooks': [{
'id': 'foo',
'args': ['i have spaces', 'and"\'quotes', '$and !this'],
}],
},
)
def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store):
in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin')
_test_hook_repo(
tempdir_factory, store, 'python_hooks_repo',
'foo', [os.devnull],
f'[{os.devnull!r}]\nHello World\n'.encode(),
)
def test_python_venv_deprecation(store, caplog): def test_python_venv_deprecation(store, caplog):
config = { config = {
'repo': 'local', 'repo': 'local',
@ -198,7 +156,7 @@ def test_intermixed_stdout_stderr(tempdir_factory, store):
) )
@pytest.mark.xfail(os.name == 'nt', reason='ptys are posix-only') @pytest.mark.xfail(sys.platform == 'win32', reason='ptys are posix-only')
def test_output_isatty(tempdir_factory, store): def test_output_isatty(tempdir_factory, store):
_test_hook_repo( _test_hook_repo(
tempdir_factory, store, 'stdout_stderr_repo', tempdir_factory, store, 'stdout_stderr_repo',
@ -430,7 +388,7 @@ def test_local_python_repo(store, local_python_config):
def test_default_language_version(store, local_python_config): def test_default_language_version(store, local_python_config):
config: dict[str, Any] = { config: dict[str, Any] = {
'default_language_version': {'python': 'fake'}, 'default_language_version': {'python': 'fake'},
'default_stages': ['commit'], 'default_stages': ['pre-commit'],
'repos': [local_python_config], 'repos': [local_python_config],
} }
@ -447,18 +405,18 @@ def test_default_language_version(store, local_python_config):
def test_default_stages(store, local_python_config): def test_default_stages(store, local_python_config):
config: dict[str, Any] = { config: dict[str, Any] = {
'default_language_version': {'python': C.DEFAULT}, 'default_language_version': {'python': C.DEFAULT},
'default_stages': ['commit'], 'default_stages': ['pre-commit'],
'repos': [local_python_config], 'repos': [local_python_config],
} }
# `stages` was not set, should default # `stages` was not set, should default
hook, = all_hooks(config, store) hook, = all_hooks(config, store)
assert hook.stages == ['commit'] assert hook.stages == ['pre-commit']
# `stages` is set, should not default # `stages` is set, should not default
config['repos'][0]['hooks'][0]['stages'] = ['push'] config['repos'][0]['hooks'][0]['stages'] = ['pre-push']
hook, = all_hooks(config, store) hook, = all_hooks(config, store)
assert hook.stages == ['push'] assert hook.stages == ['pre-push']
def test_hook_id_not_present(tempdir_factory, store, caplog): def test_hook_id_not_present(tempdir_factory, store, caplog):
@ -526,11 +484,19 @@ def test_manifest_hooks(tempdir_factory, store):
name='Bash hook', name='Bash hook',
pass_filenames=True, pass_filenames=True,
require_serial=False, require_serial=False,
stages=( stages=[
'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'commit-msg',
'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', 'post-checkout',
'post-commit',
'post-merge',
'post-rewrite', 'post-rewrite',
), 'pre-commit',
'pre-merge-commit',
'pre-push',
'pre-rebase',
'prepare-commit-msg',
'manual',
],
types=['file'], types=['file'],
types_or=[], types_or=[],
verbose=False, verbose=False,
@ -582,3 +548,14 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog):
'using language `system` which does not install an environment. ' 'using language `system` which does not install an environment. '
'Perhaps you meant to use a specific language?' 'Perhaps you meant to use a specific language?'
) )
def test_args_with_spaces_and_quotes(tmp_path):
ret = run_language(
tmp_path, system,
f"{shlex.quote(sys.executable)} -c 'import sys; print(sys.argv[1:])'",
('i have spaces', 'and"\'quotes', '$and !this'),
)
expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n"
assert ret == (0, expected)

View file

@ -187,7 +187,7 @@ def test_xargs_propagate_kwargs_to_cmd():
assert b'Pre commit is awesome' in stdout assert b'Pre commit is awesome' in stdout
@pytest.mark.xfail(os.name == 'nt', reason='posix only') @pytest.mark.xfail(sys.platform == 'win32', reason='posix only')
def test_xargs_color_true_makes_tty(): def test_xargs_color_true_makes_tty():
retcode, out = xargs.xargs( retcode, out = xargs.xargs(
(sys.executable, '-c', 'import sys; print(sys.stdout.isatty())'), (sys.executable, '-c', 'import sys; print(sys.stdout.isatty())'),

View file

@ -6,7 +6,7 @@ deps = -rrequirements-dev.txt
passenv = * passenv = *
commands = commands =
coverage erase coverage erase
coverage run -m pytest {posargs:tests} --ignore=tests/languages coverage run -m pytest {posargs:tests} --ignore=tests/languages --durations=20
coverage report --omit=pre_commit/languages/*,tests/languages/* coverage report --omit=pre_commit/languages/*,tests/languages/*
[testenv:pre-commit] [testenv:pre-commit]