157 lines
5.5 KiB
Python
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}"
|