Initial commit
This commit is contained in:
156
skills/git-pr/scripts/milestone_cache.py
Normal file
156
skills/git-pr/scripts/milestone_cache.py
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/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}"
|
||||
Reference in New Issue
Block a user