#!/usr/bin/env python3 import json import re import subprocess import sys from pathlib import Path REPO_ROOT = Path(subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()) PLUGIN_JSON_PATH = REPO_ROOT / "plugin.json" STOP_PATTERNS = [ r"\[no[-_ ]?bump\]", r"\[skip[-_ ]?bump\]", r"\[no[-_ ]?version[-_ ]?bump\]", r"\[skip[-_ ]?version\]", r"\bno[-_ ]bump\b", r"\bskip[-_ ]bump\b", r"\bno[-_ ]version[-_ ]bump\b", ] def increment_patch_version(version_str: str) -> str: match = re.fullmatch(r"^(\d+)\.(\d+)\.(\d+)(.*)$", version_str.strip()) if not match: raise ValueError(f"Version '{version_str}' does not match x.y.z format") major, minor, patch, suffix = match.groups() new_patch = int(patch) + 1 return f"{major}.{minor}.{new_patch}{suffix}" def get_head_version() -> str | None: res = subprocess.run( ["git", "rev-parse", "--verify", "HEAD"], cwd=REPO_ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) if res.returncode != 0: return None show_res = subprocess.run( ["git", "show", "HEAD:plugin.json"], cwd=REPO_ROOT, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, ) if show_res.returncode != 0: return None try: head_data = json.loads(show_res.stdout) return str(head_data.get("version")) if "version" in head_data else None except json.JSONDecodeError: return None def should_skip_from_commit_msg() -> bool: # 1. Check GIT_PARAMS / COMMIT_EDITMSG / MERGE_MSG candidates = [ REPO_ROOT / ".git" / "COMMIT_EDITMSG", REPO_ROOT / ".git" / "MERGE_MSG", ] for p in candidates: if p.exists(): content = p.read_text(encoding="utf-8", errors="replace") lines = [line for line in content.splitlines() if not line.strip().startswith("#")] text = " ".join(lines) for pattern in STOP_PATTERNS: if re.search(pattern, text, re.IGNORECASE): return True # 2. Check process command-line arguments (e.g. git commit -m "...") try: # On Linux, inspect parent process /proc cmdline ppid = subprocess.check_output(["ps", "-o", "ppid=", "-p", str(Path("/proc/self").resolve().name)], text=True).strip() cmdline_path = Path(f"/proc/{ppid}/cmdline") if cmdline_path.exists(): cmd_args = cmdline_path.read_bytes().decode("utf-8", errors="replace") for pattern in STOP_PATTERNS: if re.search(pattern, cmd_args, re.IGNORECASE): return True except Exception: pass return False def main() -> int: # Don't auto-bump during merge/rebase in progress if (REPO_ROOT / ".git" / "MERGE_HEAD").exists() or (REPO_ROOT / ".git" / "rebase-merge").exists(): return 0 if should_skip_from_commit_msg(): print("pre-commit: stop word detected in commit message, skipping version bump") return 0 if not PLUGIN_JSON_PATH.exists(): return 0 try: data = json.loads(PLUGIN_JSON_PATH.read_text(encoding="utf-8")) current_version = str(data.get("version", "")) except Exception as e: print(f"pre-commit: failed to parse {PLUGIN_JSON_PATH}: {e}", file=sys.stderr) return 1 head_version = get_head_version() # Skip if version was manually changed (differs from HEAD) if head_version is not None and current_version != head_version: print( f"pre-commit: version already modified ({head_version} -> {current_version}), skipping auto-increment" ) return 0 try: new_version = increment_patch_version(current_version) data["version"] = new_version PLUGIN_JSON_PATH.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") except Exception as e: print(f"pre-commit: failed to bump version in {PLUGIN_JSON_PATH}: {e}", file=sys.stderr) return 1 res = subprocess.run(["git", "add", str(PLUGIN_JSON_PATH)], cwd=REPO_ROOT) if res.returncode != 0: print("pre-commit: failed to stage plugin.json", file=sys.stderr) return res.returncode print(f"pre-commit: bumped version {current_version} -> {new_version}") return 0 if __name__ == "__main__": sys.exit(main())