Pulse/scripts/release_control/internal/verify_commit_slice.py
rcourtman 971520a8e5 fix(release-control): scrub hook git env in remaining scratch-repo git users
The core.bare=true corruption of the shared repository recurred on
2026-07-17: a script that runs scratch git commands while the pre-commit
environment from a linked worktree (absolute GIT_DIR) is still exported
re-initializes the REAL repository as bare. a0fda6b26 fixed four helper
test files but missed two spots in scripts/release_control/internal:

- verify_commit_slice_test.py's git() helper only popped GIT_INDEX_FILE,
  so its scratch 'git init' calls re-init the real repo when GIT_DIR is
  inherited. It now scrubs via the shared repo_file_io.strip_local_git_env.
- verify_commit_slice.py's production git_env() kept the inherited hook
  env even when unit tests patch REPO_ROOT to a temporary repository,
  pointing git plumbing (including index writes) at the wrong repo. It
  now scrubs in the test-patched branch only, matching format_staged_go.

Regression teeth:
- verify_commit_slice_test.py gains a canary test that exports the real
  hook env shape (absolute GIT_DIR + GIT_INDEX_FILE, no GIT_WORK_TREE —
  with GIT_WORK_TREE set the corruption does not reproduce) against a
  scratch repo + linked worktree and asserts core.bare stays false.
- repo_file_io_test.py (runs in the pre-commit battery) gains a static
  guard failing any release-control *_test.py that runs scratch
  'git init' without referencing strip_local_git_env.
- The six hand-rolled 4-var pop loops from a0fda6b26 migrate to the
  shared strip_local_git_env helper so the guard enforces one pattern.

Verified: full release-control battery green; every touched test file
also green with GIT_DIR/GIT_INDEX_FILE pointed at a canary repo's linked
worktree, canary config and status intact afterward.
2026-07-17 16:39:17 +01:00

104 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Run pre-commit against a proposed commit slice in an isolated git index."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import subprocess
import sys
import tempfile
from typing import Iterable
REPO_ROOT = Path(__file__).resolve().parents[3]
DEFAULT_REPO_ROOT = REPO_ROOT
HOOK_PATH = REPO_ROOT / ".husky" / "pre-commit"
def git_env(index_path: Path) -> dict[str, str]:
env = os.environ.copy()
# Unit tests patch REPO_ROOT to a temporary repository. In that case, the
# inherited hook environment (the absolute GIT_DIR a pre-commit run from a
# linked worktree exports) must not point git plumbing at a different
# repository than the patched one.
if REPO_ROOT != DEFAULT_REPO_ROOT:
for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"):
env.pop(name, None)
env["GIT_INDEX_FILE"] = str(index_path)
return env
def repo_relative_path(path: str | Path) -> str:
candidate = Path(path)
if candidate.is_absolute():
candidate = candidate.relative_to(REPO_ROOT)
return candidate.as_posix()
def git(index_path: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", *args],
cwd=REPO_ROOT,
check=check,
capture_output=True,
text=True,
env=git_env(index_path),
)
def stage_paths(index_path: Path, paths: Iterable[str]) -> None:
normalized = [repo_relative_path(path) for path in paths]
if not normalized:
return
git(index_path, "add", "-f", "--", *normalized)
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Verify a proposed commit slice by running .husky/pre-commit in a copied index."
)
parser.add_argument(
"--add-updated",
action="store_true",
help="Stage all tracked modifications (`git add -u`) into the copied index before explicit paths.",
)
parser.add_argument(
"--show-staged",
action="store_true",
help="Print the staged file list in the copied index before running the hook.",
)
parser.add_argument(
"paths",
nargs="*",
help="Explicit repo-relative or absolute paths to stage into the copied index.",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(list(argv or sys.argv[1:]))
with tempfile.NamedTemporaryFile(prefix="pulse-index.", delete=True) as tmp_index:
index_path = Path(tmp_index.name)
git(index_path, "read-tree", "HEAD")
if args.add_updated:
git(index_path, "add", "-u")
stage_paths(index_path, args.paths)
if args.show_staged:
staged = git(index_path, "diff", "--cached", "--name-only").stdout.strip()
if staged:
print(staged)
result = subprocess.run(
[str(HOOK_PATH)],
cwd=REPO_ROOT,
env=git_env(index_path),
text=True,
)
return result.returncode
if __name__ == "__main__":
sys.exit(main())