mirror of
https://github.com/pre-commit/pre-commit.git
synced 2026-02-17 08:14:42 +04:00
Compare commits
26 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8416413a0e | ||
|
|
37a879e65e | ||
|
|
8a0630ca1a | ||
|
|
fcbc745744 | ||
|
|
51592eecec | ||
|
|
67e8faf80b | ||
|
|
c251e6b6d0 | ||
|
|
98ccafa3ce | ||
|
|
48953556d0 | ||
|
|
2cedd58e69 | ||
|
|
465192d7de | ||
|
|
fd42f96874 | ||
|
|
8ea2b790d8 | ||
|
|
1af6c8fa95 | ||
|
|
3358a3b540 | ||
|
|
bdf68790b7 | ||
|
|
e436690f14 | ||
|
|
8d34f95308 | ||
|
|
9c7ea88ab9 | ||
|
|
844dacc168 | ||
|
|
6a1d543e52 | ||
|
|
66278a9a0b | ||
|
|
1b32c50bc7 | ||
|
|
063229aee7 | ||
|
|
49e28eea48 | ||
|
|
d5c273a2ba |
19 changed files with 376 additions and 87 deletions
|
|
@ -10,7 +10,7 @@ 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: v3.1.0
|
rev: v3.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
|
||||||
|
|
@ -24,7 +24,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: add-trailing-comma
|
- id: add-trailing-comma
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.21.0
|
rev: v3.21.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py310-plus]
|
args: [--py310-plus]
|
||||||
|
|
@ -37,7 +37,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.18.2
|
rev: v1.19.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
additional_dependencies: [types-pyyaml]
|
additional_dependencies: [types-pyyaml]
|
||||||
|
|
|
||||||
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -1,3 +1,17 @@
|
||||||
|
4.5.1 - 2025-12-16
|
||||||
|
==================
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- Fix `language: python` with `repo: local` without `additional_dependencies`.
|
||||||
|
- #3597 PR by @asottile.
|
||||||
|
|
||||||
|
4.5.0 - 2025-11-22
|
||||||
|
==================
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Add `pre-commit hazmat`.
|
||||||
|
- #3585 PR by @asottile.
|
||||||
|
|
||||||
4.4.0 - 2025-11-08
|
4.4.0 - 2025-11-08
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -270,10 +270,19 @@ class InvalidManifestError(FatalError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _load_manifest_forward_compat(contents: str) -> object:
|
||||||
|
obj = yaml_load(contents)
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
check_min_version('5')
|
||||||
|
raise AssertionError('unreachable')
|
||||||
|
else:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
load_manifest = functools.partial(
|
load_manifest = functools.partial(
|
||||||
cfgv.load_from_filename,
|
cfgv.load_from_filename,
|
||||||
schema=MANIFEST_SCHEMA,
|
schema=MANIFEST_SCHEMA,
|
||||||
load_strategy=yaml_load,
|
load_strategy=_load_manifest_forward_compat,
|
||||||
exc_tp=InvalidManifestError,
|
exc_tp=InvalidManifestError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from pre_commit.clientlib import load_manifest
|
||||||
from pre_commit.clientlib import LOCAL
|
from pre_commit.clientlib import LOCAL
|
||||||
from pre_commit.clientlib import META
|
from pre_commit.clientlib import META
|
||||||
from pre_commit.store import Store
|
from pre_commit.store import Store
|
||||||
|
from pre_commit.util import rmtree
|
||||||
|
|
||||||
|
|
||||||
def _mark_used_repos(
|
def _mark_used_repos(
|
||||||
|
|
@ -26,7 +27,8 @@ def _mark_used_repos(
|
||||||
for hook in repo['hooks']:
|
for hook in repo['hooks']:
|
||||||
deps = hook.get('additional_dependencies')
|
deps = hook.get('additional_dependencies')
|
||||||
unused_repos.discard((
|
unused_repos.discard((
|
||||||
store.db_repo_name(repo['repo'], deps), C.LOCAL_REPO_VERSION,
|
store.db_repo_name(repo['repo'], deps),
|
||||||
|
C.LOCAL_REPO_VERSION,
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
key = (repo['repo'], repo['rev'])
|
key = (repo['repo'], repo['rev'])
|
||||||
|
|
@ -56,17 +58,19 @@ def _mark_used_repos(
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
def _gc_repos(store: Store) -> int:
|
def _gc(store: Store) -> int:
|
||||||
configs = store.select_all_configs()
|
with store.exclusive_lock(), store.connect() as db:
|
||||||
repos = store.select_all_repos()
|
store._create_configs_table(db)
|
||||||
|
|
||||||
# delete config paths which do not exist
|
|
||||||
dead_configs = [p for p in configs if not os.path.exists(p)]
|
|
||||||
live_configs = [p for p in configs if os.path.exists(p)]
|
|
||||||
|
|
||||||
|
repos = db.execute('SELECT repo, ref, path FROM repos').fetchall()
|
||||||
all_repos = {(repo, ref): path for repo, ref, path in repos}
|
all_repos = {(repo, ref): path for repo, ref, path in repos}
|
||||||
unused_repos = set(all_repos)
|
unused_repos = set(all_repos)
|
||||||
for config_path in live_configs:
|
|
||||||
|
configs_rows = db.execute('SELECT path FROM configs').fetchall()
|
||||||
|
configs = [path for path, in configs_rows]
|
||||||
|
|
||||||
|
dead_configs = []
|
||||||
|
for config_path in configs:
|
||||||
try:
|
try:
|
||||||
config = load_config(config_path)
|
config = load_config(config_path)
|
||||||
except InvalidConfigError:
|
except InvalidConfigError:
|
||||||
|
|
@ -76,14 +80,19 @@ def _gc_repos(store: Store) -> int:
|
||||||
for repo in config['repos']:
|
for repo in config['repos']:
|
||||||
_mark_used_repos(store, all_repos, unused_repos, repo)
|
_mark_used_repos(store, all_repos, unused_repos, repo)
|
||||||
|
|
||||||
store.delete_configs(dead_configs)
|
paths = [(path,) for path in dead_configs]
|
||||||
for db_repo_name, ref in unused_repos:
|
db.executemany('DELETE FROM configs WHERE path = ?', paths)
|
||||||
store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)])
|
|
||||||
|
db.executemany(
|
||||||
|
'DELETE FROM repos WHERE repo = ? and ref = ?',
|
||||||
|
sorted(unused_repos),
|
||||||
|
)
|
||||||
|
for k in unused_repos:
|
||||||
|
rmtree(all_repos[k])
|
||||||
|
|
||||||
return len(unused_repos)
|
return len(unused_repos)
|
||||||
|
|
||||||
|
|
||||||
def gc(store: Store) -> int:
|
def gc(store: Store) -> int:
|
||||||
with store.exclusive_lock():
|
output.write_line(f'{_gc(store)} repo(s) removed.')
|
||||||
repos_removed = _gc_repos(store)
|
|
||||||
output.write_line(f'{repos_removed} repo(s) removed.')
|
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
95
pre_commit/commands/hazmat.py
Normal file
95
pre_commit/commands/hazmat.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit.parse_shebang import normalize_cmd
|
||||||
|
|
||||||
|
|
||||||
|
def add_parsers(parser: argparse.ArgumentParser) -> None:
|
||||||
|
subparsers = parser.add_subparsers(dest='tool')
|
||||||
|
|
||||||
|
cd_parser = subparsers.add_parser(
|
||||||
|
'cd', help='cd to a subdir and run the command',
|
||||||
|
)
|
||||||
|
cd_parser.add_argument('subdir')
|
||||||
|
cd_parser.add_argument('cmd', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
ignore_exit_code_parser = subparsers.add_parser(
|
||||||
|
'ignore-exit-code', help='run the command but ignore the exit code',
|
||||||
|
)
|
||||||
|
ignore_exit_code_parser.add_argument('cmd', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
n1_parser = subparsers.add_parser(
|
||||||
|
'n1', help='run the command once per filename',
|
||||||
|
)
|
||||||
|
n1_parser.add_argument('cmd', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_filenames(cmd: tuple[str, ...]) -> tuple[
|
||||||
|
tuple[str, ...],
|
||||||
|
tuple[str, ...],
|
||||||
|
]:
|
||||||
|
for idx, val in enumerate(reversed(cmd)):
|
||||||
|
if val == '--':
|
||||||
|
split = len(cmd) - idx
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise SystemExit('hazmat entry must end with `--`')
|
||||||
|
|
||||||
|
return cmd[:split - 1], cmd[split:]
|
||||||
|
|
||||||
|
|
||||||
|
def cd(subdir: str, cmd: tuple[str, ...]) -> int:
|
||||||
|
cmd, filenames = _cmd_filenames(cmd)
|
||||||
|
|
||||||
|
prefix = f'{subdir}/'
|
||||||
|
new_filenames = []
|
||||||
|
for filename in filenames:
|
||||||
|
if not filename.startswith(prefix):
|
||||||
|
raise SystemExit(f'unexpected file without {prefix=}: {filename}')
|
||||||
|
else:
|
||||||
|
new_filenames.append(filename.removeprefix(prefix))
|
||||||
|
|
||||||
|
cmd = normalize_cmd(cmd)
|
||||||
|
return subprocess.call((*cmd, *new_filenames), cwd=subdir)
|
||||||
|
|
||||||
|
|
||||||
|
def ignore_exit_code(cmd: tuple[str, ...]) -> int:
|
||||||
|
cmd = normalize_cmd(cmd)
|
||||||
|
subprocess.call(cmd)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def n1(cmd: tuple[str, ...]) -> int:
|
||||||
|
cmd, filenames = _cmd_filenames(cmd)
|
||||||
|
cmd = normalize_cmd(cmd)
|
||||||
|
ret = 0
|
||||||
|
for filename in filenames:
|
||||||
|
ret |= subprocess.call((*cmd, filename))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def impl(args: argparse.Namespace) -> int:
|
||||||
|
args.cmd = tuple(args.cmd)
|
||||||
|
if args.tool == 'cd':
|
||||||
|
return cd(args.subdir, args.cmd)
|
||||||
|
elif args.tool == 'ignore-exit-code':
|
||||||
|
return ignore_exit_code(args.cmd)
|
||||||
|
elif args.tool == 'n1':
|
||||||
|
return n1(args.cmd)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f'unexpected tool: {args.tool}')
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
add_parsers(parser)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
return impl(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -5,6 +5,7 @@ import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
|
import sys
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
@ -171,7 +172,10 @@ def run_xargs(
|
||||||
|
|
||||||
|
|
||||||
def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]:
|
def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]:
|
||||||
return (*shlex.split(entry), *args)
|
cmd = shlex.split(entry)
|
||||||
|
if cmd[:2] == ['pre-commit', 'hazmat']:
|
||||||
|
cmd = [sys.executable, '-m', 'pre_commit.commands.hazmat', *cmd[2:]]
|
||||||
|
return (*cmd, *args)
|
||||||
|
|
||||||
|
|
||||||
def basic_run_hook(
|
def basic_run_hook(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import pre_commit.constants as C
|
||||||
from pre_commit import clientlib
|
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 import hazmat
|
||||||
from pre_commit.commands.autoupdate import autoupdate
|
from pre_commit.commands.autoupdate import autoupdate
|
||||||
from pre_commit.commands.clean import clean
|
from pre_commit.commands.clean import clean
|
||||||
from pre_commit.commands.gc import gc
|
from pre_commit.commands.gc import gc
|
||||||
|
|
@ -41,7 +42,7 @@ os.environ.pop('__PYVENV_LAUNCHER__', None)
|
||||||
os.environ.pop('PYTHONEXECUTABLE', None)
|
os.environ.pop('PYTHONEXECUTABLE', None)
|
||||||
|
|
||||||
COMMANDS_NO_GIT = {
|
COMMANDS_NO_GIT = {
|
||||||
'clean', 'gc', 'init-templatedir', 'sample-config',
|
'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config',
|
||||||
'validate-config', 'validate-manifest',
|
'validate-config', 'validate-manifest',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,6 +246,11 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
|
||||||
_add_cmd('gc', help='Clean unused cached repos.')
|
_add_cmd('gc', help='Clean unused cached repos.')
|
||||||
|
|
||||||
|
hazmat_parser = _add_cmd(
|
||||||
|
'hazmat', help='Composable tools for rare use in hook `entry`.',
|
||||||
|
)
|
||||||
|
hazmat.add_parsers(hazmat_parser)
|
||||||
|
|
||||||
init_templatedir_parser = _add_cmd(
|
init_templatedir_parser = _add_cmd(
|
||||||
'init-templatedir',
|
'init-templatedir',
|
||||||
help=(
|
help=(
|
||||||
|
|
@ -389,6 +395,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||||
return clean(store)
|
return clean(store)
|
||||||
elif args.command == 'gc':
|
elif args.command == 'gc':
|
||||||
return gc(store)
|
return gc(store)
|
||||||
|
elif args.command == 'hazmat':
|
||||||
|
return hazmat.impl(args)
|
||||||
elif args.command == 'hook-impl':
|
elif args.command == 'hook-impl':
|
||||||
return hook_impl(
|
return hook_impl(
|
||||||
store,
|
store,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
|
|
||||||
setup(name='pre-commit-placeholder-package', version='0.0.0')
|
setup(name='pre-commit-placeholder-package', version='0.0.0', py_modules=[])
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ from pre_commit.util import CalledProcessError
|
||||||
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 resource_text
|
from pre_commit.util import resource_text
|
||||||
from pre_commit.util import rmtree
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('pre_commit')
|
logger = logging.getLogger('pre_commit')
|
||||||
|
|
@ -96,7 +95,7 @@ class Store:
|
||||||
' PRIMARY KEY (repo, ref)'
|
' PRIMARY KEY (repo, ref)'
|
||||||
');',
|
');',
|
||||||
)
|
)
|
||||||
self._create_config_table(db)
|
self._create_configs_table(db)
|
||||||
|
|
||||||
# Atomic file move
|
# Atomic file move
|
||||||
os.replace(tmpfile, self.db_path)
|
os.replace(tmpfile, self.db_path)
|
||||||
|
|
@ -215,7 +214,7 @@ class Store:
|
||||||
'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo,
|
'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_config_table(self, db: sqlite3.Connection) -> None:
|
def _create_configs_table(self, db: sqlite3.Connection) -> None:
|
||||||
db.executescript(
|
db.executescript(
|
||||||
'CREATE TABLE IF NOT EXISTS configs ('
|
'CREATE TABLE IF NOT EXISTS configs ('
|
||||||
' path TEXT NOT NULL,'
|
' path TEXT NOT NULL,'
|
||||||
|
|
@ -232,28 +231,5 @@ class Store:
|
||||||
return
|
return
|
||||||
with self.connect() as db:
|
with self.connect() as db:
|
||||||
# TODO: eventually remove this and only create in _create
|
# TODO: eventually remove this and only create in _create
|
||||||
self._create_config_table(db)
|
self._create_configs_table(db)
|
||||||
db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))
|
db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))
|
||||||
|
|
||||||
def select_all_configs(self) -> list[str]:
|
|
||||||
with self.connect() as db:
|
|
||||||
self._create_config_table(db)
|
|
||||||
rows = db.execute('SELECT path FROM configs').fetchall()
|
|
||||||
return [path for path, in rows]
|
|
||||||
|
|
||||||
def delete_configs(self, configs: list[str]) -> None:
|
|
||||||
with self.connect() as db:
|
|
||||||
rows = [(path,) for path in configs]
|
|
||||||
db.executemany('DELETE FROM configs WHERE path = ?', rows)
|
|
||||||
|
|
||||||
def select_all_repos(self) -> list[tuple[str, str, str]]:
|
|
||||||
with self.connect() as db:
|
|
||||||
return db.execute('SELECT repo, ref, path from repos').fetchall()
|
|
||||||
|
|
||||||
def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None:
|
|
||||||
with self.connect() as db:
|
|
||||||
db.execute(
|
|
||||||
'DELETE FROM repos WHERE repo = ? and ref = ?',
|
|
||||||
(db_repo_name, ref),
|
|
||||||
)
|
|
||||||
rmtree(path)
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
name = pre_commit
|
name = pre_commit
|
||||||
version = 4.4.0
|
version = 4.5.1
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,6 @@ def main() -> int:
|
||||||
shebang = '/usr/bin/env python3'
|
shebang = '/usr/bin/env python3'
|
||||||
zipapp.create_archive(tmpdir, filename, interpreter=shebang)
|
zipapp.create_archive(tmpdir, filename, interpreter=shebang)
|
||||||
|
|
||||||
with open(f'{filename}.sha256sum', 'w') as f:
|
|
||||||
subprocess.check_call(('sha256sum', filename), stdout=f)
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ 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 InvalidManifestError
|
||||||
|
from pre_commit.clientlib import load_manifest
|
||||||
from pre_commit.clientlib import MANIFEST_HOOK_DICT
|
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
|
||||||
|
|
@ -588,3 +590,18 @@ def test_config_hook_stages_defaulting():
|
||||||
'id': 'fake-hook',
|
'id': 'fake-hook',
|
||||||
'stages': ['commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit'],
|
'stages': ['commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_manifest_v5_forward_compat(tmp_path):
|
||||||
|
manifest = tmp_path.joinpath('.pre-commit-hooks.yaml')
|
||||||
|
manifest.write_text('hooks: {}')
|
||||||
|
|
||||||
|
with pytest.raises(InvalidManifestError) as excinfo:
|
||||||
|
load_manifest(manifest)
|
||||||
|
assert str(excinfo.value) == (
|
||||||
|
f'\n'
|
||||||
|
f'==> File {manifest}\n'
|
||||||
|
f'=====> \n'
|
||||||
|
f'=====> pre-commit version 5 is required but version {C.VERSION} '
|
||||||
|
f'is installed. Perhaps run `pip install --upgrade pre-commit`.'
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,13 @@ from testing.util import git_commit
|
||||||
|
|
||||||
|
|
||||||
def _repo_count(store):
|
def _repo_count(store):
|
||||||
return len(store.select_all_repos())
|
with store.connect() as db:
|
||||||
|
return db.execute('SELECT COUNT(1) FROM repos').fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
def _config_count(store):
|
def _config_count(store):
|
||||||
return len(store.select_all_configs())
|
with store.connect() as db:
|
||||||
|
return db.execute('SELECT COUNT(1) FROM configs').fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
def _remove_config_assert_cleared(store, cap_out):
|
def _remove_config_assert_cleared(store, cap_out):
|
||||||
|
|
@ -153,7 +155,8 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out):
|
||||||
install_hooks(C.CONFIG_FILE, store)
|
install_hooks(C.CONFIG_FILE, store)
|
||||||
|
|
||||||
# we'll "break" the manifest to simulate an old version clone
|
# we'll "break" the manifest to simulate an old version clone
|
||||||
(_, _, path), = store.select_all_repos()
|
with store.connect() as db:
|
||||||
|
path, = db.execute('SELECT path FROM repos').fetchone()
|
||||||
os.remove(os.path.join(path, C.MANIFEST_FILE))
|
os.remove(os.path.join(path, C.MANIFEST_FILE))
|
||||||
|
|
||||||
assert _config_count(store) == 1
|
assert _config_count(store) == 1
|
||||||
|
|
@ -162,3 +165,11 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out):
|
||||||
assert _config_count(store) == 1
|
assert _config_count(store) == 1
|
||||||
assert _repo_count(store) == 0
|
assert _repo_count(store) == 0
|
||||||
assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.'
|
assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.'
|
||||||
|
|
||||||
|
|
||||||
|
def test_gc_pre_1_14_roll_forward(store, cap_out):
|
||||||
|
with store.connect() as db: # simulate pre-1.14.0
|
||||||
|
db.executescript('DROP TABLE configs')
|
||||||
|
|
||||||
|
assert not gc(store)
|
||||||
|
assert cap_out.get() == '0 repo(s) removed.\n'
|
||||||
|
|
|
||||||
99
tests/commands/hazmat_test.py
Normal file
99
tests/commands/hazmat_test.py
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pre_commit.commands.hazmat import _cmd_filenames
|
||||||
|
from pre_commit.commands.hazmat import main
|
||||||
|
from testing.util import cwd
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_filenames_no_dash_dash():
|
||||||
|
with pytest.raises(SystemExit) as excinfo:
|
||||||
|
_cmd_filenames(('no', 'dashdash', 'here'))
|
||||||
|
msg, = excinfo.value.args
|
||||||
|
assert msg == 'hazmat entry must end with `--`'
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_filenames_no_filenames():
|
||||||
|
cmd, filenames = _cmd_filenames(('hello', 'world', '--'))
|
||||||
|
assert cmd == ('hello', 'world')
|
||||||
|
assert filenames == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_filenames_some_filenames():
|
||||||
|
cmd, filenames = _cmd_filenames(('hello', 'world', '--', 'f1', 'f2'))
|
||||||
|
assert cmd == ('hello', 'world')
|
||||||
|
assert filenames == ('f1', 'f2')
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_filenames_multiple_dashdash():
|
||||||
|
cmd, filenames = _cmd_filenames(('hello', '--', 'arg', '--', 'f1', 'f2'))
|
||||||
|
assert cmd == ('hello', '--', 'arg')
|
||||||
|
assert filenames == ('f1', 'f2')
|
||||||
|
|
||||||
|
|
||||||
|
def test_cd_unexpected_filename():
|
||||||
|
with pytest.raises(SystemExit) as excinfo:
|
||||||
|
main(('cd', 'subdir', 'cmd', '--', 'subdir/1', 'not-subdir/2'))
|
||||||
|
msg, = excinfo.value.args
|
||||||
|
assert msg == "unexpected file without prefix='subdir/': not-subdir/2"
|
||||||
|
|
||||||
|
|
||||||
|
def _norm(out):
|
||||||
|
return out.replace('\r\n', '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_cd(tmp_path, capfd):
|
||||||
|
subdir = tmp_path.joinpath('subdir')
|
||||||
|
subdir.mkdir()
|
||||||
|
subdir.joinpath('a').write_text('a')
|
||||||
|
subdir.joinpath('b').write_text('b')
|
||||||
|
|
||||||
|
with cwd(tmp_path):
|
||||||
|
ret = main((
|
||||||
|
'cd', 'subdir',
|
||||||
|
sys.executable, '-c',
|
||||||
|
'import os; print(os.getcwd());'
|
||||||
|
'import sys; [print(open(f).read()) for f in sys.argv[1:]]',
|
||||||
|
'--',
|
||||||
|
'subdir/a', 'subdir/b',
|
||||||
|
))
|
||||||
|
|
||||||
|
assert ret == 0
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert _norm(out) == f'{subdir}\na\nb\n'
|
||||||
|
assert err == ''
|
||||||
|
|
||||||
|
|
||||||
|
def test_ignore_exit_code(capfd):
|
||||||
|
ret = main((
|
||||||
|
'ignore-exit-code', sys.executable, '-c', 'raise SystemExit("bye")',
|
||||||
|
))
|
||||||
|
assert ret == 0
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert out == ''
|
||||||
|
assert _norm(err) == 'bye\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_n1(capfd):
|
||||||
|
ret = main((
|
||||||
|
'n1', sys.executable, '-c', 'import sys; print(sys.argv[1:])',
|
||||||
|
'--',
|
||||||
|
'foo', 'bar', 'baz',
|
||||||
|
))
|
||||||
|
assert ret == 0
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert _norm(out) == "['foo']\n['bar']\n['baz']\n"
|
||||||
|
assert err == ''
|
||||||
|
|
||||||
|
|
||||||
|
def test_n1_some_error_code():
|
||||||
|
ret = main((
|
||||||
|
'n1', sys.executable, '-c',
|
||||||
|
'import sys; raise SystemExit(sys.argv[1] == "error")',
|
||||||
|
'--',
|
||||||
|
'ok', 'error', 'ok',
|
||||||
|
))
|
||||||
|
assert ret == 1
|
||||||
|
|
@ -164,3 +164,15 @@ def test_basic_run_hook(tmp_path):
|
||||||
assert ret == 0
|
assert ret == 0
|
||||||
out = out.replace(b'\r\n', b'\n')
|
out = out.replace(b'\r\n', b'\n')
|
||||||
assert out == b'hi hello file file file\n'
|
assert out == b'hi hello file file file\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_hook_cmd():
|
||||||
|
assert lang_base.hook_cmd('echo hi', ()) == ('echo', 'hi')
|
||||||
|
|
||||||
|
|
||||||
|
def test_hook_cmd_hazmat():
|
||||||
|
ret = lang_base.hook_cmd('pre-commit hazmat cd a echo -- b', ())
|
||||||
|
assert ret == (
|
||||||
|
sys.executable, '-m', 'pre_commit.commands.hazmat',
|
||||||
|
'cd', 'a', 'echo', '--', 'b',
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import pre_commit.constants as C
|
||||||
from pre_commit.envcontext import envcontext
|
from pre_commit.envcontext import envcontext
|
||||||
from pre_commit.languages import python
|
from pre_commit.languages import python
|
||||||
from pre_commit.prefix import Prefix
|
from pre_commit.prefix import Prefix
|
||||||
|
from pre_commit.store import _make_local_repo
|
||||||
|
from pre_commit.util import cmd_output_b
|
||||||
from pre_commit.util import make_executable
|
from pre_commit.util import make_executable
|
||||||
from pre_commit.util import win_exe
|
from pre_commit.util import win_exe
|
||||||
from testing.auto_namedtuple import auto_namedtuple
|
from testing.auto_namedtuple import auto_namedtuple
|
||||||
|
|
@ -351,3 +353,15 @@ def test_python_hook_weird_setup_cfg(tmp_path):
|
||||||
|
|
||||||
ret = run_language(tmp_path, python, 'socks', [os.devnull])
|
ret = run_language(tmp_path, python, 'socks', [os.devnull])
|
||||||
assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode())
|
assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode())
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_repo_with_other_artifacts(tmp_path):
|
||||||
|
cmd_output_b('git', 'init', tmp_path)
|
||||||
|
_make_local_repo(str(tmp_path))
|
||||||
|
# pretend a rust install also ran here
|
||||||
|
tmp_path.joinpath('target').mkdir()
|
||||||
|
|
||||||
|
ret, out = run_language(tmp_path, python, 'python --version')
|
||||||
|
|
||||||
|
assert ret == 0
|
||||||
|
assert out.startswith(b'Python ')
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import contextlib
|
||||||
import os.path
|
import os.path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
|
@ -8,6 +9,7 @@ import pytest
|
||||||
|
|
||||||
import pre_commit.constants as C
|
import pre_commit.constants as C
|
||||||
from pre_commit import main
|
from pre_commit import main
|
||||||
|
from pre_commit.commands import hazmat
|
||||||
from pre_commit.errors import FatalError
|
from pre_commit.errors import FatalError
|
||||||
from pre_commit.util import cmd_output
|
from pre_commit.util import cmd_output
|
||||||
from testing.auto_namedtuple import auto_namedtuple
|
from testing.auto_namedtuple import auto_namedtuple
|
||||||
|
|
@ -97,11 +99,9 @@ CMDS = tuple(fn.replace('_', '-') for fn in FNS)
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_commands():
|
def mock_commands():
|
||||||
mcks = {fn: mock.patch.object(main, fn).start() for fn in FNS}
|
with contextlib.ExitStack() as ctx:
|
||||||
ret = auto_namedtuple(**mcks)
|
mcks = {f: ctx.enter_context(mock.patch.object(main, f)) for f in FNS}
|
||||||
yield ret
|
yield auto_namedtuple(**mcks)
|
||||||
for mck in ret:
|
|
||||||
mck.stop()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -158,6 +158,17 @@ def test_all_cmds(command, mock_commands, mock_store_dir):
|
||||||
assert_only_one_mock_called(mock_commands)
|
assert_only_one_mock_called(mock_commands)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hazmat(mock_store_dir):
|
||||||
|
with mock.patch.object(hazmat, 'impl') as mck:
|
||||||
|
main.main(('hazmat', 'cd', 'subdir', '--', 'cmd', '--', 'f1', 'f2'))
|
||||||
|
assert mck.call_count == 1
|
||||||
|
(arg,), dct = mck.call_args
|
||||||
|
assert dct == {}
|
||||||
|
assert arg.tool == 'cd'
|
||||||
|
assert arg.subdir == 'subdir'
|
||||||
|
assert arg.cmd == ['cmd', '--', 'f1', 'f2']
|
||||||
|
|
||||||
|
|
||||||
def test_try_repo(mock_store_dir):
|
def test_try_repo(mock_store_dir):
|
||||||
with mock.patch.object(main, 'try_repo') as patch:
|
with mock.patch.object(main, 'try_repo') as patch:
|
||||||
main.main(('try-repo', '.'))
|
main.main(('try-repo', '.'))
|
||||||
|
|
|
||||||
|
|
@ -506,3 +506,14 @@ def test_args_with_spaces_and_quotes(tmp_path):
|
||||||
|
|
||||||
expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n"
|
expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n"
|
||||||
assert ret == (0, expected)
|
assert ret == (0, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hazmat(tmp_path):
|
||||||
|
ret = run_language(
|
||||||
|
tmp_path, unsupported,
|
||||||
|
f'pre-commit hazmat ignore-exit-code {shlex.quote(sys.executable)} '
|
||||||
|
f"-c 'import sys; raise SystemExit(sys.argv[1:])'",
|
||||||
|
('f1', 'f2'),
|
||||||
|
)
|
||||||
|
expected = b"['f1', 'f2']\n"
|
||||||
|
assert ret == (0, expected)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,17 @@ from testing.util import git_commit
|
||||||
from testing.util import xfailif_windows
|
from testing.util import xfailif_windows
|
||||||
|
|
||||||
|
|
||||||
|
def _select_all_configs(store: Store) -> list[str]:
|
||||||
|
with store.connect() as db:
|
||||||
|
rows = db.execute('SELECT * FROM configs').fetchall()
|
||||||
|
return [path for path, in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def _select_all_repos(store: Store) -> list[tuple[str, str, str]]:
|
||||||
|
with store.connect() as db:
|
||||||
|
return db.execute('SELECT repo, ref, path FROM repos').fetchall()
|
||||||
|
|
||||||
|
|
||||||
def test_our_session_fixture_works():
|
def test_our_session_fixture_works():
|
||||||
"""There's a session fixture which makes `Store` invariantly raise to
|
"""There's a session fixture which makes `Store` invariantly raise to
|
||||||
prevent writing to the home directory.
|
prevent writing to the home directory.
|
||||||
|
|
@ -91,7 +102,7 @@ def test_clone(store, tempdir_factory, caplog):
|
||||||
assert git.head_rev(ret) == rev
|
assert git.head_rev(ret) == rev
|
||||||
|
|
||||||
# Assert there's an entry in the sqlite db for this
|
# Assert there's an entry in the sqlite db for this
|
||||||
assert store.select_all_repos() == [(path, rev, ret)]
|
assert _select_all_repos(store) == [(path, rev, ret)]
|
||||||
|
|
||||||
|
|
||||||
def test_warning_for_deprecated_stages_on_init(store, tempdir_factory, caplog):
|
def test_warning_for_deprecated_stages_on_init(store, tempdir_factory, caplog):
|
||||||
|
|
@ -217,7 +228,7 @@ def test_clone_shallow_failure_fallback_to_complete(
|
||||||
assert git.head_rev(ret) == rev
|
assert git.head_rev(ret) == rev
|
||||||
|
|
||||||
# Assert there's an entry in the sqlite db for this
|
# Assert there's an entry in the sqlite db for this
|
||||||
assert store.select_all_repos() == [(path, rev, ret)]
|
assert _select_all_repos(store) == [(path, rev, ret)]
|
||||||
|
|
||||||
|
|
||||||
def test_clone_tag_not_on_mainline(store, tempdir_factory):
|
def test_clone_tag_not_on_mainline(store, tempdir_factory):
|
||||||
|
|
@ -265,7 +276,7 @@ def test_mark_config_as_used(store, tmpdir):
|
||||||
with tmpdir.as_cwd():
|
with tmpdir.as_cwd():
|
||||||
f = tmpdir.join('f').ensure()
|
f = tmpdir.join('f').ensure()
|
||||||
store.mark_config_used('f')
|
store.mark_config_used('f')
|
||||||
assert store.select_all_configs() == [f.strpath]
|
assert _select_all_configs(store) == [f.strpath]
|
||||||
|
|
||||||
|
|
||||||
def test_mark_config_as_used_idempotent(store, tmpdir):
|
def test_mark_config_as_used_idempotent(store, tmpdir):
|
||||||
|
|
@ -275,21 +286,12 @@ def test_mark_config_as_used_idempotent(store, tmpdir):
|
||||||
|
|
||||||
def test_mark_config_as_used_does_not_exist(store):
|
def test_mark_config_as_used_does_not_exist(store):
|
||||||
store.mark_config_used('f')
|
store.mark_config_used('f')
|
||||||
assert store.select_all_configs() == []
|
assert _select_all_configs(store) == []
|
||||||
|
|
||||||
|
|
||||||
def _simulate_pre_1_14_0(store):
|
|
||||||
with store.connect() as db:
|
|
||||||
db.executescript('DROP TABLE configs')
|
|
||||||
|
|
||||||
|
|
||||||
def test_select_all_configs_roll_forward(store):
|
|
||||||
_simulate_pre_1_14_0(store)
|
|
||||||
assert store.select_all_configs() == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_mark_config_as_used_roll_forward(store, tmpdir):
|
def test_mark_config_as_used_roll_forward(store, tmpdir):
|
||||||
_simulate_pre_1_14_0(store)
|
with store.connect() as db: # simulate pre-1.14.0
|
||||||
|
db.executescript('DROP TABLE configs')
|
||||||
test_mark_config_as_used(store, tmpdir)
|
test_mark_config_as_used(store, tmpdir)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -314,7 +316,7 @@ def test_mark_config_as_used_readonly(tmpdir):
|
||||||
assert store.readonly
|
assert store.readonly
|
||||||
# should be skipped due to readonly
|
# should be skipped due to readonly
|
||||||
store.mark_config_used(str(cfg))
|
store.mark_config_used(str(cfg))
|
||||||
assert store.select_all_configs() == []
|
assert _select_all_configs(store) == []
|
||||||
|
|
||||||
|
|
||||||
def test_clone_with_recursive_submodules(store, tmp_path):
|
def test_clone_with_recursive_submodules(store, tmp_path):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue