#!/usr/bin/env python3 """Create a path-scoped local git checkpoint commit.""" from __future__ import annotations import argparse import subprocess import sys from pathlib import Path def run_git(args: list[str], cwd: Path, *, check: bool = True) -> subprocess.CompletedProcess[str]: result = subprocess.run( ["git", *args], cwd=cwd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=60, ) if check and result.returncode != 0: message = result.stderr.strip() or result.stdout.strip() raise SystemExit(f"git {' '.join(args)} failed: {message}") return result def repo_root() -> Path: result = subprocess.run( ["git", "rev-parse", "--show-toplevel"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=60, ) if result.returncode != 0: raise SystemExit("Not inside a git repository.") return Path(result.stdout.strip()) def load_paths(args: argparse.Namespace) -> list[str]: paths = list(args.paths) if args.paths_from_file: raw_lines = Path(args.paths_from_file).read_text(encoding="utf-8").splitlines() paths.extend(line.strip() for line in raw_lines if line.strip() and not line.startswith("#")) return paths def ensure_clean_index(root: Path) -> None: staged = run_git(["diff", "--cached", "--name-only"], root).stdout.strip() if staged: raise SystemExit( "Refusing to continue because the index already has staged changes:\n" f"{staged}\n" "Commit or unstage them before running this checkpoint helper." ) def status_for(root: Path, paths: list[str], allow_all: bool) -> str: command = ["status", "--short"] if not allow_all: command.extend(["--", *paths]) return run_git(command, root).stdout.strip() def infer_message(paths: list[str], allow_all: bool) -> str: if allow_all or not paths: return "chore(checkpoint): backup current task changes" first_parts = [Path(item).parts[0] for item in paths if Path(item).parts] scope = first_parts[0] if first_parts else "task" return f"chore(checkpoint): backup {scope} changes" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("paths", nargs="*", help="Task-owned paths to stage and commit.") parser.add_argument("-m", "--message", help="Commit message subject/body.") parser.add_argument("--paths-from-file", help="Read additional pathspecs from a UTF-8 text file.") parser.add_argument("--dry-run", action="store_true", help="Show matching changes without staging.") parser.add_argument("--allow-all", action="store_true", help="Allow committing all dirty files.") parser.add_argument("--no-verify", action="store_true", help="Pass --no-verify to git commit.") return parser.parse_args() def main() -> int: args = parse_args() root = repo_root() paths = load_paths(args) if not paths and not args.allow_all: raise SystemExit("Pass explicit paths, or use --allow-all when the current task owns all changes.") current_status = status_for(root, paths, args.allow_all) if not current_status: print("No matching changes to commit.") return 0 print(current_status) if args.dry_run: return 0 ensure_clean_index(root) # 使用 -A 保留删除/重命名等变更,但只作用于明确传入的 pathspec。 if args.allow_all: run_git(["add", "-A"], root) else: run_git(["add", "-A", "--", *paths], root) staged_summary = run_git(["diff", "--cached", "--name-status"], root).stdout.strip() if not staged_summary: raise SystemExit("No staged changes after git add.") message = args.message or infer_message(paths, args.allow_all) commit_command = ["commit"] if args.no_verify: commit_command.append("--no-verify") commit_command.extend(["-m", message]) commit_result = run_git(commit_command, root) sys.stdout.write(commit_result.stdout) commit_hash = run_git(["rev-parse", "--short", "HEAD"], root).stdout.strip() print(f"checkpoint_commit={commit_hash}") return 0 if __name__ == "__main__": raise SystemExit(main())