2025-10-28 02:50:31 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Pre-push hook: Commitizen check for *new commits only*.
|
|
|
|
|
|
2026-02-23 20:59:03 +01:00
|
|
|
Cross-platform + uv/.venv aware:
|
2025-10-28 02:50:31 +01:00
|
|
|
- Prefers activated virtual environment (VIRTUAL_ENV)
|
2026-02-23 20:59:03 +01:00
|
|
|
- Falls back to uv-managed .uv/venv
|
|
|
|
|
- Falls back to .venv
|
|
|
|
|
- Falls back to global cz
|
2025-10-28 02:50:31 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
2026-02-23 20:59:03 +01:00
|
|
|
def find_cz() -> list[str]:
|
|
|
|
|
"""Return command to invoke Commitizen via virtualenv or globally."""
|
|
|
|
|
candidates = []
|
|
|
|
|
|
|
|
|
|
# 1️⃣ Currently active virtualenv
|
|
|
|
|
venv = os.getenv("VIRTUAL_ENV")
|
|
|
|
|
if venv:
|
|
|
|
|
candidates.append(Path(venv))
|
|
|
|
|
|
|
|
|
|
# 2️⃣ uv-managed virtualenv
|
|
|
|
|
uv_venv = Path(".uv") / "venv"
|
|
|
|
|
if uv_venv.exists():
|
|
|
|
|
candidates.append(uv_venv)
|
|
|
|
|
|
|
|
|
|
# 3️⃣ traditional .venv
|
|
|
|
|
dot_venv = Path(".venv")
|
|
|
|
|
if dot_venv.exists():
|
|
|
|
|
candidates.append(dot_venv)
|
|
|
|
|
|
|
|
|
|
# Check each candidate for Commitizen binary
|
|
|
|
|
for base in candidates:
|
|
|
|
|
cz_path = base / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz")
|
2025-10-28 02:50:31 +01:00
|
|
|
if cz_path.exists():
|
2026-02-23 20:59:03 +01:00
|
|
|
return [str(cz_path)]
|
2025-10-28 02:50:31 +01:00
|
|
|
|
2026-02-23 20:59:03 +01:00
|
|
|
# 4️⃣ fallback to uv run cz
|
|
|
|
|
try:
|
|
|
|
|
subprocess.run(["uv", "run", "cz", "--version"], check=True, stdout=subprocess.DEVNULL)
|
|
|
|
|
return ["uv", "run", "cz"]
|
|
|
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
|
|
|
pass
|
2025-10-28 02:50:31 +01:00
|
|
|
|
2026-02-23 20:59:03 +01:00
|
|
|
# 5️⃣ fallback to system cz
|
|
|
|
|
return ["cz"]
|
2025-10-28 02:50:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_merge_base() -> str | None:
|
|
|
|
|
"""Return merge-base between HEAD and upstream branch, or None if unavailable."""
|
|
|
|
|
try:
|
|
|
|
|
return (
|
|
|
|
|
subprocess.check_output(
|
|
|
|
|
["git", "merge-base", "@{u}", "HEAD"],
|
|
|
|
|
stderr=subprocess.DEVNULL,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
.strip()
|
|
|
|
|
)
|
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
2026-02-23 20:59:03 +01:00
|
|
|
cz = find_cz()
|
2025-10-28 02:50:31 +01:00
|
|
|
base = get_merge_base()
|
|
|
|
|
|
|
|
|
|
if not base:
|
2026-02-23 20:59:03 +01:00
|
|
|
print("⚠️ No upstream found; skipping Commitizen check {cz} for new commits.")
|
2025-10-28 02:50:31 +01:00
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
print(f"🔍 Using {cz} to check new commits from {base}..HEAD ...")
|
|
|
|
|
|
|
|
|
|
try:
|
2026-02-23 20:59:03 +01:00
|
|
|
subprocess.check_call(cz + ["check", "--rev-range", f"{base}..HEAD"])
|
2025-10-28 02:50:31 +01:00
|
|
|
print("✅ All new commits follow Commitizen conventions.")
|
|
|
|
|
return 0
|
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
|
print("❌ Commitizen check failed for one or more new commits.")
|
|
|
|
|
return e.returncode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|