From a583922049684eb426c84086d696ba28659d0862 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:30:41 +0000 Subject: [PATCH 1/4] fix: install each --version into its own directory pre-commit shares one hook environment between all projects on a machine that use the same rev. Every run with a different --version replaced the wheel in it, so two projects pinned to different versions reinstalled (and contacted PyPI) on each switch, and a project without --version ran whatever version another project had left behind. A --version is now installed with `pip install --target` into /cpp-linter-hooks/-/ and the hook runs the binary the wheel ships. Versions coexist, each is downloaded once, and later runs use it without network access. Runs without --version keep using the wheel in the environment and never pick a per-version install. --- AGENTS.md | 4 +- README.md | 12 ++- cpp_linter_hooks/util.py | 117 +++++++++++++++++++++---- tests/test_util.py | 178 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 281 insertions(+), 30 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7bef9d3..8d7835c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ A pre-commit hook repo that auto-installs and runs `clang-format` and `clang-tid | File | Purpose | |------|---------| | `pyproject.toml` | Build config, deps (`pip>=26.1`, `filelock>=3.16`), dev deps, entry points | -| `cpp_linter_hooks/util.py` | Installed-wheel check, PyPI version resolution + pip install (prefix matching, offline fallback), all under a `filelock` lock on the hook environment so parallel hook processes install once | +| `cpp_linter_hooks/util.py` | Installed-wheel and per-version install lookup, PyPI version resolution + pip install (prefix matching, offline fallback, `--version` into its own directory), all under a `filelock` lock on the hook environment so parallel hook processes install once | | `cpp_linter_hooks/clang_format.py` | clang-format wrapper (`--verbose`, `--dry-run`) | | `cpp_linter_hooks/clang_tidy.py` | clang-tidy wrapper (`--compile-commands`, missing-database policy, `--jobs`, `--fix`, `--all-lines`, `-quiet` + status-line filter, `--checks` repair, error hints) | | `cpp_linter_hooks/changed_lines.py` | Added line ranges per file from `git diff` (staged changes, or `PRE_COMMIT_FROM_REF...PRE_COMMIT_TO_REF`) | @@ -43,7 +43,7 @@ bash testing/run.sh # Integration tests (full pre-commit pipeline) ## Core Patterns -- **Version resolution**: A wheel already installed in the hook environment (found by its `-.dist-info` directory in site-packages, e.g. from `additional_dependencies`, with its executable in the environment's scripts directory) is used without network access when it matches `--version` (exact or prefix) or when no `--version` is given. Otherwise the version is resolved dynamically from PyPI at runtime. No hardcoded version lists. Prefix match supported (`--version=20` → `20.1.8`). +- **Version resolution**: A wheel already installed in the hook environment (found by its `-.dist-info` directory in site-packages, e.g. from `additional_dependencies`, with its executable in the environment's scripts directory) is used without network access when it matches `--version` (exact or prefix) or when no `--version` is given. With `--version`, a finished per-version install in `/cpp-linter-hooks/-/` (`pip install --target`, `RECORD` present, binary in `/data/bin` or `/bin`) is used next, newest match first; a run without `--version` never uses those. Otherwise the version is resolved dynamically from PyPI at runtime: `--version` installs into its own directory (pre-commit shares one hook environment between all projects with the same `rev`, so versions must not replace each other), no `--version` installs into the environment. No hardcoded version lists. Prefix match supported (`--version=20` → `20.1.8`). - **Return values**: All hooks return `tuple[int, str]` — `(retval, output)`, where `retval` is the tool's exit code (`0` on success, non-zero on failure; `--dry-run` auto-adds `--Werror` so formatting issues fail). - **clang-tidy compile DB**: Auto-detected from `build/`, `out/`, `cmake-build-debug/`, `_build/`, `builddir/`, then `cmake-build-*/`, `build-*/`, `build/*/`, `out/build/*/` (newest wins). Override with `--compile-commands=`, disable with `--no-compile-commands`. If none is found, no `-p`/`--extra-arg*` is given and clang-tidy finds no `compile_commands.json`/`compile_flags.txt` next to the files or in their parents, the hook skips clang-tidy (retval 0, message on stderr), or fails when `CI` is set. - **clang-tidy changed lines**: By default only diagnostics on added lines are reported, via clang-tidy's `--line-filter`, so `-fix` applies only the fixes of those diagnostics (a fix may still edit an unchanged line nearby). Files without added lines are not checked. If any passed file has no changes (`--all-files`), differs on disk from the diffed version (index or `PRE_COMMIT_TO_REF`), or git fails, every line is checked. `--all-lines` or a user `--line-filter` turns this off. Tests: `tests/conftest.py` disables this and the missing-database check by default; use the `real_changed_lines` / `real_compile_db_check` fixtures to test them. diff --git a/README.md b/README.md index d21b793..845dba8 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,14 @@ picks and installs the wheel the first time it runs: give clang-tidy the major version. - clang-format wheels cover LLVM 6 to 23 and clang-tidy wheels LLVM 13 to 23. For a version without a wheel, the hook fails and lists some of the versions that exist. -- When the hook environment already has a wheel that matches `--version` (or any wheel, without - `--version`), the hook uses it and does not contact pypi.org. Otherwise it looks the version up - on pypi.org and installs it, so that run needs network access. pre-commit runs the hook on - groups of files in parallel; one of them installs the wheel and the others wait for it. +- Each `--version` is installed once, into its own directory in the hook environment, and later + runs use it without contacting pypi.org. pre-commit shares the hook environment between all + your projects that use the same `rev`, so projects that ask for different versions keep their + own copy instead of reinstalling. A wheel from `additional_dependencies` that matches + `--version` is used as well. +- The first run with a new `--version`, or without any version, looks the version up on pypi.org + and installs it, so that run needs network access. pre-commit runs the hook on groups of files + in parallel; one of them installs the wheel and the others wait for it. ### Install the tool with the hook diff --git a/cpp_linter_hooks/util.py b/cpp_linter_hooks/util.py index d129cb2..978702d 100644 --- a/cpp_linter_hooks/util.py +++ b/cpp_linter_hooks/util.py @@ -156,6 +156,11 @@ def _env_paths() -> dict[str, str]: return sysconfig.get_paths() +def _dist_name(tool: str) -> str: + """Return the normalized distribution name, as in ``clang_format``.""" + return re.sub(r"[-_.]+", "_", tool).lower() + + def _installed_wheel_version(tool: str) -> str | None: """Return the version of the *tool* wheel installed in this Python environment. @@ -167,7 +172,7 @@ def _installed_wheel_version(tool: str) -> str | None: only once its ``RECORD`` file exists: pip writes it last, so a wheel another hook process is still installing is not used half-written. """ - prefix = f"{re.sub(r'[-_.]+', '_', tool).lower()}-" + prefix = f"{_dist_name(tool)}-" suffix = ".dist-info" paths = _env_paths() site_dirs = dict.fromkeys((paths["purelib"], paths["platlib"])) @@ -214,6 +219,77 @@ def _find_installed_wheel(tool: str, version: str | None) -> tuple[Path, str] | return Path(path), installed +def _versions_dir() -> str: + """Return the directory that holds one install per requested ``--version``. + + pre-commit shares one hook environment between all projects on a machine + that use the same ``rev``. Each version gets its own directory there, so + projects that ask for different versions do not replace each other's wheel. + """ + return os.path.join(sys.prefix, "cpp-linter-hooks") + + +def _versioned_executable(tool: str, target: str) -> str | None: + """Return the executable inside a wheel installed with ``pip --target``. + + The wheel's own console script cannot run from there, because its package + is not on ``sys.path``, so this returns the binary the wheel ships: in + ``/data/bin`` (or ``/bin`` in old clang-format wheels). + """ + package = os.path.join(target, _dist_name(tool)) + bin_dirs = (os.path.join(package, "data", "bin"), os.path.join(package, "bin")) + return shutil.which(tool, path=os.pathsep.join(bin_dirs)) + + +def _version_key(version: str) -> tuple[int, ...]: + """Return a sortable key for a release version such as ``21.1.8``.""" + return tuple(int(part) for part in version.split(".") if part.isdigit()) + + +def _find_versioned_install(tool: str, version: str) -> tuple[Path, str] | None: + """Return (path, version) of the newest finished install that matches *version*. + + An install counts only once its ``RECORD`` file exists, so a directory left + behind by an interrupted ``pip install`` is never used. + """ + base = _versions_dir() + prefix = f"{tool}-" + try: + names = os.listdir(base) + except OSError: + return None + found = [] + for name in names: + installed = name[len(prefix) :] + if not name.startswith(prefix) or not _version_satisfies(installed, version): + continue + target = os.path.join(base, name) + record = os.path.join( + target, f"{_dist_name(tool)}-{installed}.dist-info", "RECORD" + ) + executable = _versioned_executable(tool, target) + if os.path.isfile(record) and executable: + found.append((_version_key(installed), Path(executable), installed)) + if not found: + return None + _, path, installed = max(found, key=lambda item: item[0]) + return path, installed + + +def _find_installed(tool: str, version: str | None) -> tuple[Path, str] | None: + """Return (path, version) of an installed tool that satisfies *version*. + + The wheel in the hook environment (from ``additional_dependencies``, or from + a run without ``--version``) comes first; then the per-version installs. + A run without ``--version`` never uses a per-version install, which another + project may have asked for. + """ + installed = _find_installed_wheel(tool, version) + if installed is None and version is not None: + installed = _find_versioned_install(tool, version) + return installed + + # Seconds to wait for another hook process to finish installing. _INSTALL_LOCK_TIMEOUT = 300 @@ -246,15 +322,23 @@ def _install_lock(): lock.release() -def _install_tool(tool: str, version: str) -> Path | None: - """Install a tool using pip, logging output on failure.""" - result = subprocess.run( - [sys.executable, "-m", "pip", "install", f"{tool}=={version}"], - capture_output=True, - text=True, - check=False, - ) +def _install_tool(tool: str, version: str, versioned: bool = False) -> Path | None: + """Install a tool using pip, logging output on failure. + + With *versioned*, the wheel goes to its own directory under + :func:`_versions_dir` instead of the hook environment. + """ + command = [sys.executable, "-m", "pip", "install", f"{tool}=={version}"] + if versioned: + target = os.path.join(_versions_dir(), f"{tool}-{version}") + # Remove what an interrupted install left behind; pip --target does + # not replace an existing directory. + shutil.rmtree(target, ignore_errors=True) + command += ["--target", target] + result = subprocess.run(command, capture_output=True, text=True, check=False) if result.returncode == 0: + if versioned: + return _versioned_executable(tool, target) return _env_executable(tool) or shutil.which(tool) LOG.error("pip failed to install %s %s", tool, version) LOG.error(result.stdout) @@ -269,18 +353,20 @@ def resolve_install_with_diagnostics( A wheel already installed in the hook's environment is used as is when it matches ``--version`` (or when no version was given), so pinned - ``additional_dependencies`` are kept and the hook works offline. - Otherwise the version is resolved dynamically from PyPI — no hardcoded - list is maintained in-tree. + ``additional_dependencies`` are kept and the hook works offline. So is an + earlier per-version install that matches ``--version``. Otherwise the + version is resolved dynamically from PyPI — no hardcoded list is + maintained in-tree — and a ``--version`` is installed into its own + directory. Installing runs under :func:`_install_lock`, so when several hook processes start at once, one installs and the others wait and then use its wheel. """ - installed_wheel = _find_installed_wheel(tool, version) + installed_wheel = _find_installed(tool, version) if installed_wheel is None: with _install_lock(): # Another hook process may have installed it while this one waited. - installed_wheel = _find_installed_wheel(tool, version) + installed_wheel = _find_installed(tool, version) if installed_wheel is None: return _resolve_and_install(tool, version, verbose) path, installed = installed_wheel @@ -316,7 +402,8 @@ def _resolve_and_install( ) return ( - _is_version_installed(tool, user_version) or _install_tool(tool, user_version), + _is_version_installed(tool, user_version) + or _install_tool(tool, user_version, versioned=version is not None), None, ) diff --git a/tests/test_util.py b/tests/test_util.py index 563141b..3279970 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -3,6 +3,7 @@ import errno import json import logging +import os import subprocess import sys import sysconfig @@ -12,17 +13,23 @@ import pytest +from cpp_linter_hooks import util from cpp_linter_hooks.util import ( _detect_installed_version, _env_paths, + _find_installed, _find_installed_wheel, + _find_versioned_install, _get_pypi_versions, _install_lock, _install_tool, _installed_wheel_version, _is_version_installed, _resolve_version_from_pypi, + _version_key, _version_satisfies, + _versioned_executable, + _versions_dir, resolve_install, resolve_install_with_diagnostics, ) @@ -34,13 +41,20 @@ @pytest.fixture(autouse=True) -def no_installed_wheel(): +def no_installed_wheel(tmp_path): """Hide wheels installed in the test environment from the resolver. - Tests that need an installed wheel patch ``_installed_wheel_version`` or + Per-version installs go to an empty temporary directory. Tests that need an + installed wheel patch ``_installed_wheel_version`` or ``_find_installed_wheel`` themselves. """ - with patch("cpp_linter_hooks.util._installed_wheel_version", return_value=None): + with ( + patch("cpp_linter_hooks.util._installed_wheel_version", return_value=None), + patch( + "cpp_linter_hooks.util._versions_dir", + return_value=str(tmp_path / "versions"), + ), + ): yield @@ -393,7 +407,7 @@ def patched_run(*args, **kwargs): ): result = resolve_install("clang-format", "20.1.8") assert result == Path(mock_path) - mock_install.assert_called_once_with("clang-format", "20.1.8") + mock_install.assert_called_once_with("clang-format", "20.1.8", versioned=True) @pytest.mark.benchmark @@ -410,7 +424,7 @@ def test_resolve_install_tool_not_installed(): ): result = resolve_install("clang-format", "20.1.8") assert result == Path("/usr/bin/clang-format") - mock_install.assert_called_once_with("clang-format", "20.1.8") + mock_install.assert_called_once_with("clang-format", "20.1.8", versioned=True) @pytest.mark.benchmark @@ -427,7 +441,7 @@ def test_resolve_install_no_version_uses_latest(): ): result = resolve_install("clang-format", None) assert result == Path("/usr/bin/clang-format") - mock_install.assert_called_once_with("clang-format", "22.1.5") + mock_install.assert_called_once_with("clang-format", "22.1.5", versioned=False) @pytest.mark.benchmark @@ -648,7 +662,7 @@ def test_resolve_install_with_diagnostics_is_quiet_by_default(capsys): assert result == (Path("/usr/bin/clang-tidy"), None) assert capsys.readouterr().err == "" - mock_install.assert_called_once_with("clang-tidy", "21.1.6") + mock_install.assert_called_once_with("clang-tidy", "21.1.6", versioned=True) def test_resolve_install_with_diagnostics_offline_uses_installed_tool(): @@ -914,7 +928,7 @@ def test_resolve_install_other_installed_wheel_resolves_from_pypi(): result = resolve_install_with_diagnostics("clang-tidy", "21") assert result == (Path("/venv/bin/clang-tidy"), None) - mock_install.assert_called_once_with("clang-tidy", "21.1.6") + mock_install.assert_called_once_with("clang-tidy", "21.1.6", versioned=True) def test_resolve_install_verbose_reports_installed_wheel(capsys): @@ -1023,7 +1037,7 @@ def __enter__(self): def __exit__(self, *exc): events.append("unlock") - def install(tool, version): + def install(tool, version, versioned): events.append("install") return Path("/venv/bin/clang-format") @@ -1070,3 +1084,149 @@ def test_resolve_install_uses_wheel_installed_while_waiting(): assert result == (Path("/venv/bin/clang-format"), None) pypi.assert_not_called() install.assert_not_called() + + +# ═══════════════════════════════════════════════════════════════════════ +# Per-version installs (--version gets its own directory) +# ═══════════════════════════════════════════════════════════════════════ + +EXE = ".exe" if os.name == "nt" else "" + + +def _base() -> Path: + """Return the per-version directory the autouse fixture points to.""" + return Path(util._versions_dir()) + + +def _fake_versioned_install( + base: Path, tool: str, version: str, bin_dir=("data", "bin"), record=True +) -> Path: + """Lay out what ``pip install --target`` leaves for a clang tool wheel.""" + target = base / f"{tool}-{version}" + package = target / tool.replace("-", "_") + executable = package.joinpath(*bin_dir) / f"{tool}{EXE}" + executable.parent.mkdir(parents=True) + executable.write_text("") + executable.chmod(0o755) + dist_info = target / f"{tool.replace('-', '_')}-{version}.dist-info" + dist_info.mkdir() + if record: + (dist_info / "RECORD").write_text("") + return executable + + +def test_versions_dir_is_in_hook_environment(monkeypatch): + # The original function: the autouse fixture patches only the module attribute. + monkeypatch.setattr(sys, "prefix", "/env") + assert _versions_dir() == os.path.join("/env", "cpp-linter-hooks") + + +@pytest.mark.parametrize("bin_dir", [("data", "bin"), ("bin",)]) +def test_versioned_executable_finds_wheel_binary(tmp_path, bin_dir): + executable = _fake_versioned_install(tmp_path, "clang-format", "21.1.8", bin_dir) + target = str(tmp_path / "clang-format-21.1.8") + assert Path(_versioned_executable("clang-format", target)) == executable + + +def test_versioned_executable_missing(tmp_path): + assert _versioned_executable("clang-tidy", str(tmp_path)) is None + + +@pytest.mark.parametrize( + "version, expected", + [("21.1.8", (21, 1, 8)), ("19.1.0.1", (19, 1, 0, 1)), ("10.0.1.1", (10, 0, 1, 1))], +) +def test_version_key(version, expected): + assert _version_key(version) == expected + + +def test_find_versioned_install_newest_match(): + base = _base() + _fake_versioned_install(base, "clang-tidy", "21.1.1") + newest = _fake_versioned_install(base, "clang-tidy", "21.1.6") + _fake_versioned_install(base, "clang-tidy", "20.1.0") + _fake_versioned_install(base, "clang-format", "21.1.8") + + assert _find_versioned_install("clang-tidy", "21") == (newest, "21.1.6") + assert _find_versioned_install("clang-tidy", "21.1.1")[1] == "21.1.1" + assert _find_versioned_install("clang-tidy", "22") is None + + +def test_find_versioned_install_skips_unfinished_and_broken(): + base = _base() + _fake_versioned_install(base, "clang-format", "21.1.8", record=False) + (base / "clang-format-20.1.8").mkdir() + assert _find_versioned_install("clang-format", "21") is None + assert _find_versioned_install("clang-format", "20") is None + + +def test_find_versioned_install_without_directory(): + assert not _base().exists() + assert _find_versioned_install("clang-format", "21") is None + + +def test_find_installed_pinned_uses_versioned_install(): + executable = _fake_versioned_install(_base(), "clang-format", "18.1.8") + assert _find_installed("clang-format", "18") == (executable, "18.1.8") + + +def test_find_installed_unpinned_ignores_versioned_install(): + """A run without --version never picks a version another project asked for.""" + _fake_versioned_install(_base(), "clang-format", "18.1.8") + assert _find_installed("clang-format", None) is None + + +def test_find_installed_prefers_environment_wheel(): + _fake_versioned_install(_base(), "clang-format", "21.1.8") + with patch( + "cpp_linter_hooks.util._find_installed_wheel", + return_value=(Path("/venv/bin/clang-format"), "21.1.8"), + ): + assert _find_installed("clang-format", "21") == ( + Path("/venv/bin/clang-format"), + "21.1.8", + ) + + +def test_install_tool_versioned_uses_own_directory(): + base = _base() + target = base / "clang-format-18.1.8" + leftover = target / "leftover" + leftover.mkdir(parents=True) + + def pip(command, **kwargs): + assert not leftover.exists() + _fake_versioned_install(base, "clang-format", "18.1.8") + return subprocess.CompletedProcess(command, returncode=0) + + with patch("subprocess.run", side_effect=pip) as run: + result = _install_tool("clang-format", "18.1.8", versioned=True) + + assert Path(result) == ( + target / "clang_format" / "data" / "bin" / f"clang-format{EXE}" + ) + assert run.call_args.args[0] == [ + sys.executable, + "-m", + "pip", + "install", + "clang-format==18.1.8", + "--target", + str(target), + ] + + +def test_resolve_install_versions_coexist_without_pypi(): + """Switching --version between projects reuses each install, offline.""" + base = _base() + v18 = _fake_versioned_install(base, "clang-format", "18.1.8") + v21 = _fake_versioned_install(base, "clang-format", "21.1.8") + with ( + patch("cpp_linter_hooks.util._get_pypi_versions") as pypi, + patch("cpp_linter_hooks.util._install_tool") as install, + ): + assert resolve_install("clang-format", "18") == v18 + assert resolve_install("clang-format", "21") == v21 + assert resolve_install("clang-format", "18.1.8") == v18 + pypi.assert_not_called() + install.assert_not_called() From 076712a9ac03be5c82d68e956cc939b24f3c5020 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:38:16 +0000 Subject: [PATCH 2/4] docs: network access only when no matching wheel is installed --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 845dba8..923e601 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,9 @@ picks and installs the wheel the first time it runs: your projects that use the same `rev`, so projects that ask for different versions keep their own copy instead of reinstalling. A wheel from `additional_dependencies` that matches `--version` is used as well. -- The first run with a new `--version`, or without any version, looks the version up on pypi.org - and installs it, so that run needs network access. pre-commit runs the hook on groups of files - in parallel; one of them installs the wheel and the others wait for it. +- Only a run that finds no matching wheel installed looks the version up on pypi.org and + installs it, so that run needs network access. pre-commit runs the hook on groups of files in + parallel; one of them installs the wheel and the others wait for it. ### Install the tool with the hook From 20e6ae29fe87cabd02d27b07b1a24e34c445a049 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Fri, 23 Oct 2026 23:02:02 +0300 Subject: [PATCH 3/4] docs: Simplify AGENTS.md by removing sections Removed unnecessary details from the important note and deleted sections on key files, core patterns, and CI. --- AGENTS.md | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8d7835c..8b3e5be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -> **IMPORTANT**: When you modify any code in this project, also update this file if the change affects agent behavior, project structure, dependencies, git workflow, or any guidance documented here. +> **IMPORTANT**: When you modify any code in this project, also update this file if the change affects agent behavior, git workflow, or any guidance documented here. ## Git Rules (MUST FOLLOW) @@ -19,19 +19,6 @@ A pre-commit hook repo that auto-installs and runs `clang-format` and `clang-tid **Hook definitions**: `.pre-commit-hooks.yaml` -## Key Files - -| File | Purpose | -|------|---------| -| `pyproject.toml` | Build config, deps (`pip>=26.1`, `filelock>=3.16`), dev deps, entry points | -| `cpp_linter_hooks/util.py` | Installed-wheel and per-version install lookup, PyPI version resolution + pip install (prefix matching, offline fallback, `--version` into its own directory), all under a `filelock` lock on the hook environment so parallel hook processes install once | -| `cpp_linter_hooks/clang_format.py` | clang-format wrapper (`--verbose`, `--dry-run`) | -| `cpp_linter_hooks/clang_tidy.py` | clang-tidy wrapper (`--compile-commands`, missing-database policy, `--jobs`, `--fix`, `--all-lines`, `-quiet` + status-line filter, `--checks` repair, error hints) | -| `cpp_linter_hooks/changed_lines.py` | Added line ranges per file from `git diff` (staged changes, or `PRE_COMMIT_FROM_REF...PRE_COMMIT_TO_REF`) | -| `.pre-commit-hooks.yaml` | Hook metadata for pre-commit framework | -| `testing/` | Test fixtures (`main.c`, `good.c`, `.clang-format`, `.clang-tidy`, CMakeLists) | -| `examples/` | Example configs (CMake, Meson, large-project scoping) | - ## Development ```bash @@ -41,17 +28,3 @@ uv run coverage run --source=tests,cpp_linter_hooks -m pytest -vv # With covera bash testing/run.sh # Integration tests (full pre-commit pipeline) ``` -## Core Patterns - -- **Version resolution**: A wheel already installed in the hook environment (found by its `-.dist-info` directory in site-packages, e.g. from `additional_dependencies`, with its executable in the environment's scripts directory) is used without network access when it matches `--version` (exact or prefix) or when no `--version` is given. With `--version`, a finished per-version install in `/cpp-linter-hooks/-/` (`pip install --target`, `RECORD` present, binary in `/data/bin` or `/bin`) is used next, newest match first; a run without `--version` never uses those. Otherwise the version is resolved dynamically from PyPI at runtime: `--version` installs into its own directory (pre-commit shares one hook environment between all projects with the same `rev`, so versions must not replace each other), no `--version` installs into the environment. No hardcoded version lists. Prefix match supported (`--version=20` → `20.1.8`). -- **Return values**: All hooks return `tuple[int, str]` — `(retval, output)`, where `retval` is the tool's exit code (`0` on success, non-zero on failure; `--dry-run` auto-adds `--Werror` so formatting issues fail). -- **clang-tidy compile DB**: Auto-detected from `build/`, `out/`, `cmake-build-debug/`, `_build/`, `builddir/`, then `cmake-build-*/`, `build-*/`, `build/*/`, `out/build/*/` (newest wins). Override with `--compile-commands=`, disable with `--no-compile-commands`. If none is found, no `-p`/`--extra-arg*` is given and clang-tidy finds no `compile_commands.json`/`compile_flags.txt` next to the files or in their parents, the hook skips clang-tidy (retval 0, message on stderr), or fails when `CI` is set. -- **clang-tidy changed lines**: By default only diagnostics on added lines are reported, via clang-tidy's `--line-filter`, so `-fix` applies only the fixes of those diagnostics (a fix may still edit an unchanged line nearby). Files without added lines are not checked. If any passed file has no changes (`--all-files`), differs on disk from the diffed version (index or `PRE_COMMIT_TO_REF`), or git fails, every line is checked. `--all-lines` or a user `--line-filter` turns this off. Tests: `tests/conftest.py` disables this and the missing-database check by default; use the `real_changed_lines` / `real_compile_db_check` fixtures to test them. -- **clang-tidy parallel**: `--jobs=N` runs per-file in parallel. Forced serial when `--export-fixes`, `-fix`, or `-fix-errors` is used. -- **clang-tidy output**: Without `--verbose`, the hook adds `-quiet` (unless the user set it) and strips status lines (`N warnings generated.`, `Suppressed N warnings ...`, `Use -header-filter=...`, `[1/N] Processing file ...`). With `--verbose`, output is unchanged. -- **clang-tidy check lists**: `--checks=` / `--warnings-as-errors=` lists that YAML split at commas are joined again, quotes inside the value are removed, and `--checks=` (e.g. `.clang-tidy`) gets a note. Notes are appended to the output, which pre-commit shows when the hook fails. - -## CI - -- `test.yml` — matrix (3 OS × 6 Python versions), pytest + coverage + integration tests -- `codspeed.yml` — benchmarks via CodSpeed on path-filtered changes From 1090e7ccf9df9027087d8a1919c0622e213ca9f7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:02:10 +0000 Subject: [PATCH 4/4] chore: auto fixes from pre-commit.com hooks --- AGENTS.md | 1 - 1 file changed, 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 8b3e5be..9ed7e9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,4 +27,3 @@ uv run pytest -vv # Run tests uv run coverage run --source=tests,cpp_linter_hooks -m pytest -vv # With coverage bash testing/run.sh # Integration tests (full pre-commit pipeline) ``` -