chore(skills): add git checkpoint commit loop
This commit is contained in:
127
.codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py
Normal file
127
.codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user