#!/usr/bin/env python3 """Install the opt-in local git hook workflow for the C++ template project.""" from __future__ import annotations import argparse import os import pathlib import stat import subprocess from collections.abc import Sequence from typing import cast from lib import common STATUS_PASS = "PASS" STATUS_FAIL = "FAIL" def main(argv: Sequence[str] | None = None) -> int: args = build_parser().parse_args(argv) root = common.find_project_root(pathlib.Path(__file__)) hooks_dir = root / ".githooks" hook_path = hooks_dir / "pre-commit" status = cast(bool, args.status) dry_run = cast(bool, args.dry_run) if status: return report_status(root, hooks_dir, hook_path) plan = build_plan(root) if dry_run: common.print_plan(plan) print(f"{STATUS_PASS}: dry-run planned opt-in local hook setup") return 0 print("local git hook setup") common.print_plan(plan) hooks_dir.mkdir(parents=True, exist_ok=True) if not hook_path.is_file(): print(f"{STATUS_FAIL}: expected hook file is missing: {hook_path}") return 2 ensure_executable(hook_path) return common.run_plans(plan, dry_run=False) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) _ = parser.add_argument("--dry-run", action="store_true", help="Print planned setup without writing hooks or git config.") _ = parser.add_argument("--status", action="store_true", help="Report local hook status without mutating files or git config.") return parser def build_plan(root: pathlib.Path) -> tuple[common.CommandPlan, ...]: return ( common.CommandPlan( label="set-local-hooks-path", command=("git", "config", "--local", "core.hooksPath", ".githooks"), cwd=root, required_tool="git", ), ) def report_status(root: pathlib.Path, hooks_dir: pathlib.Path, hook_path: pathlib.Path) -> int: print(f"hooks_dir: {hooks_dir}") print(f"pre_commit_hook_exists: {hook_path.is_file()}") print(f"pre_commit_hook_executable: {os.access(hook_path, os.X_OK)}") print(f"local core.hooksPath: {local_hooks_path(root)}") return 0 def local_hooks_path(root: pathlib.Path) -> str: result = subprocess.run( ["git", "config", "--local", "--get", "core.hooksPath"], cwd=str(root), check=False, capture_output=True, text=True, ) if result.returncode == 0: return result.stdout.strip() return "" def ensure_executable(path: pathlib.Path) -> None: current_mode = path.stat().st_mode executable_bits = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH if current_mode & executable_bits == executable_bits: return path.chmod(current_mode | executable_bits) if __name__ == "__main__": raise SystemExit(main())