Files
gh-atournayre-claude-market…/skills/git-pr/scripts/milestone_cache.py
2025-11-29 17:58:54 +08:00

157 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""Module de cache pour milestones GitHub"""
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
CACHE_DIR = Path(".claude/cache")
CACHE_FILE = CACHE_DIR / "git-milestones.json"
class MilestoneCache:
"""Gestion cache milestones"""
def __init__(self):
self.cache = self.load()
def load(self) -> dict:
"""Charge cache depuis fichier"""
if not CACHE_FILE.exists():
CACHE_DIR.mkdir(parents=True, exist_ok=True)
return {"updated_at": None, "milestones": []}
try:
with open(CACHE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return {"updated_at": None, "milestones": []}
def save(self):
"""Sauvegarde cache vers fichier"""
CACHE_DIR.mkdir(parents=True, exist_ok=True)
self.cache["updated_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
with open(CACHE_FILE, "w", encoding="utf-8") as f:
json.dump(self.cache, f, indent=2, ensure_ascii=False)
def find(self, query: str) -> Optional[dict]:
"""Cherche milestone par titre exact ou alias"""
for milestone in self.cache.get("milestones", []):
if milestone["title"] == query:
return milestone
if query in milestone.get("aliases", []):
return milestone
return None
def add(self, milestone: dict):
"""Ajoute milestone au cache avec génération aliases"""
aliases = self.generate_aliases(milestone["title"])
milestone["aliases"] = aliases
existing = [m for m in self.cache.get("milestones", []) if m["number"] == milestone["number"]]
if not existing:
if "milestones" not in self.cache:
self.cache["milestones"] = []
self.cache["milestones"].append(milestone)
self.save()
def generate_aliases(self, title: str) -> list[str]:
"""Génère aliases depuis titre (version exacte + formes courtes)
Exemples:
- "26.1.1 (Hotfix)" → ["26.1.1", "26.1", "26"]
- "26.1.0" → ["26.1.0", "26.1", "26"]
- "26.0.0 (Avenant)" → ["26.0.0", "26.0", "26"]
Logique: extraire version semver et générer formes courtes
"""
match = re.match(r'^(\d+)\.(\d+)\.(\d+)', title)
if match:
major, minor, patch = match.groups()
aliases = []
full_version = f"{major}.{minor}.{patch}"
if full_version != title:
aliases.append(full_version)
aliases.append(f"{major}.{minor}")
aliases.append(major)
return aliases
return []
def refresh_from_api(self, milestones: list[dict]):
"""Remplace cache entièrement avec données API"""
enriched = []
for milestone in milestones:
aliases = self.generate_aliases(milestone["title"])
enriched.append({
"number": milestone["number"],
"title": milestone["title"],
"aliases": aliases
})
self.cache["milestones"] = enriched
self.save()
def create(self, title: str) -> dict:
"""Crée milestone sur GitHub via API et l'ajoute au cache
Récupère repo via get_repo_info()
gh api repos/{repo}/milestones -f title="99.0.0" -f state="open"
Retourne: {"number": 43, "title": "99.0.0", ...}
"""
repo = self.get_repo_info()
try:
result = subprocess.run(
["gh", "api", f"repos/{repo}/milestones", "-f", f"title={title}", "-f", "state=open"],
capture_output=True,
text=True,
check=True
)
milestone = json.loads(result.stdout)
self.add(milestone)
return milestone
except subprocess.CalledProcessError as e:
print(f"❌ Erreur création milestone: {e.stderr}", file=sys.stderr)
raise
except json.JSONDecodeError as e:
print(f"❌ Erreur parsing JSON: {e}", file=sys.stderr)
raise
def get_repo_info(self) -> str:
"""Récupère owner/repo depuis git remote"""
try:
result = subprocess.run(
["git", "remote", "get-url", "origin"],
capture_output=True,
text=True,
check=True
)
url = result.stdout.strip()
match = re.search(r'github\.com[:/](.+/.+?)(?:\.git)?$', url)
if match:
return match.group(1)
raise ValueError(f"Format URL invalide: {url}")
except Exception as e:
print(f"❌ Erreur récupération repo: {e}", file=sys.stderr)
raise
def normalize_semver(self, version: str) -> str:
"""Normalise version en semver complet
Exemples:
- "26""26.0.0"
- "26.1""26.1.0"
- "26.1.1""26.1.1"
- "26.0.0 (Avenant)""26.0.0 (Avenant)" (conserve suffixe)
"""
match = re.match(r'^(\d+)(?:\.(\d+))?(?:\.(\d+))?(.*)?$', version)
if not match:
return version
major = match.group(1)
minor = match.group(2) or "0"
patch = match.group(3) or "0"
suffix = match.group(4) or ""
return f"{major}.{minor}.{patch}{suffix}"