diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index b70c942fe..58a4ae67d 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -6,4 +6,4 @@ runs: using: composite steps: - uses: asottile/workflows/.github/actions/latest-git@v1.4.0 - if: inputs.env == 'py39' && runner.os == 'Linux' + if: inputs.env == 'py311' && runner.os == 'Linux' diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index fccf29892..92e0e24fa 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.11' - name: install deps run: python -mpip install -e . -r requirements-dev.txt - name: vars @@ -39,7 +39,7 @@ jobs: - uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1 - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.11' - run: echo "$CONDA\Scripts" >> "$GITHUB_PATH" shell: bash @@ -63,7 +63,7 @@ jobs: echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" shell: bash if: matrix.os == 'windows-latest' && matrix.language == 'perl' - - uses: haskell/actions/setup@v2 + - uses: haskell-actions/setup@v2 if: matrix.language == 'haskell' - uses: r-lib/actions/setup-r@v2 if: matrix.os == 'ubuntu-latest' && matrix.language == 'r' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7fda646ff..bf12183c6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,12 +12,12 @@ concurrency: jobs: main-windows: - uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 + uses: asottile/workflows/.github/workflows/tox.yml@v1.11.1 with: - env: '["py39"]' + env: '["py311"]' os: windows-latest main-linux: - uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 + uses: asottile/workflows/.github/workflows/tox.yml@v1.11.1 with: - env: '["py39", "py310", "py311", "py312"]' + env: '["py311", "py312", "py313", "py314"]' os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 464cfe600..0aa1867c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,34 +10,34 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.8.0 + rev: v3.3.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.15.0 + rev: v3.18.0 hooks: - id: reorder-python-imports - exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) - args: [--py39-plus, --add-import, 'from __future__ import annotations'] + exclude: ^pre_commit/resources/ + args: [--py311-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v3.2.0 + rev: v4.0.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.20.0 + rev: v3.22.0 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py311-plus] - repo: https://github.com/hhatto/autopep8 rev: v2.3.2 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.3.0 + rev: 7.4.1 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v2.4.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a63f781..6f8c2942c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,93 @@ +4.7.0 - 2026-10-12 +================== + +### Fixes +- Fix `language: node` hooks during commit with `-a` or inside a submodule. + - Regressed in 4.6.1. + - #3773 issue by @cbirkhold. + - #3774 PR by @cbirkhold. + +4.6.2 - 2026-08-10 +================== + +### Fixes +- Fix `language: node` hooks that contain `"scripts": {"build": ...}` with + npm 11.x. + - Regressed in 4.6.1. + - #3737 issue by @mheiges. + - #3743 PR by @asottile. + +4.6.1 - 2026-07-21 +================== + +### Fixes +- Install `language: node` hooks via `git`. + - Fixes npm 12.x compatibility + - #3719 PR by @asottile. + - #3517 issue by @ojob. +- Set `JULIA_DEPOT_PATH` for `language: julia`. + - #3711 PR by @damonbayer. + - pre-commit-ci/runner-image#335 issue by @damonbayer. +- Produce error on mistyped `--repo` for `pre-commit autoupdate`. + - #3701 PR by @mxr. + - #3695 issue by @mxr. +- Improve performance of commit existence check in `pre-push`. + - #3726 PR by @asottile. + - #3604 issue by @ptarjan. +- Avoid duplicating conflicted filenames during `pre-commit run --all-files`. + - #3727 PR by @asottile. + - #3706 issue by @RomanValov. + +4.6.0 - 2026-04-21 +================== + +### Features +- `pre-commit hook-impl`: allow `--hook-dir` to be missing to enable easier + usage with `git` 2.54+ git hooks. + - #3662 PR by @asottile. + +### Fixes +- `pre-commit hook-impl`: `--hook-type` is required. + - #3661 PR by @asottile. + +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 +================== + +### Features +- Add `--fail-fast` option to `pre-commit run`. + - #3528 PR by @JulianMaurin. +- Upgrade `ruby-build` / `rbenv`. + - #3566 PR by @asottile. + - #3565 issue by @MRigal. +- Add `language: unsupported` / `language: unsupported_script` as aliases + for `language: system` / `language: script` (which will eventually be + deprecated). + - #3577 PR by @asottile. +- Add support docker-in-docker detection for cgroups v2. + - #3535 PR by @br-rhrbacek. + - #3360 issue by @JasonAlt. + +### Fixes +- Handle when docker gives `SecurityOptions: null`. + - #3537 PR by @asottile. + - #3514 issue by @jenstroeger. +- Fix error context for invalid `stages` in `.pre-commit-config.yaml`. + - #3576 PR by @asottile. + 4.3.0 - 2025-08-09 ================== @@ -71,7 +161,7 @@ - #3315 PR by @asottile. - #2732 issue by @asottile. -### Migrating +### Updating - `language: python_venv` has been removed -- use `language: python` instead. - #3320 PR by @asottile. - #2734 issue by @asottile. @@ -159,7 +249,7 @@ - Use `time.monotonic()` for more accurate hook timing. - #3024 PR by @adamchainz. -### Migrating +### Updating - Require npm 6.x+ for `language: node` hooks. - #2996 PR by @RoelAdriaans. - #1983 issue by @henryiii. diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py index ba569c377..166bc167f 100644 --- a/pre_commit/all_languages.py +++ b/pre_commit/all_languages.py @@ -19,9 +19,9 @@ from pre_commit.languages import r from pre_commit.languages import ruby from pre_commit.languages import rust -from pre_commit.languages import script from pre_commit.languages import swift -from pre_commit.languages import system +from pre_commit.languages import unsupported +from pre_commit.languages import unsupported_script languages: dict[str, Language] = { @@ -43,8 +43,8 @@ 'r': r, 'ruby': ruby, 'rust': rust, - 'script': script, 'swift': swift, - 'system': system, + 'unsupported': unsupported, + 'unsupported_script': unsupported_script, } language_names = sorted(languages) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c0f736d92..51f14d26e 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -6,6 +6,7 @@ import re import shlex import sys +from collections.abc import Callable from collections.abc import Sequence from typing import Any from typing import NamedTuple @@ -116,11 +117,12 @@ 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) + with cfgv.validate_context(f'At key: {self.key}'): + 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) + 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: @@ -189,6 +191,42 @@ def remove_default(self, dct: dict[str, Any]) -> None: raise NotImplementedError +def _translate_language(name: str) -> str: + return { + 'system': 'unsupported', + 'script': 'unsupported_script', + }.get(name, name) + + +class LanguageMigration(NamedTuple): # remove + key: str + check_fn: Callable[[object], None] + + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + with cfgv.validate_context(f'At key: {self.key}'): + self.check_fn(_translate_language(dct[self.key])) + + def apply_default(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + dct[self.key] = _translate_language(dct[self.key]) + + def remove_default(self, dct: dict[str, Any]) -> None: + raise NotImplementedError + + +class LanguageMigrationRequired(LanguageMigration): # replace with Required + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + raise cfgv.ValidationError(f'Missing required key: {self.key}') + + super().check(dct) + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -202,7 +240,7 @@ def remove_default(self, dct: dict[str, Any]) -> None: cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), - cfgv.Required('language', cfgv.check_one_of(language_names)), + LanguageMigrationRequired('language', cfgv.check_one_of(language_names)), cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional('files', check_string_regex, ''), @@ -232,10 +270,19 @@ class InvalidManifestError(FatalError): 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( cfgv.load_from_filename, schema=MANIFEST_SCHEMA, - load_strategy=yaml_load, + load_strategy=_load_manifest_forward_compat, exc_tp=InvalidManifestError, ) @@ -367,8 +414,10 @@ def check(self, dct: dict[str, Any]) -> None: 'Hook', 'id', cfgv.Required('id', cfgv.check_string), cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), - # language must be system - cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), + # language must be `unsupported` + cfgv.Optional( + 'language', cfgv.check_one_of({'unsupported'}), 'unsupported', + ), # entry cannot be overridden NotAllowed('entry', cfgv.check_any), *( @@ -401,8 +450,10 @@ def check(self, dct: dict[str, Any]) -> None: for item in MANIFEST_HOOK_DICT.items if item.key != 'id' if item.key != 'stages' + if item.key != 'language' # remove ), StagesMigrationNoDefault('stages', []), + LanguageMigration('language', cfgv.check_one_of(language_names)), # remove *_COMMON_HOOK_WARNINGS, ) LOCAL_HOOK_DICT = cfgv.Map( diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index aa0c5e25e..f07e54b80 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -175,6 +175,13 @@ def autoupdate( repo for repo in load_config(config_file)['repos'] if repo['repo'] not in {LOCAL, META} ] + missing_repos = set(repos) - {r['repo'] for r in config_repos} + if missing_repos: + output.write_line( + f'repos ({", ".join(sorted(missing_repos))}) were ' + f'not found in {config_file}', + ) + return 1 rev_infos: list[RevInfo | None] = [None] * len(config_repos) jobs = jobs or xargs.cpu_count() # 0 => number of cpus diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index 6892e097c..975d5e4c1 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -12,6 +12,7 @@ from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META from pre_commit.store import Store +from pre_commit.util import rmtree def _mark_used_repos( @@ -26,7 +27,8 @@ def _mark_used_repos( for hook in repo['hooks']: deps = hook.get('additional_dependencies') 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: key = (repo['repo'], repo['rev']) @@ -56,34 +58,41 @@ def _mark_used_repos( )) -def _gc_repos(store: Store) -> int: - configs = store.select_all_configs() - repos = store.select_all_repos() +def _gc(store: Store) -> int: + with store.exclusive_lock(), store.connect() as db: + 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} + unused_repos = set(all_repos) - all_repos = {(repo, ref): path for repo, ref, path in repos} - unused_repos = set(all_repos) - for config_path in live_configs: - try: - config = load_config(config_path) - except InvalidConfigError: - dead_configs.append(config_path) - continue - else: - for repo in config['repos']: - _mark_used_repos(store, all_repos, unused_repos, repo) + configs_rows = db.execute('SELECT path FROM configs').fetchall() + configs = [path for path, in configs_rows] + + dead_configs = [] + for config_path in configs: + try: + config = load_config(config_path) + except InvalidConfigError: + dead_configs.append(config_path) + continue + else: + for repo in config['repos']: + _mark_used_repos(store, all_repos, unused_repos, repo) + + paths = [(path,) for path in dead_configs] + db.executemany('DELETE FROM configs WHERE path = ?', paths) + + db.executemany( + 'DELETE FROM repos WHERE repo = ? and ref = ?', + sorted(unused_repos), + ) + for k in unused_repos: + rmtree(all_repos[k]) - store.delete_configs(dead_configs) - for db_repo_name, ref in unused_repos: - store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)]) - return len(unused_repos) + return len(unused_repos) def gc(store: Store) -> int: - with store.exclusive_lock(): - repos_removed = _gc_repos(store) - output.write_line(f'{repos_removed} repo(s) removed.') + output.write_line(f'{_gc(store)} repo(s) removed.') return 0 diff --git a/pre_commit/commands/hazmat.py b/pre_commit/commands/hazmat.py new file mode 100644 index 000000000..01b27ce61 --- /dev/null +++ b/pre_commit/commands/hazmat.py @@ -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()) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 49a80b7b3..0ec303564 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -16,7 +16,7 @@ def _run_legacy( hook_type: str, - hook_dir: str, + hook_dir: str | None, args: Sequence[str], ) -> tuple[int, bytes]: if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'): @@ -33,6 +33,9 @@ def _run_legacy( else: stdin = b'' + if hook_dir is None: # git 2.54+ hooks + return 0, stdin + # not running in legacy mode legacy_hook = os.path.join(hook_dir, f'{hook_type}.legacy') if not os.access(legacy_hook, os.X_OK): @@ -106,11 +109,12 @@ def _ns( hook=None, verbose=False, show_diff_on_failure=False, + fail_fast=False, ) def _rev_exists(rev: str) -> bool: - return not subprocess.call(('git', 'rev-list', '--quiet', rev)) + return not subprocess.call(('git', 'cat-file', '-e', f'{rev}^{{commit}}')) def _pre_push_ns( @@ -258,7 +262,7 @@ def hook_impl( config: str, color: bool, hook_type: str, - hook_dir: str, + hook_dir: str | None, skip_on_missing_config: bool, args: Sequence[str], ) -> int: diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index c5d47a08e..b04c53a5e 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -3,7 +3,7 @@ import functools import itertools import textwrap -from typing import Callable +from collections.abc import Callable import cfgv import yaml diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 793adbdb2..8ab505ffb 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -298,7 +298,8 @@ def _run_hooks( verbose=args.verbose, use_color=args.color, ) retval |= current_retval - if current_retval and (config['fail_fast'] or hook.fail_fast): + fail_fast = (config['fail_fast'] or hook.fail_fast or args.fail_fast) + if current_retval and fail_fast: break if retval and args.show_diff_on_failure and prior_diff: if args.all_files: diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index c840ad8b5..6223f869e 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -3,8 +3,8 @@ import contextlib import errno import sys +from collections.abc import Callable from collections.abc import Generator -from typing import Callable if sys.platform == 'win32': # pragma: no cover (windows) diff --git a/pre_commit/git.py b/pre_commit/git.py index 2f424f89e..3d3f58016 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -152,7 +152,7 @@ def intent_to_add_files() -> list[str]: def get_all_files() -> list[str]: - return zsplit(cmd_output('git', 'ls-files', '-z')[1]) + return zsplit(cmd_output('git', 'ls-files', '-z', '--deduplicate')[1]) def get_changed_files(old: str, new: str) -> list[str]: @@ -219,7 +219,7 @@ def check_for_cygwin_mismatch() -> None: if is_cygwin_python ^ is_cygwin_git: exe_type = {True: '(cygwin)', False: '(windows)'} - logger.warn( + logger.warning( f'pre-commit has detected a mix of cygwin python / git\n' f'This combination is not supported, it is likely you will ' f'receive an error later in the program.\n' diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 95be7b9b3..198e93657 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -5,6 +5,7 @@ import random import re import shlex +import sys from collections.abc import Generator from collections.abc import Sequence from typing import Any @@ -171,7 +172,10 @@ def run_xargs( 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( diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 086e874d5..7f45ac865 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,9 +1,11 @@ from __future__ import annotations +import contextlib import functools import hashlib import json import os +import re from collections.abc import Sequence from pre_commit import lang_base @@ -17,31 +19,33 @@ health_check = lang_base.basic_health_check in_env = lang_base.no_env # no special environment for docker +_HOSTNAME_MOUNT_RE = re.compile( + rb""" + /containers + (?:/overlay-containers)? + /([a-z0-9]{64}) + (?:/userdata)? + /hostname + """, + re.VERBOSE, +) -def _is_in_docker() -> bool: - try: - with open('/proc/1/cgroup', 'rb') as f: - return b'docker' in f.read() - except FileNotFoundError: - return False +def _get_container_id() -> str | None: + with contextlib.suppress(FileNotFoundError): + with open('/proc/1/mountinfo', 'rb') as f: + for line in f: + m = _HOSTNAME_MOUNT_RE.search(line) + if m: + return m[1].decode() -def _get_container_id() -> str: - # It's assumed that we already check /proc/1/cgroup in _is_in_docker. The - # cpuset cgroup controller existed since cgroups were introduced so this - # way of getting the container ID is pretty reliable. - with open('/proc/1/cgroup', 'rb') as f: - for line in f.readlines(): - if line.split(b':')[1] == b'cpuset': - return os.path.basename(line.split(b':')[2]).strip().decode() - raise RuntimeError('Failed to find the container ID in /proc/1/cgroup.') + return None def _get_docker_path(path: str) -> str: - if not _is_in_docker(): - return path - container_id = _get_container_id() + if container_id is None: + return path try: _, out, _ = cmd_output_b('docker', 'inspect', container_id) @@ -115,7 +119,7 @@ def _is_rootless() -> bool: # pragma: win32 no cover return ( # docker: # https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemInfo - 'name=rootless' in info.get('SecurityOptions', ()) or + 'name=rootless' in (info.get('SecurityOptions') or ()) or # podman: # https://docs.podman.io/en/latest/_static/api.html?version=v5.4#tag/system/operation/SystemInfoLibpod info['host']['security']['rootless'] diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 678c04b14..bedbd114b 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -90,8 +90,7 @@ def _infer_go_version(version: str) -> str: if version != C.DEFAULT: return version resp = urllib.request.urlopen('https://go.dev/dl/?mode=json') - # TODO: 3.9+ .removeprefix('go') - return json.load(resp)[0]['version'][2:] + return json.load(resp)[0]['version'].removeprefix('go') def _get_url(version: str) -> str: diff --git a/pre_commit/languages/julia.py b/pre_commit/languages/julia.py index 7559b5ba6..5122c3fc2 100644 --- a/pre_commit/languages/julia.py +++ b/pre_commit/languages/julia.py @@ -51,6 +51,13 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT: ('JULIA_LOAD_PATH', target_dir), # May be set, remove it to not interfer with LOAD_PATH ('JULIA_PROJECT', UNSET), + # Keep the package depot inside the hook environment so installed + # packages and precompile caches persist with the env instead of + # leaking into a shared depot. The trailing separator leaves an empty + # entry, which julia expands to its default depots. This keeps the + # bundled stdlib resources (and their precompile caches) available so + # hooks don't recompile from scratch on first run. + ('JULIA_DEPOT_PATH', os.path.join(target_dir, 'depot') + os.pathsep), ) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index af7dc6f87..3ab6e2b8b 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -13,11 +13,11 @@ from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var +from pre_commit.git import no_git_env from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' run_hook = lang_base.basic_run_hook @@ -77,7 +77,18 @@ def health_check(prefix: Prefix, version: str) -> str | None: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - assert prefix.exists('package.json') + if prefix.exists('.git') and prefix.exists('package.json'): + # this requires a new-enough npm (2019ish?) + pkgs = (f'git+file://{prefix.prefix_dir}', *additional_dependencies) + else: + pkgs = (*additional_dependencies,) + + if not pkgs: + raise AssertionError( + '`language: node` must have package.json or ' + 'additional_dependencies', + ) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath @@ -89,22 +100,8 @@ def install_environment( cmd_output_b(*cmd) with in_env(prefix, version): - # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 - # install as if we installed from git - - local_install_cmd = ( - 'npm', 'install', '--include=dev', '--include=prod', - '--ignore-prepublish', '--no-progress', '--no-save', + install = ( + 'npm', 'install', '--allow-git=root', '--install-links', '-g', + *pkgs, ) - lang_base.setup_cmd(prefix, local_install_cmd) - - _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) - pkg = prefix.path(pkg.strip()) - - install = ('npm', 'install', '-g', pkg, *additional_dependencies) - lang_base.setup_cmd(prefix, install) - - # clean these up after installation - if prefix.exists('node_modules'): # pragma: win32 no cover - rmtree(prefix.path('node_modules')) - os.remove(pkg) + lang_base.setup_cmd(prefix, install, env=no_git_env()) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/unsupported.py similarity index 100% rename from pre_commit/languages/system.py rename to pre_commit/languages/unsupported.py diff --git a/pre_commit/languages/script.py b/pre_commit/languages/unsupported_script.py similarity index 100% rename from pre_commit/languages/script.py rename to pre_commit/languages/unsupported_script.py diff --git a/pre_commit/main.py b/pre_commit/main.py index 559c927c9..d46c0d5b7 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -10,6 +10,7 @@ from pre_commit import clientlib from pre_commit import git 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.clean import clean from pre_commit.commands.gc import gc @@ -41,7 +42,7 @@ os.environ.pop('PYTHONEXECUTABLE', None) COMMANDS_NO_GIT = { - 'clean', 'gc', 'init-templatedir', 'sample-config', + 'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config', 'validate-config', 'validate-manifest', } @@ -62,10 +63,10 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument('hook', nargs='?', help='A single hook-id to run') - parser.add_argument('--verbose', '-v', action='store_true', default=False) + parser.add_argument('--verbose', '-v', action='store_true') mutex_group = parser.add_mutually_exclusive_group(required=False) mutex_group.add_argument( - '--all-files', '-a', action='store_true', default=False, + '--all-files', '-a', action='store_true', help='Run on all the files in the repo.', ) mutex_group.add_argument( @@ -76,6 +77,10 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--show-diff-on-failure', action='store_true', help='When hooks fail, run `git diff` directly afterward.', ) + parser.add_argument( + '--fail-fast', action='store_true', + help='Stop after the first failing hook.', + ) parser.add_argument( '--hook-stage', choices=clientlib.STAGES, @@ -241,6 +246,11 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: _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', help=( @@ -275,7 +285,7 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: ) _add_hook_type_option(install_parser) install_parser.add_argument( - '--allow-missing-config', action='store_true', default=False, + '--allow-missing-config', action='store_true', help=( 'Whether to allow a missing `pre-commit` configuration file ' 'or exit with a failure code.' @@ -347,7 +357,7 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: hook_impl_parser = subparsers.add_parser('hook-impl') add_color_option(hook_impl_parser) _add_config_option(hook_impl_parser) - hook_impl_parser.add_argument('--hook-type') + hook_impl_parser.add_argument('--hook-type', required=True) hook_impl_parser.add_argument('--hook-dir') hook_impl_parser.add_argument( '--skip-on-missing-config', action='store_true', @@ -385,6 +395,8 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: return clean(store) elif args.command == 'gc': return gc(store) + elif args.command == 'hazmat': + return hazmat.impl(args) elif args.command == 'hook-impl': return hook_impl( store, diff --git a/pre_commit/resources/empty_template_.npmignore b/pre_commit/resources/empty_template_.npmignore deleted file mode 100644 index 72e8ffc0d..000000000 --- a/pre_commit/resources/empty_template_.npmignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/pre_commit/resources/empty_template_package.json b/pre_commit/resources/empty_template_package.json deleted file mode 100644 index 042e9583c..000000000 --- a/pre_commit/resources/empty_template_package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "pre_commit_placeholder_package", - "version": "0.0.0" -} diff --git a/pre_commit/resources/empty_template_setup.py b/pre_commit/resources/empty_template_setup.py index ef05eef84..e8b1ff02c 100644 --- a/pre_commit/resources/empty_template_setup.py +++ b/pre_commit/resources/empty_template_setup.py @@ -1,4 +1,4 @@ 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=[]) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 111546e3d..b5df08744 100644 Binary files a/pre_commit/resources/rbenv.tar.gz and b/pre_commit/resources/rbenv.tar.gz differ diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index a4f7eb24f..5c82c9060 100644 Binary files a/pre_commit/resources/ruby-build.tar.gz and b/pre_commit/resources/ruby-build.tar.gz differ diff --git a/pre_commit/store.py b/pre_commit/store.py index 1235942c5..8bd1e1955 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -5,9 +5,9 @@ import os.path import sqlite3 import tempfile +from collections.abc import Callable from collections.abc import Generator from collections.abc import Sequence -from typing import Callable import pre_commit.constants as C from pre_commit import clientlib @@ -17,7 +17,6 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b from pre_commit.util import resource_text -from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') @@ -38,8 +37,8 @@ def _get_default_directory() -> str: _LOCAL_RESOURCES = ( - 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', - 'package.json', 'pre-commit-package-dev-1.rockspec', + 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', + 'pre-commit-package-dev-1.rockspec', 'pre_commit_placeholder_package.gemspec', 'setup.py', 'environment.yml', 'Makefile.PL', 'pubspec.yaml', 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', @@ -96,7 +95,7 @@ def __init__(self, directory: str | None = None) -> None: ' PRIMARY KEY (repo, ref)' ');', ) - self._create_config_table(db) + self._create_configs_table(db) # Atomic file move os.replace(tmpfile, self.db_path) @@ -215,7 +214,7 @@ def make_local(self, deps: Sequence[str]) -> str: '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( 'CREATE TABLE IF NOT EXISTS configs (' ' path TEXT NOT NULL,' @@ -232,28 +231,5 @@ def mark_config_used(self, path: str) -> None: return with self.connect() as db: # 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,)) - - 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) diff --git a/pre_commit/util.py b/pre_commit/util.py index e199d0807..19b1880bf 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -8,10 +8,10 @@ import stat import subprocess import sys +from collections.abc import Callable from collections.abc import Generator from types import TracebackType from typing import Any -from typing import Callable from pre_commit import parse_shebang diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index a1345b583..7c98d1674 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -7,12 +7,12 @@ import os import subprocess import sys +from collections.abc import Callable from collections.abc import Generator from collections.abc import Iterable from collections.abc import MutableMapping from collections.abc import Sequence from typing import Any -from typing import Callable from typing import TypeVar from pre_commit import parse_shebang diff --git a/setup.cfg b/setup.cfg index 9b0e02ad4..3e4a84970 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.3.0 +version = 4.7.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown @@ -23,7 +23,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 virtualenv>=20.10.0 -python_requires = >=3.9 +python_requires = >=3.11 [options.packages.find] exclude = @@ -52,6 +52,7 @@ check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true +enable_error_code = deprecated warn_redundant_casts = true warn_unused_ignores = true diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh index 958e73b24..0087feb82 100755 --- a/testing/get-coursier.sh +++ b/testing/get-coursier.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -if [ "$OSTYPE" = msys ]; then +if [ "$OSTYPE" = cygwin ]; then URL='https://github.com/coursier/coursier/releases/download/v2.1.0-RC4/cs-x86_64-pc-win32.zip' SHA256='0d07386ff0f337e3e6264f7dde29d137dda6eaa2385f29741435e0b93ccdb49d' TARGET='/tmp/coursier/cs.zip' diff --git a/testing/get-dart.sh b/testing/get-dart.sh index b4545e71e..ad7f8608a 100755 --- a/testing/get-dart.sh +++ b/testing/get-dart.sh @@ -3,7 +3,7 @@ set -euo pipefail VERSION=2.19.6 -if [ "$OSTYPE" = msys ]; then +if [ "$OSTYPE" = cygwin ]; then URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" cygpath -w /tmp/dart-sdk/bin >> "$GITHUB_PATH" else diff --git a/testing/make-archives b/testing/make-archives index eb3f3af85..10f40a3a7 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -16,8 +16,8 @@ from collections.abc import Sequence REPOS = ( - ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', 'ed384c8'), + ('rbenv', 'https://github.com/rbenv/rbenv', '10e96bfc'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', '447468b1'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 2c2370092..000000000 --- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: python3-hook - name: Python 3 Hook - entry: python3-hook - language: python - language_version: python3 - files: \.py$ diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py deleted file mode 100644 index 8c9cda4c6..000000000 --- a/testing/resources/python3_hooks_repo/py3_hook.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - - -def main(): - print(sys.version_info[0]) - print(repr(sys.argv[1:])) - print('Hello World') - return 0 diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py deleted file mode 100644 index 9125dc1df..000000000 --- a/testing/resources/python3_hooks_repo/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -from setuptools import setup - -setup( - name='python3_hook', - version='0.0.0', - py_modules=['py3_hook'], - entry_points={'console_scripts': ['python3-hook = py3_hook:main']}, -) diff --git a/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml b/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml deleted file mode 100644 index b2c347c14..000000000 --- a/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: system-hook-with-spaces - name: System hook with spaces - entry: bash -c 'echo "Hello World"' - language: system - files: \.sh$ diff --git a/testing/util.py b/testing/util.py index 08d52cbc3..894cebd6b 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,6 +1,5 @@ from __future__ import annotations -import contextlib import os.path import subprocess import sys @@ -40,6 +39,7 @@ def run_opts( color=False, verbose=False, hook=None, + fail_fast=False, remote_branch='', local_branch='', from_ref='', @@ -65,6 +65,7 @@ def run_opts( color=color, verbose=verbose, hook=hook, + fail_fast=fail_fast, remote_branch=remote_branch, local_branch=local_branch, from_ref=from_ref, @@ -84,16 +85,6 @@ def run_opts( ) -@contextlib.contextmanager -def cwd(path): - original_cwd = os.getcwd() - os.chdir(path) - try: - yield - finally: - os.chdir(original_cwd) - - def git_commit(*args, fn=cmd_output, msg='commit!', all_files=True, **kwargs): kwargs.setdefault('stderr', subprocess.STDOUT) diff --git a/testing/zipapp/Dockerfile b/testing/zipapp/Dockerfile index ea967e383..119c0ba46 100644 --- a/testing/zipapp/Dockerfile +++ b/testing/zipapp/Dockerfile @@ -1,9 +1,7 @@ -FROM ubuntu:jammy +FROM ubuntu:noble RUN : \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - python3 \ - python3-distutils \ python3-venv \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/testing/zipapp/make b/testing/zipapp/make index 165046f66..20d6d35a8 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -44,7 +44,7 @@ def _add_shim(dest: str) -> None: with tempfile.TemporaryDirectory() as tmpdir: _exit_if_retv( 'podman', 'run', '--rm', '--volume', f'{tmpdir}:/out:rw', IMG, - 'cp', '/venv/lib/python3.10/site-packages/distlib/t32.exe', '/out', + 'cp', '/venv/lib/python3.12/site-packages/distlib/t32.exe', '/out', ) with open(os.path.join(dest, 'python.exe'), 'wb') as f: @@ -107,9 +107,6 @@ def main() -> int: shebang = '/usr/bin/env python3' zipapp.create_archive(tmpdir, filename, interpreter=shebang) - with open(f'{filename}.sha256sum', 'w') as f: - subprocess.check_call(('sha256sum', filename), stdout=f) - return 0 diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 7aa84af0e..2c42b80cf 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -12,6 +12,8 @@ from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION +from pre_commit.clientlib import InvalidManifestError +from pre_commit.clientlib import load_manifest from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT @@ -309,6 +311,27 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] +def test_invalid_stages_error(): + cfg = {'repos': [sample_local_config()]} + cfg['repos'][0]['hooks'][0]['stages'] = ['invalid'] + + with pytest.raises(cfgv.ValidationError) as excinfo: + cfgv.validate(cfg, CONFIG_SCHEMA) + + assert str(excinfo.value) == ( + '\n' + '==> At Config()\n' + '==> At key: repos\n' + "==> At Repository(repo='local')\n" + '==> At key: hooks\n' + "==> At Hook(id='do_not_commit')\n" + # this line was missing due to the custom validator + '==> At key: stages\n' + '==> At index 0\n' + "=====> Expected one of commit-msg, manual, post-checkout, post-commit, post-merge, post-rewrite, pre-commit, pre-merge-commit, pre-push, pre-rebase, prepare-commit-msg but got: 'invalid'" # noqa: E501 + ) + + def test_warning_for_deprecated_stages(caplog): config_obj = sample_local_config() config_obj['hooks'][0]['stages'] = ['commit', 'push'] @@ -359,6 +382,26 @@ def test_no_warning_for_non_deprecated_default_stages(caplog): assert caplog.record_tuples == [] +def test_unsupported_language_migration(): + cfg = {'repos': [sample_local_config(), sample_local_config()]} + cfg['repos'][0]['hooks'][0]['language'] = 'system' + cfg['repos'][1]['hooks'][0]['language'] = 'script' + + cfgv.validate(cfg, CONFIG_SCHEMA) + ret = cfgv.apply_defaults(cfg, CONFIG_SCHEMA) + + assert ret['repos'][0]['hooks'][0]['language'] == 'unsupported' + assert ret['repos'][1]['hooks'][0]['language'] == 'unsupported_script' + + +def test_unsupported_language_migration_language_required(): + cfg = {'repos': [sample_local_config()]} + del cfg['repos'][0]['hooks'][0]['language'] + + with pytest.raises(cfgv.ValidationError): + cfgv.validate(cfg, CONFIG_SCHEMA) + + @pytest.mark.parametrize( 'manifest_obj', ( @@ -547,3 +590,18 @@ def test_config_hook_stages_defaulting(): 'id': 'fake-hook', '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`.' + ) diff --git a/tests/color_test.py b/tests/color_test.py index 89b4fd3eb..e8da98abc 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -36,24 +36,30 @@ def test_use_color_no_tty(): def test_use_color_tty_with_color_support(): - with mock.patch.object(sys.stderr, 'isatty', return_value=True): - with mock.patch('pre_commit.color.terminal_supports_color', True): - with envcontext.envcontext((('TERM', envcontext.UNSET),)): - assert use_color('auto') is True + with ( + mock.patch.object(sys.stderr, 'isatty', return_value=True), + mock.patch('pre_commit.color.terminal_supports_color', True), + envcontext.envcontext((('TERM', envcontext.UNSET),)), + ): + assert use_color('auto') is True def test_use_color_tty_without_color_support(): - with mock.patch.object(sys.stderr, 'isatty', return_value=True): - with mock.patch('pre_commit.color.terminal_supports_color', False): - with envcontext.envcontext((('TERM', envcontext.UNSET),)): - assert use_color('auto') is False + with ( + mock.patch.object(sys.stderr, 'isatty', return_value=True), + mock.patch('pre_commit.color.terminal_supports_color', False), + envcontext.envcontext((('TERM', envcontext.UNSET),)), + ): + assert use_color('auto') is False def test_use_color_dumb_term(): - with mock.patch.object(sys.stderr, 'isatty', return_value=True): - with mock.patch('pre_commit.color.terminal_supports_color', True): - with envcontext.envcontext((('TERM', 'dumb'),)): - assert use_color('auto') is False + with ( + mock.patch.object(sys.stderr, 'isatty', return_value=True), + mock.patch('pre_commit.color.terminal_supports_color', True), + envcontext.envcontext((('TERM', 'dumb'),)), + ): + assert use_color('auto') is False def test_use_color_raises_if_given_shenanigans(): diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 71bd04446..cbf22f393 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -260,8 +260,8 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( assert 'local' in after -def test_autoupdate_out_of_date_repo_with_wrong_repo_name( - out_of_date, in_tmpdir, +def test_autoupdate_missing_repo_name( + out_of_date, in_tmpdir, cap_out, ): config = make_config_from_repo( out_of_date.path, rev=out_of_date.original_rev, check=False, @@ -270,15 +270,17 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( with open(C.CONFIG_FILE) as f: before = f.read() - # It will not update it, because the name doesn't match ret = autoupdate( C.CONFIG_FILE, freeze=False, tags_only=False, - repos=('dne',), + repos=('dne', 'foo'), ) with open(C.CONFIG_FILE) as f: after = f.read() - assert ret == 0 + assert ret == 1 assert before == after + assert cap_out.get() == ( + f"repos (dne, foo) were not found in {C.CONFIG_FILE}\n" + ) def test_does_not_reformat(tmpdir, out_of_date): diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 95113ed5c..992b02f3b 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -19,11 +19,13 @@ 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): - 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): @@ -153,7 +155,8 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out): install_hooks(C.CONFIG_FILE, store) # 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)) 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 _repo_count(store) == 0 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' diff --git a/tests/commands/hazmat_test.py b/tests/commands/hazmat_test.py new file mode 100644 index 000000000..b5533c76b --- /dev/null +++ b/tests/commands/hazmat_test.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import contextlib +import sys + +import pytest + +from pre_commit.commands.hazmat import _cmd_filenames +from pre_commit.commands.hazmat import main + + +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 contextlib.chdir(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 diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index d757e85c0..0c8529343 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import subprocess import sys from unittest import mock @@ -15,7 +16,6 @@ from testing.fixtures import git_dir from testing.fixtures import sample_local_config from testing.fixtures import write_config -from testing.util import cwd from testing.util import git_commit @@ -63,6 +63,11 @@ def test_run_legacy_does_not_exist(tmpdir): assert (retv, stdin) == (0, b'') +def test_run_legacy_git_2_54(): + retv, stdin = hook_impl._run_legacy('pre-commit', None, ()) + assert (retv, stdin) == (0, b'') + + def test_run_legacy_executes_legacy_script(tmpdir, capfd): hook = tmpdir.join('pre-commit.legacy') hook.write('#!/usr/bin/env bash\necho hi "$@"\nexit 1\n') @@ -264,7 +269,7 @@ def push_example(tempdir_factory): def test_run_ns_pre_push_updating_branch(push_example): src, src_head, clone, clone_head = push_example - with cwd(clone): + with contextlib.chdir(clone): args = ('origin', src) stdin = f'HEAD {clone_head} refs/heads/b {src_head}\n'.encode() ns = hook_impl._run_ns('pre-push', False, args, stdin) @@ -282,7 +287,7 @@ def test_run_ns_pre_push_updating_branch(push_example): def test_run_ns_pre_push_new_branch(push_example): src, src_head, clone, clone_head = push_example - with cwd(clone): + with contextlib.chdir(clone): args = ('origin', src) stdin = f'HEAD {clone_head} refs/heads/b {hook_impl.Z40}\n'.encode() ns = hook_impl._run_ns('pre-push', False, args, stdin) @@ -295,7 +300,7 @@ def test_run_ns_pre_push_new_branch(push_example): def test_run_ns_pre_push_new_branch_existing_rev(push_example): src, src_head, clone, _ = push_example - with cwd(clone): + with contextlib.chdir(clone): args = ('origin', src) stdin = f'HEAD {src_head} refs/heads/b2 {hook_impl.Z40}\n'.encode() ns = hook_impl._run_ns('pre-push', False, args, stdin) @@ -306,7 +311,7 @@ def test_run_ns_pre_push_new_branch_existing_rev(push_example): def test_run_ns_pre_push_ref_with_whitespace(push_example): src, src_head, clone, _ = push_example - with cwd(clone): + with contextlib.chdir(clone): args = ('origin', src) line = f'HEAD^{{/ }} {src_head} refs/heads/b2 {hook_impl.Z40}\n' stdin = line.encode() @@ -322,7 +327,7 @@ def test_pushing_orphan_branch(push_example): git_commit(cwd=clone, msg='something else to get unique hash') clone_rev = git.head_rev(clone) - with cwd(clone): + with contextlib.chdir(clone): args = ('origin', src) stdin = f'HEAD {clone_rev} refs/heads/b2 {hook_impl.Z40}\n'.encode() ns = hook_impl._run_ns('pre-push', False, args, stdin) @@ -334,7 +339,7 @@ def test_pushing_orphan_branch(push_example): def test_run_ns_pre_push_deleting_branch(push_example): src, src_head, clone, _ = push_example - with cwd(clone): + with contextlib.chdir(clone): args = ('origin', src) stdin = f'(delete) {hook_impl.Z40} refs/heads/b {src_head}'.encode() ns = hook_impl._run_ns('pre-push', False, args, stdin) @@ -347,7 +352,7 @@ def test_hook_impl_main_noop_pre_push(cap_out, store, push_example): stdin = f'(delete) {hook_impl.Z40} refs/heads/b {src_head}'.encode() with mock.patch.object(sys.stdin.buffer, 'read', return_value=stdin): - with cwd(clone): + with contextlib.chdir(clone): write_config('.', sample_local_config()) ret = hook_impl.hook_impl( store, @@ -363,7 +368,7 @@ def test_hook_impl_main_noop_pre_push(cap_out, store, push_example): def test_hook_impl_main_runs_hooks(cap_out, tempdir_factory, store): - with cwd(git_dir(tempdir_factory)): + with contextlib.chdir(git_dir(tempdir_factory)): write_config('.', sample_local_config()) ret = hook_impl.hook_impl( store, diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 28f29b77c..d72301593 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import os.path from unittest import mock @@ -12,7 +13,6 @@ from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.util import cmd_output_mocked_pre_commit_home -from testing.util import cwd from testing.util import git_commit @@ -31,7 +31,7 @@ def test_init_templatedir(tmpdir, tempdir_factory, store, cap_out): with envcontext((('GIT_TEMPLATE_DIR', target),)): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): retcode, output = git_commit( fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, @@ -43,7 +43,7 @@ def test_init_templatedir(tmpdir, tempdir_factory, store, cap_out): def test_init_templatedir_already_set(tmpdir, tempdir_factory, store, cap_out): target = str(tmpdir.join('tmpl')) tmp_git_dir = git_dir(tempdir_factory) - with cwd(tmp_git_dir): + with contextlib.chdir(tmp_git_dir): cmd_output('git', 'config', 'init.templateDir', target) init_templatedir( C.CONFIG_FILE, store, target, hook_types=['pre-commit'], @@ -73,7 +73,7 @@ def test_init_templatedir_not_set(tmpdir, store, cap_out): def test_init_templatedir_expanduser(tmpdir, tempdir_factory, store, cap_out): target = str(tmpdir.join('tmpl')) tmp_git_dir = git_dir(tempdir_factory) - with cwd(tmp_git_dir): + with contextlib.chdir(tmp_git_dir): cmd_output('git', 'config', 'init.templateDir', '~/templatedir') with mock.patch.object(os.path, 'expanduser', return_value=target): init_templatedir( @@ -88,7 +88,7 @@ def test_init_templatedir_expanduser(tmpdir, tempdir_factory, store, cap_out): def test_init_templatedir_hookspath_set(tmpdir, tempdir_factory, store): target = tmpdir.join('tmpl') tmp_git_dir = git_dir(tempdir_factory) - with cwd(tmp_git_dir): + with contextlib.chdir(tmp_git_dir): cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') init_templatedir( C.CONFIG_FILE, store, target, hook_types=['pre-commit'], @@ -114,7 +114,7 @@ def test_init_templatedir_skip_on_missing_config( ): target = str(tmpdir.join('tmpl')) init_git_dir = git_dir(tempdir_factory) - with cwd(init_git_dir): + with contextlib.chdir(init_git_dir): cmd_output('git', 'config', 'init.templateDir', target) init_templatedir( C.CONFIG_FILE, @@ -131,7 +131,7 @@ def test_init_templatedir_skip_on_missing_config( with envcontext((('GIT_TEMPLATE_DIR', target),)): verify_git_dir = git_dir(tempdir_factory) - with cwd(verify_git_dir): + with contextlib.chdir(verify_git_dir): retcode, output = git_commit( fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 9eb0e741a..3f886b2c6 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import os.path import re @@ -24,7 +25,6 @@ from testing.fixtures import remove_config_from_repo from testing.fixtures import write_config from testing.util import cmd_output_mocked_pre_commit_home -from testing.util import cwd from testing.util import git_commit @@ -153,7 +153,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): def test_install_pre_commit_and_run(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) @@ -163,7 +163,7 @@ def test_install_pre_commit_and_run(tempdir_factory, store): def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): cmd_output('git', 'mv', C.CONFIG_FILE, 'custom.yaml') git_commit(cwd=path) assert install('custom.yaml', store, hook_types=['pre-commit']) == 0 @@ -180,7 +180,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): git_commit(cwd=parent_path) sub_pth = os.path.join(parent_path, 'sub') - with cwd(sub_pth): + with contextlib.chdir(sub_pth): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -193,7 +193,7 @@ def test_install_in_worktree_and_run(tempdir_factory, store): cmd_output('git', '-C', src_path, 'branch', '-m', 'notmaster') cmd_output('git', '-C', src_path, 'worktree', 'add', path, '-b', 'master') - with cwd(path): + with contextlib.chdir(path): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -203,7 +203,7 @@ def test_install_in_worktree_and_run(tempdir_factory, store): def test_commit_am(tempdir_factory, store): """Regression test for #322.""" path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): # Make an unstaged change open('unstaged', 'w').close() cmd_output('git', 'add', '.') @@ -219,7 +219,7 @@ def test_commit_am(tempdir_factory, store): def test_unicode_merge_commit_message(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') git_commit('-n', cwd=path) @@ -236,7 +236,7 @@ def test_unicode_merge_commit_message(tempdir_factory, store): def test_install_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 @@ -264,7 +264,7 @@ def _path_without_us(): def test_environment_not_sourced(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) # simulate deleting the virtualenv by rewriting the exe hook = os.path.join(path, '.git/hooks/pre-commit') @@ -310,7 +310,7 @@ def test_environment_not_sourced(tempdir_factory, store): def test_failing_hooks_returns_nonzero(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') - with cwd(path): + with contextlib.chdir(path): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) @@ -335,7 +335,7 @@ def _write_legacy_hook(path): def test_install_existing_hooks_no_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): _write_legacy_hook(path) # Make sure we installed the "old" hook correctly @@ -356,7 +356,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): def test_legacy_overwriting_legacy_hook(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): _write_legacy_hook(path) assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 _write_legacy_hook(path) @@ -366,7 +366,7 @@ def test_legacy_overwriting_legacy_hook(tempdir_factory, store): def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): _write_legacy_hook(path) # Install twice @@ -403,7 +403,7 @@ def test_install_with_existing_non_utf8_script(tmpdir, store): def test_failing_existing_hook_returns_1(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): # Write out a failing "old" hook os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: @@ -420,7 +420,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): def test_install_overwrite_no_existing_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): assert not install( C.CONFIG_FILE, store, hook_types=['pre-commit'], overwrite=True, ) @@ -432,7 +432,7 @@ def test_install_overwrite_no_existing_hooks(tempdir_factory, store): def test_install_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): _write_legacy_hook(path) assert not install( C.CONFIG_FILE, store, hook_types=['pre-commit'], overwrite=True, @@ -445,7 +445,7 @@ def test_install_overwrite(tempdir_factory, store): def test_uninstall_restores_legacy_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): _write_legacy_hook(path) # Now install and uninstall pre-commit @@ -460,7 +460,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): def test_replace_old_commit_script(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): # Install a script that looks like our old script pre_commit_contents = resource_text('hook-tmpl') new_contents = pre_commit_contents.replace( @@ -500,7 +500,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): def test_installs_hooks_with_hooks_True(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): install(C.CONFIG_FILE, store, hook_types=['pre-commit'], hooks=True) ret, output = _get_commit_output( tempdir_factory, pre_commit_home=store.directory, @@ -512,7 +512,7 @@ def test_installs_hooks_with_hooks_True(tempdir_factory, store): def test_install_hooks_command(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): install(C.CONFIG_FILE, store, hook_types=['pre-commit']) install_hooks(C.CONFIG_FILE, store) ret, output = _get_commit_output( @@ -525,7 +525,7 @@ def test_install_hooks_command(tempdir_factory, store): def test_installed_from_venv(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): install(C.CONFIG_FILE, store, hook_types=['pre-commit']) # No environment so pre-commit is not on the path when running! # Should still pick up the python from when we installed @@ -562,7 +562,7 @@ def test_pre_push_integration_failing(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'failing_hook_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) - with cwd(path): + with contextlib.chdir(path): install(C.CONFIG_FILE, store, hook_types=['pre-push']) # commit succeeds because pre-commit is only installed for pre-push assert _get_commit_output(tempdir_factory)[0] == 0 @@ -580,7 +580,7 @@ def test_pre_push_integration_accepted(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) - with cwd(path): + with contextlib.chdir(path): install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory)[0] == 0 @@ -596,11 +596,11 @@ def test_pre_push_force_push_without_fetch(tempdir_factory, store): path2 = tempdir_factory.get() cmd_output('git', 'clone', upstream, path1) cmd_output('git', 'clone', upstream, path2) - with cwd(path1): + with contextlib.chdir(path1): assert _get_commit_output(tempdir_factory)[0] == 0 assert _get_push_output(tempdir_factory)[0] == 0 - with cwd(path2): + with contextlib.chdir(path2): install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory, msg='force!')[0] == 0 @@ -615,7 +615,7 @@ def test_pre_push_new_upstream(tempdir_factory, store): upstream2 = git_dir(tempdir_factory) path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) - with cwd(path): + with contextlib.chdir(path): install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory)[0] == 0 @@ -645,7 +645,7 @@ def test_pre_push_environment_variables(tempdir_factory, store): clone = tempdir_factory.get() cmd_output('git', 'clone', upstream, clone) add_config_to_repo(clone, config) - with cwd(clone): + with contextlib.chdir(clone): install(C.CONFIG_FILE, store, hook_types=['pre-push']) cmd_output('git', 'remote', 'rename', 'origin', 'origin2') @@ -658,7 +658,7 @@ def test_pre_push_integration_empty_push(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) - with cwd(path): + with contextlib.chdir(path): install(C.CONFIG_FILE, store, hook_types=['pre-push']) _get_push_output(tempdir_factory) retc, output = _get_push_output(tempdir_factory) @@ -670,7 +670,7 @@ def test_pre_push_legacy(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) - with cwd(path): + with contextlib.chdir(path): os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: f.write( @@ -759,7 +759,7 @@ def test_post_commit_integration(tempdir_factory, store): ], } write_config(path, config) - with cwd(path): + with contextlib.chdir(path): _get_commit_output(tempdir_factory) assert not os.path.exists('post-commit.tmp') @@ -787,7 +787,7 @@ def test_post_merge_integration(tempdir_factory, store): ], } write_config(path, config) - with cwd(path): + with contextlib.chdir(path): # create a simple diamond of commits for a non-trivial merge open('init', 'a').close() cmd_output('git', 'add', '.') @@ -831,7 +831,7 @@ def test_pre_rebase_integration(tempdir_factory, store): ], } write_config(path, config) - with cwd(path): + with contextlib.chdir(path): install(C.CONFIG_FILE, store, hook_types=['pre-rebase']) open('foo', 'a').close() cmd_output('git', 'add', '.') @@ -871,7 +871,7 @@ def test_post_rewrite_integration(tempdir_factory, store): ], } write_config(path, config) - with cwd(path): + with contextlib.chdir(path): open('init', 'a').close() cmd_output('git', 'add', '.') install(C.CONFIG_FILE, store, hook_types=['post-rewrite']) @@ -903,7 +903,7 @@ def test_post_checkout_integration(tempdir_factory, store): ], } write_config(path, config) - with cwd(path): + with contextlib.chdir(path): cmd_output('git', 'add', '.') git_commit() @@ -936,7 +936,7 @@ def test_skips_post_checkout_unstaged_changes(tempdir_factory, store): }], } write_config(path, config) - with cwd(path): + with contextlib.chdir(path): cmd_output('git', 'add', '.') _get_commit_output(tempdir_factory) @@ -1023,7 +1023,7 @@ def test_pre_merge_commit_integration(tempdir_factory, store): ) path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): ret = install(C.CONFIG_FILE, store, hook_types=['pre-merge-commit']) assert ret == 0 @@ -1040,7 +1040,7 @@ def test_pre_merge_commit_integration(tempdir_factory, store): def test_install_disallow_missing_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): remove_config_from_repo(path) ret = install( C.CONFIG_FILE, store, hook_types=['pre-commit'], @@ -1054,7 +1054,7 @@ def test_install_disallow_missing_config(tempdir_factory, store): def test_install_allow_missing_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): remove_config_from_repo(path) ret = install( C.CONFIG_FILE, store, hook_types=['pre-commit'], @@ -1073,7 +1073,7 @@ def test_install_allow_missing_config(tempdir_factory, store): def test_install_temporarily_allow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): + with contextlib.chdir(path): remove_config_from_repo(path) ret = install( C.CONFIG_FILE, store, hook_types=['pre-commit'], diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 50a20f377..1c7172a6d 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import os.path import shlex import sys @@ -31,7 +32,6 @@ from testing.fixtures import sample_meta_config from testing.fixtures import write_config from testing.util import cmd_output_mocked_pre_commit_home -from testing.util import cwd from testing.util import git_commit from testing.util import run_opts @@ -107,21 +107,21 @@ def test_full_msg_postfix_not_colored(): @pytest.fixture def repo_with_passing_hook(tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(git_path): + with contextlib.chdir(git_path): yield git_path @pytest.fixture def repo_with_failing_hook(tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') - with cwd(git_path): + with contextlib.chdir(git_path): yield git_path @pytest.fixture def aliased_repo(tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(git_path): + with contextlib.chdir(git_path): with modify_config() as config: config['repos'][0]['hooks'].append( {'id': 'bash_hook', 'alias': 'foo_bash'}, @@ -136,7 +136,7 @@ def stage_a_file(filename='foo.py'): def _do_run(cap_out, store, repo, args, environ={}, config_file=C.CONFIG_FILE): - with cwd(repo): # replicates `main._adjust_args_and_chdir` behaviour + with contextlib.chdir(repo): # replicates `main._adjust_args_and_chdir` ret = run(config_file, store, args, environ=environ) printed = cap_out.get_bytes() return ret, printed @@ -175,7 +175,7 @@ def test_run_all_hooks_failing(cap_out, store, repo_with_failing_hook): def test_arbitrary_bytes_hook(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'arbitrary_bytes_repo') - with cwd(git_path): + with contextlib.chdir(git_path): _test_run( cap_out, store, git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True, ) @@ -185,7 +185,7 @@ def test_hook_that_modifies_but_returns_zero(cap_out, store, tempdir_factory): git_path = make_consuming_repo( tempdir_factory, 'modified_file_returns_zero_repo', ) - with cwd(git_path): + with contextlib.chdir(git_path): stage_a_file('bar.py') _test_run( cap_out, @@ -212,7 +212,7 @@ def test_hook_that_modifies_but_returns_zero(cap_out, store, tempdir_factory): def test_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'types_repo') - with cwd(git_path): + with contextlib.chdir(git_path): stage_a_file('bar.py') stage_a_file('bar.notpy') ret, printed = _do_run(cap_out, store, git_path, run_opts()) @@ -223,7 +223,7 @@ def test_types_hook_repository(cap_out, store, tempdir_factory): def test_types_or_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'types_or_repo') - with cwd(git_path): + with contextlib.chdir(git_path): stage_a_file('bar.notpy') stage_a_file('bar.pxd') stage_a_file('bar.py') @@ -236,7 +236,7 @@ def test_types_or_hook_repository(cap_out, store, tempdir_factory): def test_exclude_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'exclude_types_repo') - with cwd(git_path): + with contextlib.chdir(git_path): with open('exe', 'w') as exe: exe.write('#!/usr/bin/env python3\n') make_executable('exe') @@ -335,7 +335,7 @@ def test_show_diff_on_failure( git_path = make_consuming_repo( tempdir_factory, 'modified_file_returns_zero_repo', ) - with cwd(git_path): + with contextlib.chdir(git_path): stage_a_file('bar.py') _test_run( cap_out, store, git_path, args, @@ -406,7 +406,7 @@ def test_run_output_logfile(cap_out, store, tempdir_factory): ) git_path = make_consuming_repo(tempdir_factory, 'logfile_repo') - with cwd(git_path): + with contextlib.chdir(git_path): _test_run( cap_out, store, @@ -695,7 +695,7 @@ def test_hook_id_in_verbose_output(cap_out, store, repo_with_passing_hook): def test_multiple_hooks_same_id(cap_out, store, repo_with_passing_hook): - with cwd(repo_with_passing_hook): + with contextlib.chdir(repo_with_passing_hook): # Add bash hook on there again with modify_config() as config: config['repos'][0]['hooks'].append({'id': 'bash_hook'}) @@ -725,7 +725,7 @@ def test_aliased_hook_run(cap_out, store, aliased_repo): def test_non_ascii_hook_id(repo_with_passing_hook, tempdir_factory): - with cwd(repo_with_passing_hook): + with contextlib.chdir(repo_with_passing_hook): _, stdout, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-m', 'pre_commit.main', 'run', '☃', check=False, tempdir_factory=tempdir_factory, @@ -736,7 +736,7 @@ def test_non_ascii_hook_id(repo_with_passing_hook, tempdir_factory): def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): - with cwd(repo_with_failing_hook): + with contextlib.chdir(repo_with_failing_hook): with modify_config() as config: config['repos'][0]['hooks'][0]['args'] = ['☃'] stage_a_file() @@ -758,7 +758,7 @@ def test_lots_of_files(store, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for # our workaround git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(git_path): + with contextlib.chdir(git_path): # Override files so we run against them with modify_config() as config: config['repos'][0]['hooks'][0]['files'] = '' @@ -873,7 +873,7 @@ def test_post_checkout_hook(cap_out, store, tempdir_factory): } add_config_to_repo(path, config) - with cwd(path): + with contextlib.chdir(path): _test_run( cap_out, store, @@ -1024,12 +1024,12 @@ def test_no_unstaged_error_with_all_files_or_files( def test_files_running_subdir(repo_with_passing_hook, tempdir_factory): - with cwd(repo_with_passing_hook): + with contextlib.chdir(repo_with_passing_hook): os.mkdir('subdir') open('subdir/foo.py', 'w').close() cmd_output('git', 'add', 'subdir/foo.py') - with cwd('subdir'): + with contextlib.chdir('subdir'): # Use subprocess to demonstrate behaviour in main _, stdout, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-m', 'pre_commit.main', 'run', '-v', @@ -1104,26 +1104,43 @@ def test_fail_fast_not_prev_failures(cap_out, store, repo_with_failing_hook): assert printed.count(b'run me!') == 1 +def test_fail_fast_run_arg(cap_out, store, repo_with_failing_hook): + with modify_config() as config: + # More than one hook to demonstrate early exit + config['repos'][0]['hooks'] *= 2 + stage_a_file() + + ret, printed = _do_run( + cap_out, store, repo_with_failing_hook, run_opts(fail_fast=True), + ) + # it should have only run one hook due to the CLI flag + assert printed.count(b'Failing hook') == 1 + + def test_classifier_removes_dne(): classifier = Classifier(('this_file_does_not_exist',)) assert classifier.filenames == [] def test_classifier_normalizes_filenames_on_windows_to_forward_slashes(tmpdir): - with tmpdir.as_cwd(): - tmpdir.join('a/b/c').ensure() - with mock.patch.object(os, 'altsep', '/'): - with mock.patch.object(os, 'sep', '\\'): - classifier = Classifier.from_config((r'a\b\c',), '', '^$') - assert classifier.filenames == ['a/b/c'] + tmpdir.join('a/b/c').ensure() + with ( + tmpdir.as_cwd(), + mock.patch.object(os, 'altsep', '/'), + mock.patch.object(os, 'sep', '\\'), + ): + classifier = Classifier.from_config((r'a\b\c',), '', '^$') + assert classifier.filenames == ['a/b/c'] def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir): - with mock.patch.object(os.path, 'lexists', return_value=True): - with mock.patch.object(os, 'altsep', None): - with mock.patch.object(os, 'sep', '/'): - classifier = Classifier.from_config((r'a/b\c',), '', '^$') - assert classifier.filenames == [r'a/b\c'] + with ( + mock.patch.object(os.path, 'lexists', return_value=True), + mock.patch.object(os, 'altsep', None), + mock.patch.object(os, 'sep', '/'), + ): + classifier = Classifier.from_config((r'a/b\c',), '', '^$') + assert classifier.filenames == [r'a/b\c'] def test_classifier_empty_types_or(tmpdir): diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index c5f891ea7..c03830780 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import os.path import re import time @@ -14,7 +15,6 @@ from testing.fixtures import git_dir from testing.fixtures import make_repo from testing.fixtures import modify_manifest -from testing.util import cwd from testing.util import git_commit from testing.util import run_opts @@ -37,7 +37,7 @@ def _add_test_file(): def _run_try_repo(tempdir_factory, **kwargs): repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') - with cwd(git_dir(tempdir_factory)): + with contextlib.chdir(git_dir(tempdir_factory)): _add_test_file() assert not try_repo(try_repo_opts(repo, **kwargs)) @@ -91,7 +91,7 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): def test_try_repo_relative_path(cap_out, tempdir_factory): repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') - with cwd(git_dir(tempdir_factory)): + with contextlib.chdir(git_dir(tempdir_factory)): _add_test_file() relative_repo = os.path.relpath(repo, '.') # previously crashed on cloning a relative path @@ -100,7 +100,7 @@ def test_try_repo_relative_path(cap_out, tempdir_factory): def test_try_repo_bare_repo(cap_out, tempdir_factory): repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') - with cwd(git_dir(tempdir_factory)): + with contextlib.chdir(git_dir(tempdir_factory)): _add_test_file() bare_repo = os.path.join(repo, '.git') # previously crashed attempting modification changes @@ -111,7 +111,7 @@ def test_try_repo_specific_revision(cap_out, tempdir_factory): repo = make_repo(tempdir_factory, 'script_hooks_repo') ref = git.head_rev(repo) git_commit(cwd=repo) - with cwd(git_dir(tempdir_factory)): + with contextlib.chdir(git_dir(tempdir_factory)): _add_test_file() assert not try_repo(try_repo_opts(repo, ref=ref)) @@ -125,7 +125,7 @@ def test_try_repo_uncommitted_changes(cap_out, tempdir_factory): with modify_manifest(repo, commit=False) as manifest: manifest[0]['name'] = 'modified name!' - with cwd(git_dir(tempdir_factory)): + with contextlib.chdir(git_dir(tempdir_factory)): open('test-fie', 'a').close() cmd_output('git', 'add', '.') assert not try_repo(try_repo_opts(repo)) @@ -146,10 +146,10 @@ def test_try_repo_uncommitted_changes(cap_out, tempdir_factory): def test_try_repo_staged_changes(tempdir_factory): repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') - with cwd(repo): + with contextlib.chdir(repo): open('staged-file', 'a').close() open('second-staged-file', 'a').close() cmd_output('git', 'add', '.') - with cwd(git_dir(tempdir_factory)): + with contextlib.chdir(git_dir(tempdir_factory)): assert not try_repo(try_repo_opts(repo, hook='bash_hook')) diff --git a/tests/conftest.py b/tests/conftest.py index 8c9cd14db..7bf720ffe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import functools import io import os.path @@ -16,7 +17,6 @@ from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import write_config -from testing.util import cwd from testing.util import git_commit @@ -38,7 +38,7 @@ def get(self): @pytest.fixture def in_tmpdir(tempdir_factory): path = tempdir_factory.get() - with cwd(path): + with contextlib.chdir(path): yield path @@ -79,7 +79,7 @@ def in_merge_conflict(tempdir_factory): conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) - with cwd(conflict_path): + with contextlib.chdir(conflict_path): _make_conflict() yield os.path.join(conflict_path) @@ -90,7 +90,7 @@ def in_conflicting_submodule(tempdir_factory): git_dir_2 = git_dir(tempdir_factory) git_commit(msg=in_conflicting_submodule.__name__, cwd=git_dir_2) cmd_output('git', 'submodule', 'add', git_dir_2, 'sub', cwd=git_dir_1) - with cwd(os.path.join(git_dir_1, 'sub')): + with contextlib.chdir(os.path.join(git_dir_1, 'sub')): _make_conflict() yield @@ -109,7 +109,7 @@ def commit_msg_repo(tempdir_factory): }], } write_config(path, config) - with cwd(path): + with contextlib.chdir(path): cmd_output('git', 'add', '.') git_commit(msg=commit_msg_repo.__name__) yield path @@ -130,7 +130,7 @@ def prepare_commit_msg_repo(tempdir_factory): }], } write_config(path, config) - with cwd(path): + with contextlib.chdir(path): with open(script_name, 'w') as script_file: script_file.write( '#!/usr/bin/env bash\n' @@ -157,7 +157,7 @@ def failing_prepare_commit_msg_repo(tempdir_factory): }], } write_config(path, config) - with cwd(path): + with contextlib.chdir(path): cmd_output('git', 'add', '.') git_commit(msg=failing_prepare_commit_msg_repo.__name__) yield path diff --git a/tests/git_test.py b/tests/git_test.py index 02b6ce3ae..763aa0de5 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -225,6 +225,17 @@ def test_all_files_non_ascii(non_ascii_repo): assert ret == ['интервью'] +def test_all_files_merge_conflict(in_merge_conflict): + ret = git.get_all_files() + assert sorted(ret) == sorted([ + '.pre-commit-config.yaml', + 'bar_only_file', + 'conflict_file', + 'foo_only_file', + 'placeholder', + ]) + + def test_staged_files_non_ascii(non_ascii_repo): non_ascii_repo.join('интервью').write('hi') cmd_output('git', 'add', '.') diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py index da289aef8..9fac83da2 100644 --- a/tests/lang_base_test.py +++ b/tests/lang_base_test.py @@ -164,3 +164,15 @@ def test_basic_run_hook(tmp_path): assert ret == 0 out = out.replace(b'\r\n', b'\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', + ) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 03235c46b..e269976f7 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -14,40 +14,173 @@ from testing.language_helpers import run_language from testing.util import xfailif_windows -DOCKER_CGROUP_EXAMPLE = b'''\ -12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -7:rdma:/ -6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -0::/system.slice/containerd.service +DOCKER_CGROUPS_V1_MOUNTINFO_EXAMPLE = b'''\ +759 717 0:52 / / rw,relatime master:300 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/PCPE5P5IVGM7CFCPJR353N3ONK:/var/lib/docker/overlay2/l/EQFSDHFAJ333VEMEJD4ZTRIZCB,upperdir=/var/lib/docker/overlay2/0d9f6bf186030d796505b87d6daa92297355e47641e283d3c09d83a7f221e462/diff,workdir=/var/lib/docker/overlay2/0d9f6bf186030d796505b87d6daa92297355e47641e283d3c09d83a7f221e462/work +760 759 0:58 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +761 759 0:59 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +762 761 0:60 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +763 759 0:61 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +764 763 0:62 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755,inode64 +765 764 0:29 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd +766 764 0:32 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,rdma +767 764 0:33 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,cpu,cpuacct +768 764 0:34 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,cpuset +769 764 0:35 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,pids +770 764 0:36 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,memory +771 764 0:37 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,perf_event +772 764 0:38 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,net_cls,net_prio +773 764 0:39 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,blkio +774 764 0:40 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,misc +775 764 0:41 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,hugetlb +776 764 0:42 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,devices +777 764 0:43 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,freezer +778 761 0:57 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +779 761 0:63 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64 +780 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro +781 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hostname /etc/hostname rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro +782 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hosts /etc/hosts rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro +718 761 0:60 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +719 760 0:58 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +720 760 0:58 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +721 760 0:58 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +722 760 0:58 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +723 760 0:58 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +724 760 0:64 / /proc/asound ro,relatime - tmpfs tmpfs ro,inode64 +725 760 0:65 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64 +726 760 0:59 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +727 760 0:59 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +728 760 0:59 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +729 760 0:66 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64 +730 763 0:67 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64 +731 763 0:68 / /sys/devices/virtual/powercap ro,relatime - tmpfs tmpfs ro,inode64 +''' # noqa: E501 + +DOCKER_CGROUPS_V2_MOUNTINFO_EXAMPLE = b'''\ +721 386 0:45 / / rw,relatime master:218 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/QHZ7OM7P4AQD3XLG274ZPWAJCV:/var/lib/docker/overlay2/l/5RFG6SZWVGOG2NKEYXJDQCQYX5,upperdir=/var/lib/docker/overlay2/e4ad859fc5d4791932b9b976052f01fb0063e01de3cef916e40ae2121f6a166e/diff,workdir=/var/lib/docker/overlay2/e4ad859fc5d4791932b9b976052f01fb0063e01de3cef916e40ae2121f6a166e/work,nouserxattr +722 721 0:48 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +723 721 0:50 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +724 723 0:51 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +725 721 0:52 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +726 725 0:26 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,nsdelegate,memory_recursiveprot +727 723 0:47 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +728 723 0:53 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64 +729 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro +730 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hostname /etc/hostname rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro +731 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hosts /etc/hosts rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro +387 723 0:51 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +388 722 0:48 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +389 722 0:48 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +525 722 0:48 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +526 722 0:48 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +571 722 0:48 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +572 722 0:57 / /proc/asound ro,relatime - tmpfs tmpfs ro,inode64 +575 722 0:58 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64 +576 722 0:50 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +577 722 0:50 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +578 722 0:50 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +579 722 0:59 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64 +580 725 0:60 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64 +''' # noqa: E501 + +PODMAN_CGROUPS_V1_MOUNTINFO_EXAMPLE = b'''\ +1200 915 0:57 / / rw,relatime - overlay overlay rw,lowerdir=/home/asottile/.local/share/containers/storage/overlay/l/ZWAU3VY3ZHABQJRBUAFPBX7R5D,upperdir=/home/asottile/.local/share/containers/storage/overlay/72504ef163fda63838930450553b7306412ccad139a007626732b3dc43af5200/diff,workdir=/home/asottile/.local/share/containers/storage/overlay/72504ef163fda63838930450553b7306412ccad139a007626732b3dc43af5200/work,volatile,userxattr +1204 1200 0:62 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +1205 1200 0:63 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,uid=1000,gid=1000,inode64 +1206 1200 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw +1207 1205 0:65 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 +1208 1205 0:61 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +1209 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/.containerenv /run/.containerenv rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1210 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1211 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hosts /etc/hosts rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1212 1205 0:56 / /dev/shm rw,relatime - tmpfs shm rw,size=64000k,uid=1000,gid=1000,inode64 +1213 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hostname /etc/hostname rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1214 1206 0:66 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs cgroup rw,size=1024k,uid=1000,gid=1000,inode64 +1215 1214 0:43 / /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer +1216 1214 0:42 /user.slice /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices +1217 1214 0:41 / /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,hugetlb +1218 1214 0:40 / /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,misc +1219 1214 0:39 / /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio +1220 1214 0:38 / /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls,net_prio +1221 1214 0:37 / /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,perf_event +1222 1214 0:36 /user.slice/user-1000.slice/user@1000.service /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory +1223 1214 0:35 /user.slice/user-1000.slice/user@1000.service /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,pids +1224 1214 0:34 / /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset +1225 1214 0:33 / /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu,cpuacct +1226 1214 0:32 / /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,rdma +1227 1214 0:29 /user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-0c50448e-b395-4d76-8b92-379f16e5066f.scope /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,xattr,name=systemd +1228 1205 0:5 /null /dev/null rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1229 1205 0:5 /zero /dev/zero rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1230 1205 0:5 /full /dev/full rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1231 1205 0:5 /tty /dev/tty rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1232 1205 0:5 /random /dev/random rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1233 1205 0:5 /urandom /dev/urandom rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1234 1204 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1235 1204 0:5 /null /proc/kcore rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1236 1204 0:5 /null /proc/keys rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1237 1204 0:5 /null /proc/timer_list rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1238 1204 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1239 1206 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1240 1206 0:70 / /sys/dev/block ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1241 1204 0:62 /asound /proc/asound ro,relatime - proc proc rw +1242 1204 0:62 /bus /proc/bus ro,relatime - proc proc rw +1243 1204 0:62 /fs /proc/fs ro,relatime - proc proc rw +1244 1204 0:62 /irq /proc/irq ro,relatime - proc proc rw +1245 1204 0:62 /sys /proc/sys ro,relatime - proc proc rw +1256 1204 0:62 /sysrq-trigger /proc/sysrq-trigger ro,relatime - proc proc rw +916 1205 0:65 /0 /dev/console rw,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 +''' # noqa: E501 + +PODMAN_CGROUPS_V2_MOUNTINFO_EXAMPLE = b'''\ +685 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +686 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hosts /etc/hosts rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +687 692 0:50 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=64000k,uid=1000,gid=1000,inode64 +688 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/.containerenv /run/.containerenv rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +689 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hostname /etc/hostname rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +690 546 0:55 / / rw,relatime - overlay overlay rw,lowerdir=/home/asottile/.local/share/containers/storage/overlay/l/NPOHYOD3PI3YW6TQSGBOVOUSK6,upperdir=/home/asottile/.local/share/containers/storage/overlay/565c206fb79f876ffd5f069b8bd7a97fb5e47d5d07396b0c395a4ed6725d4a8e/diff,workdir=/home/asottile/.local/share/containers/storage/overlay/565c206fb79f876ffd5f069b8bd7a97fb5e47d5d07396b0c395a4ed6725d4a8e/work,redirect_dir=nofollow,uuid=on,volatile,userxattr +691 690 0:59 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +692 690 0:61 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,uid=1000,gid=1000,inode64 +693 690 0:62 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw +694 692 0:66 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 +695 692 0:58 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +696 693 0:28 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot +698 692 0:6 /null /dev/null rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +699 692 0:6 /zero /dev/zero rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +700 692 0:6 /full /dev/full rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +701 692 0:6 /tty /dev/tty rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +702 692 0:6 /random /dev/random rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +703 692 0:6 /urandom /dev/urandom rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +704 691 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +705 691 0:6 /null /proc/kcore ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +706 691 0:6 /null /proc/keys ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +707 691 0:6 /null /proc/latency_stats ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +708 691 0:6 /null /proc/timer_list ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +709 691 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +710 693 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +711 693 0:70 / /sys/dev/block ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +712 693 0:71 / /sys/devices/virtual/powercap ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +713 691 0:59 /asound /proc/asound ro,nosuid,nodev,noexec,relatime - proc proc rw +714 691 0:59 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +715 691 0:59 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +716 691 0:59 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +717 691 0:59 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +718 691 0:59 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +547 692 0:66 /0 /dev/console rw,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 ''' # noqa: E501 # The ID should match the above cgroup example. CONTAINER_ID = 'c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7' # noqa: E501 -NON_DOCKER_CGROUP_EXAMPLE = b'''\ -12:perf_event:/ -11:hugetlb:/ -10:devices:/ -9:blkio:/ -8:rdma:/ -7:cpuset:/ -6:cpu,cpuacct:/ -5:freezer:/ -4:memory:/ -3:pids:/ -2:net_cls,net_prio:/ -1:name=systemd:/init.scope -0::/init.scope -''' +NON_DOCKER_MOUNTINFO_EXAMPLE = b'''\ +21 27 0:19 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw +22 27 0:20 / /proc rw,nosuid,nodev,noexec,relatime shared:14 - proc proc rw +23 27 0:5 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=10219484k,nr_inodes=2554871,mode=755,inode64 +24 23 0:21 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +25 27 0:22 / /run rw,nosuid,nodev,noexec,relatime shared:5 - tmpfs tmpfs rw,size=2047768k,mode=755,inode64 +27 1 8:2 / / rw,relatime shared:1 - ext4 /dev/sda2 rw,errors=remount-ro +28 21 0:6 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:8 - securityfs securityfs rw +29 23 0:24 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw,inode64 +30 25 0:25 / /run/lock rw,nosuid,nodev,noexec,relatime shared:6 - tmpfs tmpfs rw,size=5120k,inode64 +''' # noqa: E501 def test_docker_fallback_user(): @@ -89,7 +222,8 @@ def test_docker_user_rootless(info_ret): ( (0, b'{"SecurityOptions": ["name=cgroupns"]}', b''), (0, b'{"host": {"security": {"rootless": false}}}', b''), - (0, b'{"respone_from_some_other_container_engine": true}', b''), + (0, b'{"response_from_some_other_container_engine": true}', b''), + (0, b'{"SecurityOptions": null}', b''), (1, b'', b''), ), ) @@ -98,9 +232,9 @@ def test_docker_user_non_rootless(info_ret): assert docker.get_docker_user() != () -def test_in_docker_no_file(): +def test_container_id_no_file(): with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError): - assert docker._is_in_docker() is False + assert docker._get_container_id() is None def _mock_open(data): @@ -112,38 +246,33 @@ def _mock_open(data): ) -def test_in_docker_docker_in_file(): - with _mock_open(DOCKER_CGROUP_EXAMPLE): - assert docker._is_in_docker() is True - - -def test_in_docker_docker_not_in_file(): - with _mock_open(NON_DOCKER_CGROUP_EXAMPLE): - assert docker._is_in_docker() is False +def test_container_id_not_in_file(): + with _mock_open(NON_DOCKER_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() is None def test_get_container_id(): - with _mock_open(DOCKER_CGROUP_EXAMPLE): + with _mock_open(DOCKER_CGROUPS_V1_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + with _mock_open(DOCKER_CGROUPS_V2_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + with _mock_open(PODMAN_CGROUPS_V1_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + with _mock_open(PODMAN_CGROUPS_V2_MOUNTINFO_EXAMPLE): assert docker._get_container_id() == CONTAINER_ID - - -def test_get_container_id_failure(): - with _mock_open(b''), pytest.raises(RuntimeError): - docker._get_container_id() def test_get_docker_path_not_in_docker_returns_same(): - with mock.patch.object(docker, '_is_in_docker', return_value=False): + with _mock_open(b''): assert docker._get_docker_path('abc') == 'abc' @pytest.fixture def in_docker(): - with mock.patch.object(docker, '_is_in_docker', return_value=True): - with mock.patch.object( - docker, '_get_container_id', return_value=CONTAINER_ID, - ): - yield + with mock.patch.object( + docker, '_get_container_id', return_value=CONTAINER_ID, + ): + yield def _linux_commonpath(): diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 7fb6ab18b..3a1c83ae1 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib from unittest import mock import pytest @@ -17,7 +18,6 @@ from testing.fixtures import make_config_from_repo from testing.language_helpers import run_language from testing.util import cmd_output_mocked_pre_commit_home -from testing.util import cwd from testing.util import git_commit @@ -223,7 +223,7 @@ def test_automatic_toolchain_switching_go_fmt(tmp_path, monkeypatch): main_file = test_dir.joinpath('main.go') main_file.write_text(main_go) - with cwd(test_dir): + with contextlib.chdir(test_dir): ret, out = run_language( path=hook_dir, language=golang, diff --git a/tests/languages/julia_test.py b/tests/languages/julia_test.py index 175622d65..cf9dba447 100644 --- a/tests/languages/julia_test.py +++ b/tests/languages/julia_test.py @@ -1,11 +1,13 @@ from __future__ import annotations +import contextlib import os from unittest import mock +from pre_commit import constants as C from pre_commit.languages import julia +from pre_commit.prefix import Prefix from testing.language_helpers import run_language -from testing.util import cwd def _make_hook(tmp_path, julia_code): @@ -42,6 +44,19 @@ def test_julia_hook_with_startup(tmp_path): test_julia_hook(tmp_path) +def test_julia_hook_installs_into_env_local_depot(tmp_path): + code = """ + using Example + println("Hello, world!") + """ + _make_hook(tmp_path, code) + + julia.install_environment(Prefix(str(tmp_path)), C.DEFAULT, ()) + + d = tmp_path.joinpath('juliaenv-default', 'depot', 'packages', 'Example') + assert d.is_dir() + + def test_julia_hook_manifest(tmp_path): code = """ using Example @@ -102,7 +117,7 @@ def test_julia_repo_local(tmp_path): local_dir.joinpath('local.jl').write_text( 'using TOML; foreach(println, ARGS)', ) - with cwd(local_dir): + with contextlib.chdir(local_dir): deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',) expected = (0, b'--local-arg1\n--local-arg2\n') assert run_language( diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index 055cb1e92..b2df9f6a3 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -15,7 +15,9 @@ from pre_commit.prefix import Prefix from pre_commit.store import _make_local_repo from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from testing.language_helpers import run_language +from testing.util import git_commit from testing.util import xfailif_windows @@ -40,6 +42,12 @@ def find_exe_mck(): yield mck +def _make_repo(r): + cmd_output_b('git', 'init', r) + cmd_output_b('git', 'add', '.', cwd=r) + git_commit(cwd=r) + + @pytest.mark.usefixtures('is_linux') def test_sets_system_when_node_and_npm_are_available(find_exe_mck): find_exe_mck.return_value = '/path/to/exe' @@ -61,6 +69,7 @@ def test_sets_default_on_windows(find_exe_mck): @xfailif_windows # pragma: win32 no cover def test_healthy_system_node(tmpdir): tmpdir.join('package.json').write('{"name": "t", "version": "1.0.0"}') + _make_repo(str(tmpdir)) prefix = Prefix(str(tmpdir)) node.install_environment(prefix, 'system', ()) @@ -75,6 +84,7 @@ def test_unhealthy_if_system_node_goes_missing(tmpdir): prefix_dir = tmpdir.join('prefix').ensure_dir() prefix_dir.join('package.json').write('{"name": "t", "version": "1.0.0"}') + _make_repo(str(prefix_dir)) path = ('PATH', (str(bin_dir), os.pathsep, envcontext.Var('PATH'))) with envcontext.envcontext((path,)): @@ -101,6 +111,7 @@ def test_installs_without_links_outside_env(tmpdir): 'dependencies': {'lodash': '*'}, }), ) + _make_repo(str(tmpdir)) prefix = Prefix(str(tmpdir)) node.install_environment(prefix, 'system', ()) @@ -124,6 +135,7 @@ def _make_hello_world(tmp_path): '#!/usr/bin/env node\n' 'console.log("Hello World");\n', ) + _make_repo(str(tmp_path)) def test_node_hook_system(tmp_path): @@ -132,6 +144,29 @@ def test_node_hook_system(tmp_path): assert ret == (0, b'Hello World\n') +def test_node_hook_install_does_not_write_callers_index(tmp_path): + # git sets GIT_DIR / GIT_INDEX_FILE to absolute paths when running hooks + # in a submodule, `npm install git+file://...` must not inherit them + hook_repo = tmp_path.joinpath('hook') + hook_repo.mkdir() + _make_hello_world(hook_repo) + + user_repo = tmp_path.joinpath('user') + cmd_output_b('git', 'init', user_repo) + user_repo.joinpath('f').touch() + cmd_output_b('git', 'add', 'f', cwd=user_repo) + + git_dir = user_repo.joinpath('.git') + with envcontext.envcontext(( + ('GIT_DIR', str(git_dir)), + ('GIT_INDEX_FILE', str(git_dir.joinpath('index'))), + )): + ret = run_language(hook_repo, node, 'node-hello') + assert ret == (0, b'Hello World\n') + + assert cmd_output('git', 'ls-files', cwd=user_repo)[1] == 'f\n' + + def test_node_with_user_config_set(tmp_path): cfg = tmp_path.joinpath('cfg') cfg.write_text('cache=/dne\n') @@ -146,6 +181,21 @@ def test_node_hook_versions(tmp_path, version): assert ret == (0, b'Hello World\n') +def test_npm_install_succeeds_with_build(tmp_path): + _make_hello_world(tmp_path) + + with open(tmp_path.joinpath('package.json')) as f: + contents = json.load(f) + contents['scripts'] = {'build': 'echo hello hello'} + with open(tmp_path.joinpath('package.json'), 'w') as f: + json.dump(contents, f) + _make_repo(tmp_path) + + # node version chosen to be npm 11.19.0 with this particular bug + ret = run_language(tmp_path, node, 'node-hello', version='26.7.0') + assert ret == (0, b'Hello World\n') + + def test_node_additional_deps(tmp_path): _make_local_repo(str(tmp_path)) ret, out = run_language(tmp_path, node, 'npm ls -g', deps=('lodash',)) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 565525a40..593634b79 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -10,6 +10,8 @@ from pre_commit.envcontext import envcontext from pre_commit.languages import python 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 win_exe 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]) 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 ') diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 5d767b25d..c030a9af6 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import tarfile from unittest import mock @@ -12,7 +13,6 @@ from pre_commit.languages.ruby import _resource_bytesio from pre_commit.store import _make_local_repo from testing.language_helpers import run_language -from testing.util import cwd from testing.util import xfailif_windows @@ -134,6 +134,6 @@ def test_ruby_with_bundle_disable_shared_gems(tmp_path): 'BUNDLE_PATH: vendor/gem\n', ) - with cwd(workdir): + with contextlib.chdir(workdir): # `3.2.0` has new enough `gem` reading `.bundle` test_ruby_hook_language_version(tmp_path) diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py index 52e356134..241f7a61a 100644 --- a/tests/languages/rust_test.py +++ b/tests/languages/rust_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib from unittest import mock import pytest @@ -9,7 +10,6 @@ from pre_commit.languages import rust from pre_commit.store import _make_local_repo from testing.language_helpers import run_language -from testing.util import cwd ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__ @@ -34,7 +34,7 @@ def test_selects_system_even_if_rust_toolchain_toml(tmp_path): toolchain_toml = '[toolchain]\nchannel = "wtf"\n' tmp_path.joinpath('rust-toolchain.toml').write_text(toolchain_toml) - with cwd(tmp_path): + with contextlib.chdir(tmp_path): assert ACTUAL_GET_DEFAULT_VERSION() == 'system' diff --git a/tests/languages/system_test.py b/tests/languages/system_test.py deleted file mode 100644 index dcd9cf1e0..000000000 --- a/tests/languages/system_test.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -from pre_commit.languages import system -from testing.language_helpers import run_language - - -def test_system_language(tmp_path): - expected = (0, b'hello hello world\n') - assert run_language(tmp_path, system, 'echo hello hello world') == expected diff --git a/tests/languages/script_test.py b/tests/languages/unsupported_script_test.py similarity index 63% rename from tests/languages/script_test.py rename to tests/languages/unsupported_script_test.py index a02f615a9..b15b67e76 100644 --- a/tests/languages/script_test.py +++ b/tests/languages/unsupported_script_test.py @@ -1,14 +1,14 @@ from __future__ import annotations -from pre_commit.languages import script +from pre_commit.languages import unsupported_script from pre_commit.util import make_executable from testing.language_helpers import run_language -def test_script_language(tmp_path): +def test_unsupported_script_language(tmp_path): exe = tmp_path.joinpath('main') exe.write_text('#!/usr/bin/env bash\necho hello hello world\n') make_executable(exe) expected = (0, b'hello hello world\n') - assert run_language(tmp_path, script, 'main') == expected + assert run_language(tmp_path, unsupported_script, 'main') == expected diff --git a/tests/languages/unsupported_test.py b/tests/languages/unsupported_test.py new file mode 100644 index 000000000..7f8461e02 --- /dev/null +++ b/tests/languages/unsupported_test.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from pre_commit.languages import unsupported +from testing.language_helpers import run_language + + +def test_unsupported_language(tmp_path): + expected = (0, b'hello hello world\n') + ret = run_language(tmp_path, unsupported, 'echo hello hello world') + assert ret == expected diff --git a/tests/main_test.py b/tests/main_test.py index 945349fa4..973c6e830 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import contextlib import os.path from unittest import mock @@ -8,10 +9,10 @@ import pre_commit.constants as C from pre_commit import main +from pre_commit.commands import hazmat from pre_commit.errors import FatalError from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple -from testing.util import cwd def _args(**kwargs): @@ -62,7 +63,7 @@ def test_install_on_subst(in_git_dir, store): # pragma: posix no cover assert not os.path.exists('Z:') cmd_output('subst', 'Z:', str(in_git_dir)) try: - with cwd('Z:'): + with contextlib.chdir('Z:'): test_adjust_args_and_chdir_noop('Z:\\') finally: cmd_output('subst', '/d', 'Z:') @@ -88,7 +89,7 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): FNS = ( - 'autoupdate', 'clean', 'gc', 'hook_impl', 'install', 'install_hooks', + 'autoupdate', 'clean', 'gc', 'install', 'install_hooks', 'migrate_config', 'run', 'sample_config', 'uninstall', 'validate_config', 'validate_manifest', ) @@ -97,11 +98,9 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): @pytest.fixture def mock_commands(): - mcks = {fn: mock.patch.object(main, fn).start() for fn in FNS} - ret = auto_namedtuple(**mcks) - yield ret - for mck in ret: - mck.stop() + with contextlib.ExitStack() as ctx: + mcks = {f: ctx.enter_context(mock.patch.object(main, f)) for f in FNS} + yield auto_namedtuple(**mcks) @pytest.fixture @@ -158,6 +157,23 @@ def test_all_cmds(command, mock_commands, mock_store_dir): 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_hook_impl(mock_store_dir): + with mock.patch.object(main, 'hook_impl') as mck: + main.main(('hook-impl', '--hook-type', 'pre-commit')) + assert mck.call_count == 1 + + def test_try_repo(mock_store_dir): with mock.patch.object(main, 'try_repo') as patch: main.main(('try-repo', '.')) diff --git a/tests/repository_test.py b/tests/repository_test.py index b54c910d3..4295340e9 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import os.path import shlex import shutil @@ -17,7 +18,7 @@ from pre_commit.clientlib import load_manifest from pre_commit.hook import Hook from pre_commit.languages import python -from pre_commit.languages import system +from pre_commit.languages import unsupported from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -27,7 +28,6 @@ from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.language_helpers import run_language -from testing.util import cwd from testing.util import get_resource_path @@ -80,13 +80,6 @@ def _test_hook_repo( assert out == expected -def test_system_hook_with_spaces(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'system_hook_with_spaces_repo', - 'system-hook-with-spaces', [os.devnull], b'Hello World\n', - ) - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -246,14 +239,16 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # To simulate a killed install, we'll make PythonEnv.run raise ^C # and then to simulate a second ^C during cleanup, we'll make shutil.rmtree # raise as well. - with pytest.raises(MyKeyboardInterrupt): - with mock.patch.object( - lang_base, 'setup_cmd', side_effect=MyKeyboardInterrupt, - ): - with mock.patch.object( + with ( + pytest.raises(MyKeyboardInterrupt), + mock.patch.object( + lang_base, 'setup_cmd', side_effect=MyKeyboardInterrupt, + ), + mock.patch.object( shutil, 'rmtree', side_effect=MyKeyboardInterrupt, - ): - install_hook_envs(hooks, store) + ), + ): + install_hook_envs(hooks, store) # Should have made an environment, however this environment is broken! hook, = hooks @@ -305,7 +300,7 @@ def test_really_long_file_paths(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - with cwd(really_long_path): + with contextlib.chdir(really_long_path): _get_hook(config, store, 'foo') @@ -431,7 +426,7 @@ def test_manifest_hooks(tempdir_factory, store): exclude_types=[], files='', id='bash_hook', - language='script', + language='unsupported_script', language_version='default', log_file='', minimum_pre_commit_version='0', @@ -464,7 +459,7 @@ def test_non_installable_hook_error_for_language_version(store, caplog): 'hooks': [{ 'id': 'system-hook', 'name': 'system-hook', - 'language': 'system', + 'language': 'unsupported', 'entry': 'python3 -c "import sys; print(sys.version)"', 'language_version': 'python3.10', }], @@ -476,7 +471,7 @@ def test_non_installable_hook_error_for_language_version(store, caplog): msg, = caplog.messages assert msg == ( 'The hook `system-hook` specifies `language_version` but is using ' - 'language `system` which does not install an environment. ' + 'language `unsupported` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) @@ -487,7 +482,7 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): 'hooks': [{ 'id': 'system-hook', 'name': 'system-hook', - 'language': 'system', + 'language': 'unsupported', 'entry': 'python3 -c "import sys; print(sys.version)"', 'additional_dependencies': ['astpretty'], }], @@ -499,17 +494,28 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): msg, = caplog.messages assert msg == ( 'The hook `system-hook` specifies `additional_dependencies` but is ' - 'using language `system` which does not install an environment. ' + 'using language `unsupported` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) def test_args_with_spaces_and_quotes(tmp_path): ret = run_language( - tmp_path, system, + tmp_path, unsupported, 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) + + +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) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index cd2f63870..215cd8b9c 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -14,7 +14,6 @@ from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import git_dir -from testing.util import cwd from testing.util import get_resource_path from testing.util import git_commit from testing.util import xfailif_windows @@ -189,7 +188,7 @@ def test_img_conflict(img_staged, patch_dir): @pytest.fixture def repo_with_commits(tempdir_factory): path = git_dir(tempdir_factory) - with cwd(path): + with contextlib.chdir(path): open('foo', 'a+').close() cmd_output('git', 'add', 'foo') git_commit() @@ -206,7 +205,7 @@ def checkout_submodule(rev): @pytest.fixture def sub_staged(repo_with_commits, tempdir_factory): path = git_dir(tempdir_factory) - with cwd(path): + with contextlib.chdir(path): open('bar', 'a+').close() cmd_output('git', 'add', 'bar') git_commit() @@ -224,7 +223,7 @@ def sub_staged(repo_with_commits, tempdir_factory): def _test_sub_state(path, rev='rev1', status='A'): assert os.path.exists(path.sub_path) - with cwd(path.sub_path): + with contextlib.chdir(path.sub_path): actual_rev = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() assert actual_rev == getattr(path.submodule, rev) actual_status = get_short_git_status()['sub'] diff --git a/tests/store_test.py b/tests/store_test.py index 7d4dffb09..65c2f917b 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import logging import os.path import shlex @@ -17,11 +18,21 @@ from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from testing.fixtures import git_dir -from testing.util import cwd from testing.util import git_commit 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(): """There's a session fixture which makes `Store` invariantly raise to prevent writing to the home directory. @@ -70,7 +81,7 @@ def test_store_init(store): def test_clone(store, tempdir_factory, caplog): path = git_dir(tempdir_factory) - with cwd(path): + with contextlib.chdir(path): git_commit() rev = git.head_rev(path) git_commit() @@ -91,7 +102,7 @@ def test_clone(store, tempdir_factory, caplog): assert git.head_rev(ret) == rev # 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): @@ -190,7 +201,7 @@ def test_clone_shallow_failure_fallback_to_complete( caplog, ): path = git_dir(tempdir_factory) - with cwd(path): + with contextlib.chdir(path): git_commit() rev = git.head_rev(path) git_commit() @@ -217,12 +228,12 @@ def fake_shallow_clone(self, *args, **kwargs): assert git.head_rev(ret) == rev # 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): path = git_dir(tempdir_factory) - with cwd(path): + with contextlib.chdir(path): git_commit() cmd_output('git', 'checkout', 'master', '-b', 'branch') git_commit() @@ -265,7 +276,7 @@ def test_mark_config_as_used(store, tmpdir): with tmpdir.as_cwd(): f = tmpdir.join('f').ensure() 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): @@ -275,21 +286,12 @@ def test_mark_config_as_used_idempotent(store, tmpdir): def test_mark_config_as_used_does_not_exist(store): store.mark_config_used('f') - assert store.select_all_configs() == [] - - -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() == [] + assert _select_all_configs(store) == [] 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) @@ -314,7 +316,7 @@ def _chmod_minus_w(p): assert store.readonly # should be skipped due to readonly 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):