()
| 321 | |
| 322 | |
| 323 | def main() -> int: |
| 324 | try: |
| 325 | porcelain = get_porcelain() |
| 326 | except GitPorcelainError as exc: |
| 327 | # Fail fast with a descriptive error instead of silently treating a |
| 328 | # broken git invocation as "clean tree, nothing to do". The hook |
| 329 | # depends on `git status --porcelain` to know what changed; if it |
| 330 | # cannot run, we cannot safely decide whether to skip lint/tests. |
| 331 | print(f"[stop-hook] FATAL: {exc}", file=sys.stderr) |
| 332 | return 2 |
| 333 | if should_skip_hook(porcelain): |
| 334 | print("Skipping lint+tests (no changes during this session)", file=sys.stderr) |
| 335 | warn_stale_worktrees() |
| 336 | return 0 |
| 337 | |
| 338 | # porcelain is guaranteed non-None here (should_skip_hook returned False) |
| 339 | assert porcelain is not None |
| 340 | buckets = classify_changes(porcelain) |
| 341 | |
| 342 | run_tests_phase = buckets.needs_cpp_test |
| 343 | run_meson_stage = buckets.needs_meson_lint |
| 344 | |
| 345 | test_skip_reason = "" |
| 346 | if not run_tests_phase: |
| 347 | # Describe why we are skipping tests so the timing line is self-explanatory |
| 348 | if buckets.has_any: |
| 349 | test_skip_reason = "no src/tests/examples/build changes" |
| 350 | else: |
| 351 | test_skip_reason = "no changes detected" |
| 352 | |
| 353 | print( |
| 354 | f"Running lint{' + C++ tests' if run_tests_phase else ''} " |
| 355 | f"(session changes: " |
| 356 | f"src={'Y' if buckets.src else 'N'} " |
| 357 | f"tests={'Y' if buckets.tests else 'N'} " |
| 358 | f"examples={'Y' if buckets.examples else 'N'} " |
| 359 | f"build={'Y' if buckets.build else 'N'} " |
| 360 | f"other={'Y' if buckets.other else 'N'})", |
| 361 | file=sys.stderr, |
| 362 | ) |
| 363 | |
| 364 | lint_done = threading.Event() |
| 365 | lint_results: list[subprocess.CompletedProcess[str]] = [] |
| 366 | test_proc_holder: list[subprocess.Popen[str]] = [] |
| 367 | test_results: list[subprocess.CompletedProcess[str]] = [] |
| 368 | lint_duration: list[float] = [] |
| 369 | test_duration: list[float] = [] |
| 370 | # Signalled once the test subprocess has either been spawned and registered |
| 371 | # in test_proc_holder OR the thread has exited without spawning (e.g. Popen |
| 372 | # raised). The lint-failure cancellation path waits on this so it cannot |
| 373 | # race past the start of run_tests() and miss the spawned child. |
| 374 | test_ready = threading.Event() |
| 375 | |
| 376 | def run_lint() -> None: |
| 377 | lint_cmd: list[str] = ["uv", "run", "ci/lint.py"] |
| 378 | if not run_meson_stage: |
| 379 | lint_cmd.append("--skip-meson") |
| 380 | t0 = time.perf_counter() |
no test coverage detected