commit 4d6408436ede9f63b9fd65dc8a674db2f962ad2e Author: Zhongwei Li Date: Sat Nov 29 17:58:54 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..893e1f8 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "git", + "description": "Workflow Git complet : branches, commits, conflits, PR avec automation QA", + "version": "1.4.10", + "author": { + "name": "Aurélien Tournayre", + "email": "aurelien.tournayre@gmail.com" + }, + "skills": [ + "./skills" + ], + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0adafb --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# git + +Workflow Git complet : branches, commits, conflits, PR avec automation QA diff --git a/commands/branch.md b/commands/branch.md new file mode 100644 index 0000000..41ccb65 --- /dev/null +++ b/commands/branch.md @@ -0,0 +1,152 @@ +--- +model: claude-sonnet-4-5-20250929 +allowed-tools: Bash +argument-hint: [issue-number-or-text] +description: Création de branche Git avec workflow structuré +--- + +# Création de branche Git + +## Purpose +Créer une nouvelle branche Git de manière structurée avec support des issues GitHub. + +## Variables +SOURCE_BRANCH: $1 +ISSUE_OR_TEXT: $2 + +## Instructions +- Utilise les outils Bash pour les opérations Git +- Valide que la branche source existe +- Génère un nom de branche basé sur l'issue si fournie +- Applique les conventions de nommage du projet + +## Relevant Files +- @.git/config +- @.gitignore +- @docs/README.md + +## Workflow + +**🚨 ÉTAPE CRITIQUE : CHECKOUT VERS SOURCE D'ABORD 🚨** + +1. **Vérifier SOURCE_BRANCH obligatoire** + - Si `SOURCE_BRANCH` n'est pas fourni → ARRÊTER et demander à l'utilisateur + +2. **Valider SOURCE_BRANCH existe localement** + - `git branch --list "$SOURCE_BRANCH"` + - Si n'existe pas → ARRÊTER avec erreur + +3. **🔴 CHECKOUT VERS SOURCE_BRANCH AVANT TOUT 🔴** + - `git checkout $SOURCE_BRANCH` + - Vérifier qu'on est bien dessus : `git branch --show-current` + - **CRITIQUE** : Cette étape garantit qu'on crée depuis un point propre + +4. **🔴 PULL POUR METTRE À JOUR SOURCE_BRANCH 🔴** + - `git pull origin $SOURCE_BRANCH` + - Garantit qu'on part du dernier commit de origin + - **CRITIQUE** : Évite de créer depuis un point obsolète + +5. **Générer nom de la nouvelle branche** + - Si `ISSUE_OR_TEXT` est fourni : + - Détecte si c'est un numéro (entier) ou du texte + - Si c'est un numéro : + - Récupère les informations de l'issue via GitHub CLI (`gh issue view ${ISSUE_OR_TEXT}`) + - Génère un nom de branche : `issue/${ISSUE_OR_TEXT}-{titre-simplifie}` + - Le titre est nettoyé (espaces -> tirets, caractères spéciaux supprimés, minuscules) + - Si c'est du texte : + - Génère un nom de branche : `feature/${ISSUE_OR_TEXT-simplifie}` + - Le texte est nettoyé (espaces -> tirets, caractères spéciaux supprimés, minuscules) + - Si pas de `ISSUE_OR_TEXT`, demande le nom de branche à l'utilisateur + +6. **Vérifier que la nouvelle branche n'existe pas déjà** + - `git branch --list "$NEW_BRANCH"` + - Si existe déjà → ARRÊTER avec erreur + +7. **Créer et checkout la nouvelle branche** + - `git checkout -b $NEW_BRANCH` + - La branche est créée depuis SOURCE_BRANCH (car on est dessus) + +8. **NE PAS configurer de tracking automatiquement** + - ❌ **INTERDIT** : `git branch --set-upstream-to=origin/$SOURCE_BRANCH $NEW_BRANCH` + - ✅ Le tracking sera configuré automatiquement lors du premier push avec `-u` + - ✅ Lors du push : `git push -u origin $NEW_BRANCH` + - **RAISON** : Configurer le tracking vers SOURCE_BRANCH pousse les commits sur la branche parente au lieu de créer une nouvelle branche distante + +## Expertise +Conventions de nommage des branches : +- `feature/nom-descriptif` : Nouvelles fonctionnalités +- `fix/nom-bug` : Corrections de bugs +- `issue/123-nom-descriptif` : Basé sur une issue GitHub +- Utilise des tirets, pas d'espaces ni caractères spéciaux + +## Template +```bash +# Exemple d'usage avec numéro d'issue : +/git:branch main 42 + +# Résultat attendu : +# - Récupère l'issue #42 +# - Titre: "Add user authentication system" +# - Crée la branche: issue/42-add-user-authentication-system +# - Checkout vers cette branche + +# Exemple d'usage avec texte : +/git:branch main "Add login form" + +# Résultat attendu : +# - Crée la branche: feature/add-login-form +# - Checkout vers cette branche +``` + +## Examples +```bash +# Créer une branche depuis main avec issue GitHub +/git:branch main 123 + +# Créer une branche depuis main avec texte descriptif +/git:branch main "user authentication" + +# Créer une branche depuis develop sans argument supplémentaire +/git:branch develop + +# Créer une branche depuis une branche existante avec issue +/git:branch feature/api-base 456 + +# Créer une branche fix depuis main avec texte +/git:branch main "fix login bug" +``` + +## Report +- Nom de la branche créée +- Branche source utilisée +- Issue associée (si applicable) +- Statut du checkout +- Note : Le tracking remote sera configuré lors du premier push avec `git push -u origin $NEW_BRANCH` + +## Validation +- ✅ `SOURCE_BRANCH` doit exister localement +- ✅ `SOURCE_BRANCH` est obligatoire +- ✅ **CHECKOUT vers SOURCE_BRANCH AVANT création** (CRITIQUE) +- ✅ **PULL pour mettre à jour SOURCE_BRANCH** (CRITIQUE) +- ✅ La nouvelle branche ne doit pas déjà exister +- ✅ Si `ISSUE_OR_TEXT` est un numéro, l'issue doit exister sur GitHub +- ✅ Le nom généré respecte les conventions de nommage +- ✅ Détection automatique entre numéro d'issue et texte descriptif + +## Pourquoi checkout + pull vers SOURCE_BRANCH d'abord ? + +**Problème 1 évité** : +- Si on est sur `feature/A` et on crée `feature/B` depuis `main` +- Sans checkout vers `main` d'abord, la branche est créée depuis `feature/A` +- Les commits de `feature/A` se retrouvent sur `feature/B` +- Résultat : impossible de créer une PR propre + +**Problème 2 évité** : +- Si `main` locale est en retard sur `origin/main` +- Sans pull, on crée depuis un point obsolète +- Résultat : commits manquants, conflits, PR avec historique incorrect + +**Solution** : +1. TOUJOURS faire `git checkout $SOURCE_BRANCH` +2. TOUJOURS faire `git pull origin $SOURCE_BRANCH` +3. PUIS créer avec `git checkout -b $NEW_BRANCH` diff --git a/commands/commit.md b/commands/commit.md new file mode 100644 index 0000000..ffdfc9b --- /dev/null +++ b/commands/commit.md @@ -0,0 +1,161 @@ +--- +model: claude-sonnet-4-5-20250929 +allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*), Bash(git diff:*), Bash(git log:*), Bash(git push:*) +argument-hint: [message] | --no-verify | --push +description: Créer des commits bien formatés avec format conventional et emoji +--- + +# Commit Git Intelligent + +Créer un commit bien formaté : $ARGUMENTS + +## Ce Que Fait Cette Commande + +1. Sauf si spécifié avec --no-verify, exécute automatiquement les vérifications pre-commit : + - make qa pour assurer la qualité du code +2. Vérifie quels fichiers sont stagés avec git status +3. Si 0 fichiers sont stagés, ajoute automatiquement tous les fichiers modifiés et nouveaux avec git add +4. Effectue un git diff pour comprendre les changements à commiter +5. Analyse le diff pour déterminer si plusieurs changements logiques distincts sont présents +6. Si plusieurs changements distincts sont détectés, suggère de diviser le commit en plusieurs commits plus petits +7. Pour chaque commit (ou le commit unique si pas de division), crée un message de commit utilisant le format conventional avec emoji +8. Si l'option --push est spécifiée, pousse automatiquement le(s) commit(s) vers le remote avec git push + +## Bonnes Pratiques pour les Commits + +- **Vérifier avant de commiter** : S'assurer que le code est linté, se build correctement, et que la documentation est à jour +- **Commits atomiques** : Chaque commit doit contenir des changements liés qui servent un seul objectif +- **Diviser les gros changements** : Si les changements touchent plusieurs préoccupations, les diviser en commits séparés +- **Format conventional commit** : Utiliser le format : où type est un de : + - feat: Une nouvelle fonctionnalité + - fix: Une correction de bug + - docs: Changements de documentation + - style: Changements de style de code (formatage, etc) + - refactor: Changements de code qui ne corrigent pas de bugs ni n'ajoutent de fonctionnalités + - perf: Améliorations de performance + - test: Ajout ou correction de tests + - chore: Changements du processus de build, outils, etc. +- **Présent, mode impératif** : Écrire les messages de commit comme des commandes (ex. "ajouter fonctionnalité" pas "ajouté fonctionnalité") +- **Première ligne concise** : Garder la première ligne sous 72 caractères +- **Emoji** : Chaque type de commit est associé à un emoji approprié : + - ✨ feat: Nouvelle fonctionnalité + - 🐛 fix: Correction de bug + - 📝 docs: Documentation + - 💄 style: Formatage/style + - ♻️ refactor: Refactorisation de code + - ⚡️ perf: Améliorations de performance + - ✅ test: Tests + - 🔧 chore: Outils, configuration + - 🚀 ci: Améliorations CI/CD + - 🗑️ revert: Annulation de changements + - 🧪 test: Ajouter un test qui échoue + - 🚨 fix: Corriger les warnings compilateur/linter + - 🔒️ fix: Corriger les problèmes de sécurité + - 👥 chore: Ajouter ou mettre à jour les contributeurs + - 🚚 refactor: Déplacer ou renommer des ressources + - 🏗️ refactor: Faire des changements architecturaux + - 🔀 chore: Fusionner des branches + - 📦️ chore: Ajouter ou mettre à jour les fichiers compilés ou packages + - ➕ chore: Ajouter une dépendance + - ➖ chore: Supprimer une dépendance + - 🌱 chore: Ajouter ou mettre à jour les fichiers de seed + - 🧑‍💻 chore: Améliorer l'expérience développeur + - 🧵 feat: Ajouter ou mettre à jour le code lié au multithreading ou à la concurrence + - 🔍️ feat: Améliorer le SEO + - 🏷️ feat: Ajouter ou mettre à jour les types + - 💬 feat: Ajouter ou mettre à jour le texte et les littéraux + - 🌐 feat: Internationalisation et localisation + - 👔 feat: Ajouter ou mettre à jour la logique métier + - 📱 feat: Travailler sur le design responsive + - 🚸 feat: Améliorer l'expérience utilisateur / utilisabilité + - 🩹 fix: Correction simple pour un problème non-critique + - 🥅 fix: Intercepter les erreurs + - 👽️ fix: Mettre à jour le code suite aux changements d'API externe + - 🔥 fix: Supprimer du code ou des fichiers + - 🎨 style: Améliorer la structure/format du code + - 🚑️ fix: Hotfix critique + - 🎉 chore: Commencer un projet + - 🔖 chore: Tags de release/version + - 🚧 wip: Travail en cours + - 💚 fix: Corriger le build CI + - 📌 chore: Épingler les dépendances à des versions spécifiques + - 👷 ci: Ajouter ou mettre à jour le système de build CI + - 📈 feat: Ajouter ou mettre à jour le code d'analytics ou de tracking + - ✏️ fix: Corriger les fautes de frappe + - ⏪️ revert: Annuler les changements + - 📄 chore: Ajouter ou mettre à jour la licence + - 💥 feat: Introduire des changements cassants + - 🍱 assets: Ajouter ou mettre à jour les assets + - ♿️ feat: Améliorer l'accessibilité + - 💡 docs: Ajouter ou mettre à jour les commentaires dans le code source + - 🗃️ db: Effectuer des changements liés à la base de données + - 🔊 feat: Ajouter ou mettre à jour les logs + - 🔇 fix: Supprimer les logs + - 🤡 test: Mocker des choses + - 🥚 feat: Ajouter ou mettre à jour un easter egg + - 🙈 chore: Ajouter ou mettre à jour le fichier .gitignore + - 📸 test: Ajouter ou mettre à jour les snapshots + - ⚗️ experiment: Effectuer des expériences + - 🚩 feat: Ajouter, mettre à jour, ou supprimer les feature flags + - 💫 ui: Ajouter ou mettre à jour les animations et transitions + - ⚰️ refactor: Supprimer le code mort + - 🦺 feat: Ajouter ou mettre à jour le code lié à la validation + - ✈️ feat: Améliorer le support hors ligne + +## Directives pour Diviser les Commits + +Lors de l'analyse du diff, considérer diviser les commits selon ces critères : + +1. **Préoccupations différentes** : Changements dans des parties non-liées du codebase +2. **Types de changements différents** : Mélange de fonctionnalités, corrections, refactorisation, etc. +3. **Patterns de fichiers** : Changements dans différents types de fichiers (ex. code source vs documentation) +4. **Groupement logique** : Changements qui seraient plus faciles à comprendre ou réviser séparément +5. **Taille** : Changements très larges qui seraient plus clairs s'ils étaient décomposés + +## Exemples + +Bons messages de commit : +- ✨ feat: ajouter système d'authentification utilisateur +- 🐛 fix: résoudre fuite mémoire dans le processus de rendu +- 📝 docs: mettre à jour documentation API avec nouveaux endpoints +- ♻️ refactor: simplifier la logique de gestion d'erreurs dans le parser +- 🚨 fix: résoudre warnings linter dans les fichiers de composants +- 🧑‍💻 chore: améliorer processus de setup des outils développeur +- 👔 feat: implémenter logique métier pour validation de transaction +- 🩹 fix: corriger incohérence de style mineure dans le header +- 🚑️ fix: patcher vulnérabilité de sécurité critique dans le flux d'auth +- 🎨 style: réorganiser structure des composants pour meilleure lisibilité +- 🔥 fix: supprimer code legacy déprécié +- 🦺 feat: ajouter validation d'entrée pour formulaire d'inscription utilisateur +- 💚 fix: résoudre tests CI pipeline qui échouent +- 📈 feat: implémenter tracking analytics pour engagement utilisateur +- 🔒️ fix: renforcer exigences de mot de passe d'authentification +- ♿️ feat: améliorer accessibilité des formulaires pour lecteurs d'écran + +Exemple de division de commits : +- Premier commit : ✨ feat: ajouter définitions de types pour nouvelle version solc +- Deuxième commit : 📝 docs: mettre à jour documentation pour nouvelles versions solc +- Troisième commit : 🔧 chore: mettre à jour dépendances package.json +- Quatrième commit : 🏷️ feat: ajouter définitions de types pour nouveaux endpoints API +- Cinquième commit : 🧵 feat: améliorer gestion de concurrence dans worker threads +- Sixième commit : 🚨 fix: résoudre problèmes de linting dans nouveau code +- Septième commit : ✅ test: ajouter tests unitaires pour fonctionnalités nouvelle version solc +- Huitième commit : 🔒️ fix: mettre à jour dépendances avec vulnérabilités de sécurité + +## Options de Commande + +- --no-verify: Ignorer l'exécution des vérifications pre-commit (qa) +- --push: Pousser automatiquement le(s) commit(s) vers le remote après création + +## Notes Importantes + +- Par défaut, les vérifications pre-commit (qa) s'exécuteront pour assurer la qualité du code +- Si ces vérifications échouent, il vous sera demandé si vous voulez procéder au commit quand même ou corriger les problèmes d'abord +- Si des fichiers spécifiques sont déjà stagés, la commande ne commitera que ces fichiers +- Si aucun fichier n'est stagé, elle stagera automatiquement tous les fichiers modifiés et nouveaux +- Le message de commit sera construit basé sur les changements détectés +- Avant de commiter, la commande révisera le diff pour identifier si plusieurs commits seraient plus appropriés +- Si elle suggère plusieurs commits, elle vous aidera à stager et commiter les changements séparément +- Révise toujours le diff du commit pour s'assurer que le message correspond aux changements +- Avec --push, le commit sera automatiquement poussé vers le remote après création +- Les options peuvent être combinées : /git:commit --no-verify --push diff --git a/commands/conflit.md b/commands/conflit.md new file mode 100644 index 0000000..1da6cec --- /dev/null +++ b/commands/conflit.md @@ -0,0 +1,303 @@ +--- +model: claude-sonnet-4-5-20250929 +allowed-tools: Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git merge:*), Bash(git rebase:*), Bash(git checkout:*), Bash(git add:*), Read, Edit +argument-hint: +description: Analyse les conflits git et propose à l'utilisateur une résolution pas à pas avec validation de chaque étape. +--- + +# Résolution Interactive de Conflits Git + +Résoudre les conflits git de manière interactive : $ARGUMENTS + +## Purpose +Analyser les conflits git et guider l'utilisateur dans une résolution pas à pas, fichier par fichier, avec validation à chaque étape. + +## Variables +- DESTINATION_BRANCH: $1 (branche de destination pour le merge/rebase) +- CURRENT_BRANCH: !`git branch --show-current` +- CONFLICTED_FILES: Liste des fichiers en conflit +- RESOLUTION_MODE: merge ou rebase (détecté automatiquement) + +## État Actuel du Repository + +- Branche actuelle : !`git branch --show-current` +- Status Git : !`git status --porcelain` +- Conflits détectés : !`git diff --name-only --diff-filter=U` +- Commits divergents : !`git log --oneline HEAD...$DESTINATION_BRANCH --max-count=5` + +## Ce Que Fait Cette Commande + +1. Détecte s'il y a un merge/rebase en cours ou si on doit l'initier +2. Identifie tous les fichiers en conflit +3. Pour chaque fichier en conflit : + - Affiche le contexte du conflit + - Montre les différences entre les versions + - Propose 3 stratégies de résolution + - Demande validation avant d'appliquer +4. Vérifie que tous les conflits sont résolus +5. Finalise le merge/rebase + +## Workflow + +### 1. Validation initiale + +**Vérifier DESTINATION_BRANCH obligatoire :** +- Si `DESTINATION_BRANCH` n'est pas fourni → ARRÊTER et demander à l'utilisateur + +**Vérifier que DESTINATION_BRANCH existe :** +- `git branch --list "$DESTINATION_BRANCH"` (locale) +- `git branch -r --list "origin/$DESTINATION_BRANCH"` (remote) +- Si n'existe pas → ARRÊTER avec erreur + +**Vérifier l'état du repository :** +- `git status` pour détecter : + - Merge en cours (fichiers "both modified") + - Rebase en cours (`.git/rebase-merge/` ou `.git/rebase-apply/`) + - Conflits existants + +### 2. Initier l'opération si nécessaire + +**Si aucun merge/rebase en cours :** +- Demander à l'utilisateur : "Voulez-vous merger ou rebaser $CURRENT_BRANCH sur $DESTINATION_BRANCH ?" +- Options : + 1. Merge : `git merge $DESTINATION_BRANCH` + 2. Rebase : `git rebase $DESTINATION_BRANCH` + 3. Annuler +- Exécuter l'opération choisie + +**Si l'opération échoue avec conflits :** +- Continuer avec l'analyse des conflits + +### 3. Analyse des conflits + +**Lister tous les fichiers en conflit :** +```bash +git diff --name-only --diff-filter=U +``` + +**Pour chaque fichier, collecter :** +- Chemin complet du fichier +- Nombre de sections en conflit +- Lignes concernées +- Contexte (fonction/classe/module) + +### 4. Résolution interactive par fichier + +**Pour chaque fichier en conflit :** + +**Étape A : Afficher le contexte** +- Nom du fichier et chemin +- Nombre de conflits dans ce fichier +- `git diff $FICHIER` pour voir les marqueurs de conflit + +**Étape B : Analyser les versions** +- Lire le fichier avec Read pour voir les marqueurs : + - `<<<<<<< HEAD` : version actuelle + - `=======` : séparateur + - `>>>>>>> $DESTINATION_BRANCH` : version à merger +- Afficher les différences de manière claire + +**Étape C : Proposer 3 stratégies** + +1. **Garder la version actuelle (ours)** + - `git checkout --ours $FICHIER` + - Quand : notre version est correcte, l'autre est obsolète + +2. **Garder la version entrante (theirs)** + - `git checkout --theirs $FICHIER` + - Quand : leur version est correcte, la nôtre est obsolète + +3. **Résolution manuelle** + - Utiliser Edit pour fusionner manuellement + - Supprimer les marqueurs `<<<<<<<`, `=======`, `>>>>>>>` + - Combiner les changements pertinents des deux versions + +**Étape D : Demander confirmation** +- Afficher un résumé de la stratégie choisie +- Demander : "Voulez-vous appliquer cette résolution ? (oui/non/voir le diff)" +- Si "voir le diff" : montrer le résultat avec `git diff --cached $FICHIER` + +**Étape E : Appliquer la résolution** +- Exécuter la stratégie choisie +- Marquer le fichier comme résolu : `git add $FICHIER` +- Confirmer : "✅ Conflit résolu dans $FICHIER" + +### 5. Vérification finale + +**Après avoir résolu tous les fichiers :** +```bash +git status +``` +- Vérifier qu'il n'y a plus de fichiers "both modified" +- Vérifier que tous les fichiers conflictuels sont stagés + +**Demander confirmation finale :** +- "Tous les conflits sont résolus. Voulez-vous finaliser ?" +- Options : + 1. Oui, finaliser + 2. Non, réviser les changements + 3. Annuler tout (abort) + +### 6. Finalisation + +**Si merge :** +```bash +git commit --no-edit +# ou si l'utilisateur veut personnaliser : +git commit -m "Merge branch '$DESTINATION_BRANCH' into $CURRENT_BRANCH" +``` + +**Si rebase :** +```bash +git rebase --continue +``` + +**Si annulation demandée :** +```bash +git merge --abort # ou +git rebase --abort +``` + +## Stratégies de Résolution Détaillées + +### Stratégie 1 : Garder la version actuelle (ours) +- Utilisation : Quand notre implémentation est plus récente/correcte +- Commande : `git checkout --ours $FICHIER && git add $FICHIER` +- Attention : Perte des changements de l'autre branche + +### Stratégie 2 : Garder la version entrante (theirs) +- Utilisation : Quand la version à merger est plus récente/correcte +- Commande : `git checkout --theirs $FICHIER && git add $FICHIER` +- Attention : Perte de nos changements + +### Stratégie 3 : Résolution manuelle intelligente +- Utilisation : Quand les deux versions contiennent des changements valides +- Processus : + 1. Lire le fichier avec Read + 2. Identifier les sections en conflit + 3. Analyser la logique de chaque version + 4. Utiliser Edit pour fusionner : + - Garder les imports/dépendances des deux côtés + - Fusionner la logique métier intelligemment + - Supprimer tous les marqueurs de conflit + 5. Vérifier la syntaxe du résultat + 6. `git add $FICHIER` + +## Examples + +### Exemple 1 : Merge avec conflits +```bash +# Situation : on est sur feature/auth, on veut merger main +/git:conflit main + +# Claude détecte : pas de merge en cours +# Claude demande : "Voulez-vous merger main dans feature/auth ?" +# Utilisateur : "oui, merge" +# Claude exécute : git merge main +# Conflits détectés dans : src/auth.php, config/app.php +# Claude guide la résolution fichier par fichier +``` + +### Exemple 2 : Rebase en cours avec conflits +```bash +# Situation : rebase en cours, 3 fichiers en conflit +/git:conflit develop + +# Claude détecte : rebase en cours sur develop +# Claude liste : file1.php, file2.js, file3.md +# Claude résout interactivement chaque fichier +# Claude finalise : git rebase --continue +``` + +### Exemple 3 : Résolution manuelle complexe +```bash +/git:conflit main + +# Conflit dans src/payment.php : +# HEAD : ajout méthode processRefund() +# main : ajout méthode processChargeback() +# Stratégie : Résolution manuelle +# Claude fusionne les deux méthodes +# Validation : utilisateur confirme +``` + +## Report Format + +```markdown +# Rapport de Résolution de Conflits + +## Configuration +- Branche actuelle : $CURRENT_BRANCH +- Branche destination : $DESTINATION_BRANCH +- Type d'opération : merge/rebase + +## Conflits Détectés +- Nombre total de fichiers : X +- Fichiers résolus : Y +- Fichiers restants : Z + +## Résolutions Appliquées + +### Fichier : src/auth.php +- Stratégie : Résolution manuelle +- Raison : Fusion de deux implémentations valides +- Lignes modifiées : 42-58 + +### Fichier : config/app.php +- Stratégie : Garder version actuelle (ours) +- Raison : Configuration locale spécifique + +## Statut Final +✅ Tous les conflits résolus +✅ Merge/rebase finalisé avec succès +``` + +## Best Practices + +### Avant de commencer +- ✅ S'assurer que le working directory est propre +- ✅ Avoir une sauvegarde (commit ou stash) +- ✅ Comprendre les changements des deux branches + +### Pendant la résolution +- ✅ Résoudre un fichier à la fois +- ✅ Tester la syntaxe après chaque résolution manuelle +- ✅ Ne jamais garder les marqueurs de conflit (<<<<, ====, >>>>) +- ✅ Valider que la logique est cohérente +- ✅ En cas de doute, demander à l'utilisateur + +### Après la résolution +- ✅ Vérifier que le code compile/s'exécute +- ✅ Lancer les tests si disponibles +- ✅ Réviser le diff final avant commit + +## Messages d'Erreur et Solutions + +### "error: you need to resolve your current index first" +- Cause : Conflits non résolus +- Solution : Continuer la résolution ou faire `git merge --abort` + +### "no changes added to commit" +- Cause : Fichiers résolus mais non stagés +- Solution : `git add $FICHIER` après chaque résolution + +### "conflict (content): Merge conflict in X" +- Cause : Changements incompatibles dans le même fichier +- Solution : Résoudre avec une des 3 stratégies + +## Validation + +- ✅ DESTINATION_BRANCH doit exister (locale ou remote) +- ✅ Tous les fichiers en conflit doivent être traités +- ✅ Aucun marqueur de conflit ne doit rester dans les fichiers +- ✅ Tous les fichiers résolus doivent être stagés +- ✅ L'utilisateur doit valider avant chaque résolution +- ✅ L'utilisateur doit confirmer avant la finalisation + +## Notes Importantes + +- La commande est 100% interactive : chaque action nécessite validation +- L'utilisateur garde le contrôle total du processus +- Possibilité d'annuler à tout moment avec merge/rebase --abort +- Les résolutions manuelles utilisent Edit pour garantir la qualité +- Un rapport détaillé est généré à la fin diff --git a/commands/pr.md b/commands/pr.md new file mode 100644 index 0000000..0d7ea75 --- /dev/null +++ b/commands/pr.md @@ -0,0 +1,6 @@ +--- +description: Crée une Pull Request optimisée avec workflow structuré +argument-hint: [branch-base, milestone, project, --delete, --no-review] +--- + +You must use the Skill tool to invoke the "git-pr" skill with the following arguments. diff --git a/commands/release-notes.md b/commands/release-notes.md new file mode 100644 index 0000000..4c449d1 --- /dev/null +++ b/commands/release-notes.md @@ -0,0 +1,6 @@ +--- +description: Génère des notes de release HTML orientées utilisateurs finaux +argument-hint: " [nom-release]" +--- + +You must use the Skill tool to invoke the "git:release-notes" skill. diff --git a/commands/release-report.md b/commands/release-report.md new file mode 100644 index 0000000..cf059f9 --- /dev/null +++ b/commands/release-report.md @@ -0,0 +1,270 @@ +--- +name: git:release-report +description: Génère un rapport HTML d'analyse d'impact entre deux branches +argument-hint: [branch-source, branche-cible, nom-release] +arguments: + - name: branche-source + description: Branche source à analyser (ex release/v27.0.0) + required: true + - name: branche-cible + description: Branche de référence (ex main ou develop) + required: true + - name: nom-release + description: Nom de la release pour le fichier (optionnel) + required: false +--- + +# Générer rapport d'analyse de release + +Génère un rapport HTML détaillé comparant deux branches pour analyser l'impact d'une release. + +## Usage + +```bash +/git:release-report [nom-release] +``` + +**Exemples :** +- `/git:release-report release/v27.0.0 main` +- `/git:release-report release/v27.0.0 develop v27.0.0` +- `/git:release-report feature/new-module main "Module XYZ"` + +## Description + +Cette commande génère un rapport HTML orienté Product Owner qui analyse : + +1. **Statistiques globales** : fichiers modifiés, lignes ajoutées/supprimées, commits +2. **Répartition par type de fichier** : PHP, Twig, JS, etc. +3. **Fonctionnalités principales** : extraction depuis les commits +4. **Impact métier** : par domaine fonctionnel +5. **Qualité & maintenabilité** : évolution du code + +Le rapport est généré dans `REPORT_PATH/impact_.html` + +## Variables + +REPORT_PATH: `.claude/reports` + +Variables à extraire des arguments : + +- `$BRANCH_SOURCE` : Branche source à analyser (ex: release/v27.0.0) +- `$BRANCH_TARGET` : Branche de référence (ex: main ou develop) +- `$RELEASE_NAME` : Nom de la release pour le fichier (ex: v27.0.0) + +Si `$RELEASE_NAME` n'est pas fourni, utiliser le nom de `$BRANCH_SOURCE` en retirant le préfixe "release/" + +## Workflow + +### 0. Vérification des arguments obligatoires + +**AVANT TOUTE EXÉCUTION**, vérifier que les arguments obligatoires sont fournis : + +1. Si `$BRANCH_SOURCE` est manquant : + - Utiliser `AskUserQuestion` pour demander la branche source + - Question : "Quelle est la branche source à analyser ?" + - Proposer les branches récentes de type `release/*` ou `feature/*` + +2. Si `$BRANCH_TARGET` est manquant : + - Utiliser `AskUserQuestion` pour demander la branche cible + - Question : "Quelle est la branche de référence ?" + - Options suggérées : `main`, `develop`, `master` + +**Ne pas continuer** tant que les deux arguments obligatoires ne sont pas fournis. + +### 1. Validation des paramètres + +```bash +# Vérifier que les branches existent +git rev-parse --verify $BRANCH_SOURCE +git rev-parse --verify $BRANCH_TARGET + +# Vérifier qu'il y a des différences +DIFF_COUNT=$(git rev-list --count $BRANCH_TARGET..$BRANCH_SOURCE) +if [ $DIFF_COUNT -eq 0 ]; then + echo "Aucune différence entre les branches" + exit 1 +fi +``` + +### 2. Collecte des statistiques git + +Exécuter les commandes suivantes en parallèle : + +```bash +# Statistiques globales +git diff --stat $BRANCH_TARGET..$BRANCH_SOURCE | tail -1 + +# Nombre total de fichiers modifiés et détails lignes +git diff --numstat $BRANCH_TARGET..$BRANCH_SOURCE | \ + awk '{files++; added+=$1; deleted+=$2} END {print files, added, deleted}' + +# Répartition par type de fichier +git diff --numstat $BRANCH_TARGET..$BRANCH_SOURCE | python3 -c " +import sys +from collections import defaultdict + +stats = defaultdict(lambda: {'count': 0, 'added': 0, 'deleted': 0}) + +for line in sys.stdin: + parts = line.strip().split('\t') + if len(parts) < 3: + continue + + added = int(parts[0]) if parts[0] != '-' else 0 + deleted = int(parts[1]) if parts[1] != '-' else 0 + path = parts[2] + + ext = 'autre' + if path.endswith('.php'): + ext = 'php' + elif path.endswith('.twig'): + ext = 'twig' + elif path.endswith(('.yml', '.yaml')): + ext = 'yaml' + elif path.endswith('.js'): + ext = 'js' + elif path.endswith('.scss'): + ext = 'scss' + elif path.endswith('.md'): + ext = 'md' + elif path.endswith('.json'): + ext = 'json' + elif path.endswith('.sh'): + ext = 'sh' + + stats[ext]['count'] += 1 + stats[ext]['added'] += added + stats[ext]['deleted'] += deleted + stats[ext]['total'] = stats[ext]['added'] + stats[ext]['deleted'] + +for ext in sorted(stats.items(), key=lambda x: x[1]['total'], reverse=True): + name = ext[0] + data = ext[1] + print(f'{name}|{data[\"count\"]}|{data[\"added\"]}|{data[\"deleted\"]}|{data[\"total\"]}') +" + +# Types de modifications (A/M/D/R) +git diff --name-status $BRANCH_TARGET..$BRANCH_SOURCE | cut -f1 | sort | uniq -c + +# Nombre de commits +git rev-list --count $BRANCH_TARGET..$BRANCH_SOURCE + +# Total fichiers dans la branche cible +git ls-tree -r $BRANCH_TARGET --name-only | wc -l + +# Top commits avec features/fixes +git log $BRANCH_TARGET..$BRANCH_SOURCE --oneline --no-merges | \ + grep -E "(feat|feature|✨|🚀|📝|🐛|fix|♻️|refactor)" | head -50 +``` + +### 3. Analyse des domaines fonctionnels + +Analyser les commits pour identifier les domaines principaux impactés : + +- Grouper par préfixe de commit (AVENANT, DOSSIER, APF, etc.) +- Identifier les patterns récurrents +- Extraire les fonctionnalités majeures avec leur impact + +### 4. Génération du rapport HTML + +Utiliser le template suivant et remplir avec les données collectées : + +**Structure du rapport :** + +1. **Header** avec executive summary + - Nom de la release + - Résumé en 2-3 phrases + - Chiffres clés (% code modifié, commits, variation nette) + +2. **KPI Grid** (4 indicateurs visuels) + - % du code modifié + - Nombre de commits + - Variation nette (simplification/ajout) + - Domaine principal (ex: "70% focus AVENANTS") + +3. **Fonctionnalités principales** (cards avec impact) + - Impact Très élevé (rouge) + - Impact Élevé (orange) + - Impact Moyen (jaune) + - Impact Faible (vert) + +4. **Corrections majeures** + - Liste des bugs corrigés + +5. **Qualité & Maintenabilité** + - Code simplifié + - Documentation + - Refactoring + - Interface + +6. **Vue d'ensemble technique** + - Chart bars par type de fichier + - Métrique périmètre impacté + +7. **Impact business** (4 axes) + - Gestion administrative + - Communication + - Performance + - Sécurité + +**Style CSS :** +- Gradient violet (#667eea → #764ba2) +- Cards avec border-left coloré selon impact +- Progress bars animées +- KPI boxes avec fond dégradé +- Design responsive + +### 5. Sauvegarde du fichier + +```bash +OUTPUT_FILE="REPORT_PATH/impact_${RELEASE_NAME}.html" +mkdir -p REPORT_PATH +# Écrire le contenu HTML généré +echo "Rapport généré : $OUTPUT_FILE" +``` + +## Format de sortie + +Le rapport doit être : + +- **Orienté Product Owner** : focus sur l'impact métier, pas les détails techniques +- **Visuel** : KPI, charts, couleurs par niveau d'impact +- **Actionnable** : lister les features, bugs corrigés, améliorations +- **Comparable** : même structure pour toutes les releases + +## Règles importantes + +1. **NE PAS** utiliser de termes techniques obscurs +2. **TOUJOURS** expliquer l'impact utilisateur +3. **GROUPER** les changements par domaine fonctionnel +4. **QUANTIFIER** l'impact (%, nombre, ratio) +5. **PRIORISER** par impact métier (Très élevé → Faible) + +## Exemple d'exécution + +```bash +$ /git:release-report release/v27.0.0 main + +Analyse de release/v27.0.0 vs main... + +✓ Collecte statistiques git +✓ Analyse types de fichiers +✓ Extraction fonctionnalités +✓ Génération rapport HTML + +Rapport généré : REPORT_PATH/impact_v27.0.0.html + +Résumé : +- 1 250 fichiers modifiés (17.5% du codebase) +- 45 320 lignes ajoutées, 38 100 lignes supprimées +- +7 220 lignes nettes (+10.2%) +- 780 commits +- Focus principal : NOTIFICATIONS (65%) +``` + +## Notes + +- Le rapport est auto-suffisant (HTML avec CSS inline) +- Compatible tous navigateurs modernes +- Peut être imprimé ou converti en PDF +- Les couleurs suivent le design system du projet diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..8087c1d --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,157 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:atournayre/claude-marketplace:git", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "98e9eebc3d592a577e2edf3a276683691261d8bd", + "treeHash": "c89759d4291f7378025eeb4d894e6fd1ea49f0e580cba2ee04698b9cff3fad10", + "generatedAt": "2025-11-28T10:13:59.784186Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "git", + "description": "Workflow Git complet : branches, commits, conflits, PR avec automation QA", + "version": "1.4.10" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "ea1a63e12bf87382ed6840e7545257dcaec49109e919689dab31f59782928ba6" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "c7d8a77a31f2709c837ecae7513f5e7a221b6ba637651e916daaa581d6364745" + }, + { + "path": "commands/release-notes.md", + "sha256": "69f0d84ffcffdf029d7dc6361af07a36d73632b04110969a441ef70e09661f8b" + }, + { + "path": "commands/release-report.md", + "sha256": "ed9d4bbee99a67fb01f41d7678708673aaaabfc2e917238cf49b189777f87373" + }, + { + "path": "commands/conflit.md", + "sha256": "23d90e64267685580e1b55eabf23744f3f56fcfc0520a16e652172b6adff9d72" + }, + { + "path": "commands/pr.md", + "sha256": "aea873052c8d1d74bbc94f3d1e5901aea80a149e10ac8f3cac7d995ca8d14196" + }, + { + "path": "commands/branch.md", + "sha256": "1831249b22338a0f1b1865026e2b1f67eada8f58c807e53fe55e7936cacb7253" + }, + { + "path": "commands/commit.md", + "sha256": "377fa014bddc3c89b8398e58d71e452031a709f3b676786aad501978785366bc" + }, + { + "path": "skills/release-notes/SKILL.md", + "sha256": "f5ad25a5beae3378cc5ed2f134f65c15f768cae8ddbf5525279c018e87955156" + }, + { + "path": "skills/git-pr/README.md", + "sha256": "d29792c0d324d5a992b84e9670ae04a6f801e1f139c7027207fd85b38e447017" + }, + { + "path": "skills/git-pr/SKILL.md", + "sha256": "a694f05ca0205d5313fdb011f30e3757c073d68b1595d3094398a07ad51bf248" + }, + { + "path": "skills/git-pr/.scope-consistency.md", + "sha256": "17403400ef95e0632f5d2e698afad4b4fad7107a6814d0c3f13f5c6d7b75d090" + }, + { + "path": "skills/git-pr/tests/test_project_cache.py", + "sha256": "9aef79bc287b2b0333845542f0e8726ab71127f73732579728b27452bc0ee20f" + }, + { + "path": "skills/git-pr/tests/.gitignore", + "sha256": "8967b41828f2d5926dfb060c41059c2ddbb56714788b74c5950776787e56cfd2" + }, + { + "path": "skills/git-pr/tests/run_tests.sh", + "sha256": "3a030d068fe8e12fb355eff97364398bd70b60c8e935e6ffd46268d89fd94d5a" + }, + { + "path": "skills/git-pr/tests/test_milestone_cache.py", + "sha256": "b0f39908d51702290298a4f0fa106b7431cdcedec0767830e263ea45d9d1a51f" + }, + { + "path": "skills/git-pr/scripts/milestone_cache.py", + "sha256": "f743ad0dc9243823f598dfc5d09de2dc75617191244dfee56ed73f31a5f31996" + }, + { + "path": "skills/git-pr/scripts/cleanup_branch.sh", + "sha256": "53b23ef46c9d6de8959a1d51100cf26644aea285001892a6e35e1b858aec1a51" + }, + { + "path": "skills/git-pr/scripts/final_report.sh", + "sha256": "8af34c388538d7e3e80a965336c75943d4d6e1b8a804d98f56d16c487a177549" + }, + { + "path": "skills/git-pr/scripts/assign_project.py", + "sha256": "f6a6fd248f1babd5d4dc1c92420fbce0112e65b3b5d817052408a2d26fee3d89" + }, + { + "path": "skills/git-pr/scripts/confirm_base_branch.py", + "sha256": "9bab042d1dd39f1ed560db87c3cca47533b8e9a1c4a4ba470a2019772a6cb2f7" + }, + { + "path": "skills/git-pr/scripts/verify_pr_template.sh", + "sha256": "750092b980b4861d2ee2d167ae2f317679b884fb735bc28aa922e5e9dab0620c" + }, + { + "path": "skills/git-pr/scripts/safe_push_pr.sh", + "sha256": "3375a7d44c9a197298db4549aaab78882a34dc700e7010398422060cecfc305f" + }, + { + "path": "skills/git-pr/scripts/project_cache.py", + "sha256": "e61da785c9f834567047ae01792a6fc3219b5221a3705cd293b0562a243b8c0d" + }, + { + "path": "skills/git-pr/scripts/analyze_changes.sh", + "sha256": "106648f6781b4f934fb24ba2cd5720fa28772f2191227b8e05bb9d673876fabf" + }, + { + "path": "skills/git-pr/scripts/smart_qa.sh", + "sha256": "c6a813f05c4e484caef8533f12bd9043540ba60bbe2ea0ea11603cc8a89440aa" + }, + { + "path": "skills/git-pr/scripts/assign_milestone.py", + "sha256": "d20cf3b2904fbc0f502cf490a7e913c616b40a233a6854e126c1851a4debea66" + }, + { + "path": "skills/git-pr/scripts/create_pr.sh", + "sha256": "0363bd2aec85de0492273d270537f0420033d9383980fe674f6a22deddf8778f" + }, + { + "path": "skills/git-pr/scripts/check_scopes.sh", + "sha256": "57d14de8af8992985ca9460d6ccedd41d92fe910f5be037eb2c22d9a107ee015" + }, + { + "path": "skills/git-pr/scripts/auto_review.sh", + "sha256": "05dc55caf354d71acdd7693b86a585f77ca062e1ad1dae927e34967437d4b666" + }, + { + "path": "skills/git-pr/scripts/gh_auth_setup.sh", + "sha256": "74031c0569eee3f3431e47832f969c318d5c1400101faff5cc8711760b4b4115" + } + ], + "dirSha256": "c89759d4291f7378025eeb4d894e6fd1ea49f0e580cba2ee04698b9cff3fad10" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/git-pr/.scope-consistency.md b/skills/git-pr/.scope-consistency.md new file mode 100644 index 0000000..0a48eda --- /dev/null +++ b/skills/git-pr/.scope-consistency.md @@ -0,0 +1,127 @@ +# Consistance des Scopes GitHub + +## ⚠️ IMPORTANT: Source de Vérité Unique + +Pour garantir la cohérence des scopes GitHub demandés, **UNE SEULE source de vérité existe**: + +### 📍 Fichier de Référence + +``` +scripts/gh_auth_setup.sh +``` + +**Array `REQUIRED_SCOPES`** ligne ~4-10: + +```bash +REQUIRED_SCOPES=( + "repo" # Accès complet aux repos (PRs, commits, etc.) + "read:org" # Lecture infos organisation + "read:project" # Lecture projets GitHub + "project" # Écriture/assignation aux projets + "gist" # Gestion des gists +) +``` + +## 🔒 Règles Strictes + +### DO ✅ + +1. **Modifier uniquement `gh_auth_setup.sh`** pour ajouter/retirer des scopes +2. **Utiliser systématiquement** `bash gh_auth_setup.sh` pour renouveler l'auth +3. **Vérifier les scopes** à chaque début de workflow (Étape 1.5 dans SKILL.md) +4. **Documenter** dans ce fichier toute modification de scopes + +### DON'T ❌ + +1. ❌ **NE JAMAIS** utiliser `gh auth refresh -s ` manuellement sans passer par le script +2. ❌ **NE JAMAIS** hard-coder les scopes dans SKILL.md ou ailleurs +3. ❌ **NE JAMAIS** supposer qu'un scope est présent sans vérification + +## 📋 Checklist Modification Scopes + +Si ajout/retrait de scope nécessaire: + +- [ ] Modifier `REQUIRED_SCOPES` dans `scripts/gh_auth_setup.sh` +- [ ] Mettre à jour section "Scopes Requis" dans `SKILL.md` +- [ ] Mettre à jour tableau dans `README.md` +- [ ] Documenter dans ce fichier (section Historique) +- [ ] Tester avec `bash scripts/gh_auth_setup.sh` +- [ ] Vérifier avec `gh auth status` +- [ ] Tester le workflow complet + +## 📜 Historique des Modifications + +### 2025-11-05 - Initialisation + +**Scopes définis**: `repo`, `read:org`, `read:project`, `project`, `gist` + +**Contexte**: Création du système de consistance suite à oubli intermittent de scopes lors des renouvellements d'authentification. + +**Changements**: +- Création `scripts/gh_auth_setup.sh` (script centralisant TOUS les scopes) +- Ajout Étape 1.5 dans `SKILL.md` (vérification automatique scopes) +- Création `README.md` (documentation complète) +- Création `.scope-consistency.md` (ce fichier - règles de cohérence) + +**Impact**: Garantit qu'aucun scope ne sera oublié lors des futurs renouvellements. + +--- + +### Template Modification Future + +```markdown +### YYYY-MM-DD - [Titre Modification] + +**Scopes modifiés**: `scope1`, `scope2` + +**Contexte**: [Raison de la modification] + +**Changements**: +- [Détail changement 1] +- [Détail changement 2] + +**Impact**: [Impact sur le workflow] +``` + +## 🔍 Débogage + +### Symptôme: "Scopes manquants" malgré renouvellement récent + +**Diagnostic**: +```bash +# 1. Vérifier scopes actuels +gh auth status | grep "Token scopes" + +# 2. Comparer avec REQUIRED_SCOPES +cat scripts/gh_auth_setup.sh | grep -A 10 "REQUIRED_SCOPES" +``` + +**Solution**: +```bash +# Re-run le script de setup +bash scripts/gh_auth_setup.sh +``` + +### Symptôme: Script fonctionne mais assignation projet échoue + +**Diagnostic**: +```bash +# Vérifier que 'project' ET pas seulement 'read:project' +gh auth status | grep -o "project" +``` + +**Cause Probable**: Token ancien avec seulement `read:project` + +**Solution**: +```bash +# Logout complet puis re-setup +gh auth logout +bash scripts/gh_auth_setup.sh +``` + +## 📚 Références + +- Script source: `scripts/gh_auth_setup.sh` +- Workflow: `SKILL.md` (Étape 1.5) +- Documentation: `README.md` +- GitHub Scopes Doc: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps diff --git a/skills/git-pr/README.md b/skills/git-pr/README.md new file mode 100644 index 0000000..b7ba266 --- /dev/null +++ b/skills/git-pr/README.md @@ -0,0 +1,211 @@ +# Git PR Skill - Documentation + +## Configuration Initiale + +### Authentification GitHub + +Ce skill nécessite une authentification GitHub avec des scopes spécifiques. + +#### Scopes Requis + +| Scope | Description | Usage | +|-------|-------------|-------| +| `repo` | Accès complet aux repos | Création PR, lecture commits, gestion branches | +| `read:org` | Lecture infos organisation | Récupération infos repository organisation | +| `read:project` | Lecture projets GitHub | Liste et lecture des projets | +| `project` | Écriture aux projets | Assignation PR aux projets | +| `gist` | Gestion gists | Partage extraits de code si nécessaire | + +#### Configuration Automatique (Recommandé) + +```bash +bash scripts/gh_auth_setup.sh +``` + +Ce script configure automatiquement TOUS les scopes requis. + +#### Configuration Manuelle + +```bash +gh auth refresh --hostname github.com \ + -s repo \ + -s read:org \ + -s read:project \ + -s project \ + -s gist +``` + +#### Vérification + +```bash +gh auth status +``` + +Sortie attendue: +``` +Token scopes: 'gist', 'project', 'read:org', 'repo' +``` + +⚠️ **Note**: `read:project` n'apparaît pas toujours explicitement mais est inclus avec `project`. + +## Protection Contre les Oublis + +Le skill vérifie automatiquement les scopes au démarrage (Étape 1.5). + +Si un scope manque : +- ❌ Arrêt immédiat avec message d'erreur +- 📋 Liste des scopes manquants affichée +- 🔄 Commande de renouvellement suggérée + +## Utilisation + +### Via Slash Command + +```bash +/git:pr [milestone] [projet] [--delete] [--no-review] +``` + +### Via Skill Direct + +```bash +ARGUMENTS="develop 1.0.0 TMA --delete" skill dev:git-pr +``` + +## Dépannage + +### Erreur: "Scopes manquants" + +```bash +❌ Scopes GitHub manquants: read:project project +``` + +**Solution**: Relancer `gh_auth_setup.sh` + +### Erreur: "Impossible de récupérer les projets" + +Vérifier authentification: +```bash +gh auth status +``` + +Si scopes corrects mais erreur persiste: +```bash +# Forcer renouvellement +gh auth logout +bash scripts/gh_auth_setup.sh +``` + +### Erreur: "your authentication token is missing required scopes" + +**Cause**: Token obsolète ou scopes révoqués + +**Solution**: +```bash +bash scripts/gh_auth_setup.sh +``` + +## Cache Persistant + +Le skill utilise un système de cache pour optimiser les performances. + +### Cache Milestones + +**Fichier**: `.claude/cache/git-milestones.json` + +**Fonctionnalités**: +- Stockage des milestones GitHub +- Recherche par titre exact ou alias +- Normalisation semver automatique (`26` → `26.0.0`) +- Génération d'aliases depuis titres (`26.0.0 (Hotfix)` → alias `26.0.0`) + +**Refresh**: Automatique si milestone introuvable + +### Cache Projets + +**Fichier**: `.claude/cache/git-projects.json` + +**Fonctionnalités**: +- Stockage des projets GitHub +- Recherche case-insensitive par titre ou alias +- Génération d'aliases depuis mots-clés (`Bug Tracking` → `["bug", "tracking"]`) + +**Refresh**: Automatique si projet introuvable + +### Commandes Utiles + +```bash +# Vider cache milestones +rm .claude/cache/git-milestones.json + +# Vider cache projets +rm .claude/cache/git-projects.json + +# Vider tout le cache +rm -rf .claude/cache/ +``` + +## Architecture + +``` +git-pr/ +├── SKILL.md # Définition du skill et workflow +├── README.md # Cette documentation +├── scripts/ +│ ├── gh_auth_setup.sh # Configuration automatique auth +│ ├── verify_pr_template.sh +│ ├── smart_qa.sh +│ ├── analyze_changes.sh +│ ├── confirm_base_branch.py +│ ├── safe_push_pr.sh +│ ├── assign_milestone.py # Assignation milestone avec cache +│ ├── milestone_cache.py # Module cache milestones +│ ├── assign_project.py # Assignation projet avec cache +│ ├── project_cache.py # Module cache projets +│ └── cleanup_branch.sh +└── tests/ + ├── run_tests.sh # Lance tous les tests + ├── test_milestone_cache.py # Tests unitaires milestones + └── test_project_cache.py # Tests unitaires projets +``` + +## Maintenance + +### Ajout d'un Nouveau Scope + +1. Modifier `gh_auth_setup.sh`: + ```bash + REQUIRED_SCOPES=( + # ... scopes existants + "nouveau_scope" # Description + ) + ``` + +2. Modifier `SKILL.md` section "Scopes Requis" + +3. Mettre à jour ce README + +### Tests Unitaires + +```bash +# Lancer tous les tests +bash tests/run_tests.sh + +# Lancer un test spécifique +cd tests +python3 test_milestone_cache.py -v +python3 test_project_cache.py -v +``` + +### Test de Consistance + +Après modification: +```bash +# Tester le script +bash scripts/gh_auth_setup.sh + +# Vérifier +gh auth status + +# Tester assignation projet +python3 scripts/assign_project.py --project +``` diff --git a/skills/git-pr/SKILL.md b/skills/git-pr/SKILL.md new file mode 100644 index 0000000..6a79352 --- /dev/null +++ b/skills/git-pr/SKILL.md @@ -0,0 +1,326 @@ +--- +name: git-pr +description: > + Automatise la création de Pull Requests GitHub avec workflow complet incluant: + QA intelligente (PHP), commits structurés, assignation milestone et projet, + code review automatique. Utilisable via /git:pr ou invocation automatique + quand l'utilisateur demande à créer/ouvrir/faire une PR. +allowed-tools: [Bash, Read, Write, TodoWrite, AskUserQuestion] +model: claude-sonnet-4-5-20250929 +--- + +# Git PR Skill - Checklist d'exécution + +## Configuration + +```bash +SCRIPTS_DIR="/home/atournayre/.claude/plugins/marketplaces/atournayre-claude-plugin-marketplace/git/skills/git-pr/scripts" +PR_TEMPLATE_PATH=".github/pull_request_template.md" +``` + +## Checklist d'exécution + +### 1. Initialisation + +EXÉCUTER : +```bash +START_TIME=$(date +%s) +``` + +EXÉCUTER TodoWrite avec ces todos exacts : +```yaml +todos: + - content: "Vérifier template PR" + status: "pending" + activeForm: "Vérification du template PR" + - content: "Lancer QA intelligente" + status: "pending" + activeForm: "Lancement de la QA intelligente" + - content: "Analyser changements git" + status: "pending" + activeForm: "Analyse des changements git" + - content: "Confirmer branche de base" + status: "pending" + activeForm: "Confirmation de la branche de base" + - content: "Générer description PR intelligente" + status: "pending" + activeForm: "Génération de la description PR intelligente" + - content: "Push et création PR" + status: "pending" + activeForm: "Push et création de la PR" + - content: "Assigner milestone" + status: "pending" + activeForm: "Assignation du milestone" + - content: "Assigner projet GitHub" + status: "pending" + activeForm: "Assignation du projet GitHub" + - content: "Code review automatique" + status: "pending" + activeForm: "Code review automatique" + - content: "Nettoyage branche locale" + status: "pending" + activeForm: "Nettoyage de la branche locale" +``` + +### 2. Parsing arguments + +EXÉCUTER ce bloc pour parser $ARGUMENTS : +```bash +ARGS=($ARGUMENTS) +BRANCH_BASE="" +MILESTONE="" +PROJECT_NAME="" +DELETE_FLAG="" +NO_REVIEW_FLAG="" + +for arg in "${ARGS[@]}"; do + case "$arg" in + --delete) DELETE_FLAG="--delete" ;; + --no-review) NO_REVIEW_FLAG="--no-review" ;; + *) + if [ -z "$BRANCH_BASE" ]; then + BRANCH_BASE="$arg" + elif [ -z "$MILESTONE" ]; then + MILESTONE="$arg" + elif [ -z "$PROJECT_NAME" ]; then + PROJECT_NAME="$arg" + fi + ;; + esac +done +``` + +### 3. Vérification scopes GitHub + +EXÉCUTER : +```bash +bash $SCRIPTS_DIR/check_scopes.sh +``` + +- Exit 0 → continuer +- Exit 1 → ARRÊT, afficher message du script + +### 4. Template PR + +- Marquer todo #1 in_progress + +EXÉCUTER : +```bash +bash $SCRIPTS_DIR/verify_pr_template.sh "$PR_TEMPLATE_PATH" +``` + +- Exit 0 → marquer todo #1 completed +- Exit 1 → ARRÊT + +### 5. QA (si pas --no-review) + +- Marquer todo #2 in_progress + +EXÉCUTER : +```bash +bash $SCRIPTS_DIR/smart_qa.sh +``` + +- Exit 0 → marquer todo #2 completed +- Exit 1 → ARRÊT + +### 6. Analyse changements + +- Marquer todo #3 in_progress + +EXÉCUTER : +```bash +bash $SCRIPTS_DIR/analyze_changes.sh +``` + +- Stocker sortie JSON +- Marquer todo #3 completed + +### 7. Branche de base + +- Marquer todo #4 in_progress + +Si BRANCH_BASE fourni : +```bash +python3 $SCRIPTS_DIR/confirm_base_branch.py --branch "$BRANCH_BASE" +``` + +Sinon : +```bash +python3 $SCRIPTS_DIR/confirm_base_branch.py +``` + +Si needs_user_input: true → utiliser AskUserQuestion : +```yaml +questions: + - question: "Quelle branche de base pour la PR ?" + header: "Branche" + multiSelect: false + options: + - label: "develop" + description: "Branche développement" + - label: "main" + description: "Branche production" +``` + +- Marquer todo #4 completed + +### 8. Génération description intelligente + +- Marquer todo "Générer description PR intelligente" in_progress + +EXÉCUTER pour récupérer les informations : +```bash +BRANCH_NAME=$(git branch --show-current) +echo "=== COMMITS ===" +git log $BRANCH_BASE..$BRANCH_NAME --oneline +echo "" +echo "=== DIFF STAT ===" +git diff $BRANCH_BASE..$BRANCH_NAME --stat +echo "" +echo "=== FICHIERS MODIFIÉS ===" +git diff $BRANCH_BASE..$BRANCH_NAME --name-only +``` + +LIRE le template PR avec Read tool : `$PR_TEMPLATE_PATH` + +**GÉNÉRER LA DESCRIPTION** en tant que Claude : +1. Analyser les commits et le diff +2. Remplir intelligemment chaque section du template : + - **Bug fix** : supprimer si pas de fix, sinon lier l'issue + - **Description** : résumer les changements basé sur les commits + - **Type de changement** : cocher (✔️) les types appropriés basé sur les commits + - **Tests** : indiquer si tests ajoutés/modifiés + - **Checklist** : cocher ce qui s'applique + - **Actions** : cocher ce qui est nécessaire +3. Sauvegarder dans `/tmp/pr_body_generated.md` avec Write tool + +### 9. Création PR + +EXÉCUTER : +```bash +PR_NUMBER=$(bash $SCRIPTS_DIR/create_pr.sh "$BRANCH_BASE" "/tmp/pr_body_generated.md") +``` + +- Exit 0 → stocker PR_NUMBER, marquer todo "Générer description PR intelligente" completed, puis marquer todo "Push et création PR" completed +- Exit 1 → ARRÊT + +### 10. Milestone + +- Marquer todo "Assigner milestone" in_progress + +Si MILESTONE fourni : +```bash +python3 $SCRIPTS_DIR/assign_milestone.py $PR_NUMBER --milestone "$MILESTONE" +``` + +Sinon : +```bash +python3 $SCRIPTS_DIR/assign_milestone.py $PR_NUMBER +``` + +Si needs_user_input: true → utiliser AskUserQuestion avec milestones disponibles + +- Marquer todo "Assigner milestone" completed (même si échec, non bloquant) + +### 11. Projet + +- Marquer todo "Assigner projet GitHub" in_progress + +Si PROJECT_NAME fourni : +```bash +python3 $SCRIPTS_DIR/assign_project.py $PR_NUMBER --project "$PROJECT_NAME" +``` + +Sinon : +```bash +python3 $SCRIPTS_DIR/assign_project.py $PR_NUMBER +``` + +Si needs_user_input: true → utiliser AskUserQuestion avec projets disponibles + +- Marquer todo "Assigner projet GitHub" completed (même si échec, non bloquant) + +### 12. Review intelligente (si pas --no-review) + +- Marquer todo "Code review automatique" in_progress + +EXÉCUTER pour récupérer les données : +```bash +PR_DATA=$(bash $SCRIPTS_DIR/auto_review.sh $PR_NUMBER) +``` + +**ANALYSER EN TANT QUE CLAUDE** les données JSON retournées et générer une review intelligente : + +1. **Conformité template PR** : + - Vérifier que toutes les sections requises sont remplies + - Signaler les sections manquantes ou incomplètes + +2. **Qualité du code** : + - Patterns suspects (code dupliqué, fonctions trop longues) + - Problèmes de sécurité potentiels (injections, données sensibles) + - Respect des conventions du projet + +3. **Tests** : + - Tests manquants pour les nouvelles fonctionnalités + - Couverture des cas limites + +4. **Documentation** : + - Commentaires nécessaires absents + - Mise à jour README si API modifiée + +5. **Suggestions d'amélioration** : + - Refactorisation possible + - Performance + - Lisibilité + +**GÉNÉRER le commentaire de review** avec structure : +```markdown +## 🔍 Code Review Automatique + +### ✅ Points positifs +- [ce qui est bien fait] + +### ⚠️ Points d'attention +- [problèmes potentiels à vérifier] + +### 💡 Suggestions +- [améliorations possibles] + +### 📋 Checklist conformité +- [ ] Template PR complet +- [ ] Tests présents +- [ ] Documentation à jour + +--- +*Review générée par git-pr skill* +``` + +EXÉCUTER pour poster le commentaire : +```bash +gh pr comment $PR_NUMBER --body "$REVIEW_COMMENT" +``` + +- Marquer todo "Code review automatique" completed + +### 13. Nettoyage + +- Marquer todo "Nettoyage branche locale" in_progress + +EXÉCUTER : +```bash +bash $SCRIPTS_DIR/cleanup_branch.sh "$BRANCH_BASE" "$BRANCH_NAME" $DELETE_FLAG +``` + +Si needs_user_input: true → utiliser AskUserQuestion pour confirmer suppression + +- Marquer todo "Nettoyage branche locale" completed + +### 14. Rapport final + +EXÉCUTER : +```bash +bash $SCRIPTS_DIR/final_report.sh $PR_NUMBER $START_TIME +``` + +Afficher le rapport YAML généré. diff --git a/skills/git-pr/scripts/analyze_changes.sh b/skills/git-pr/scripts/analyze_changes.sh new file mode 100755 index 0000000..618c17c --- /dev/null +++ b/skills/git-pr/scripts/analyze_changes.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Collecte les statistiques git en une fois +# Usage: analyze_changes.sh +# Output: JSON avec stats des changements + +set -euo pipefail + +# Collecter stats +FILES_CHANGED=$(git diff --cached --numstat | wc -l) +ADDITIONS=$(git diff --cached --numstat | awk '{sum+=$1} END {print sum+0}') +DELETIONS=$(git diff --cached --numstat | awk '{sum+=$2} END {print sum+0}') + +# Lister fichiers modifiés +MODIFIED_FILES=$(git diff --cached --name-only | jq -R . | jq -s .) + +# Détecter fichiers PHP +HAS_PHP_FILES=false +if git diff --cached --name-only | grep -q '\.php$'; then + HAS_PHP_FILES=true +fi + +# Générer JSON +cat < [--milestone ] +Output: Milestone assigné ou "ignored" (stdout) +""" + +import argparse +import json +import subprocess +import sys +import re +from milestone_cache import MilestoneCache + + +def get_repo_info(): + """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() + # Extraire owner/repo depuis URL GitHub + 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) + sys.exit(1) + + +def get_open_milestones(repo): + """Récupère les milestones ouverts via gh API""" + try: + result = subprocess.run( + ["gh", "api", f"repos/{repo}/milestones", "--jq", + "[.[] | select(.state == \"open\") | {number, title}]"], + capture_output=True, + text=True, + check=True + ) + return json.loads(result.stdout) + except subprocess.CalledProcessError as e: + print(f"❌ Erreur récupération milestones: {e.stderr}", file=sys.stderr) + return [] + except json.JSONDecodeError as e: + print(f"❌ Erreur parsing JSON milestones: {e}", file=sys.stderr) + return [] + + +def get_open_milestones_cached(repo): + """Récupère milestones avec cache""" + cache = MilestoneCache() + if not cache.cache.get("milestones"): + milestones = get_open_milestones(repo) + cache.refresh_from_api(milestones) + return milestones + return cache.cache["milestones"] + + +def find_milestone(repo, query): + """Cherche milestone par query (exact ou partiel)""" + cache = MilestoneCache() + result = cache.find(query) + if result: + return result + normalized = cache.normalize_semver(query) + result = cache.find(normalized) + if result: + return result + milestones = get_open_milestones(repo) + cache.refresh_from_api(milestones) + result = cache.find(normalized) + if result: + return result + return cache.create(normalized) + + +def assign_milestone(pr_number, milestone_title): + """Assigne un milestone à la PR""" + try: + subprocess.run( + ["gh", "pr", "edit", str(pr_number), "--milestone", milestone_title], + capture_output=True, + text=True, + check=True + ) + return True + except subprocess.CalledProcessError as e: + print(f"❌ Erreur assignation milestone: {e.stderr}", file=sys.stderr) + return False + + +def main(): + parser = argparse.ArgumentParser(description="Assigne un milestone à une PR") + parser.add_argument("pr_number", type=int, help="Numéro de la PR") + parser.add_argument("--milestone", help="Nom du milestone à assigner") + args = parser.parse_args() + + repo = get_repo_info() + milestones = get_open_milestones_cached(repo) + + if not milestones: + print("ℹ️ Aucun milestone ouvert - ignoré") + print("ignored") + sys.exit(0) + + # Si --milestone fourni, utiliser find_milestone pour recherche intelligente + if args.milestone: + try: + milestone = find_milestone(repo, args.milestone) + if assign_milestone(args.pr_number, milestone['title']): + print(f"✅ Milestone '{milestone['title']}' assigné") + print(milestone['title']) + sys.exit(0) + else: + sys.exit(1) + except Exception as e: + print(f"❌ Erreur: {e}", file=sys.stderr) + sys.exit(1) + + # Sinon, retourner JSON pour AskUserQuestion + # Suggérer le premier milestone (généralement le plus récent) + milestones_with_suggestion = [ + { + "number": m["number"], + "title": m["title"], + "is_suggested": i == 0 + } + for i, m in enumerate(milestones) + ] + + output = { + "milestones": milestones_with_suggestion, + "needs_user_input": True + } + + print(json.dumps(output, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/git-pr/scripts/assign_project.py b/skills/git-pr/scripts/assign_project.py new file mode 100755 index 0000000..b06ede5 --- /dev/null +++ b/skills/git-pr/scripts/assign_project.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +""" +Assigner PR à projet GitHub +Usage: assign_project.py [--project ] +Output: Projet assigné ou "ignored" (stdout) +""" + +import argparse +import json +import subprocess +import sys +import re +from project_cache import ProjectCache + + +def get_repo_info(): + """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: + repo_full = match.group(1) + owner, repo = repo_full.split('/') + return owner, repo + raise ValueError(f"Format URL invalide: {url}") + except Exception as e: + print(f"❌ Erreur récupération repo: {e}", file=sys.stderr) + sys.exit(1) + + +def get_projects_list(owner): + """Récupère les projets via gh project list""" + try: + result = subprocess.run( + ["gh", "project", "list", "--owner", owner, "--format", "json"], + capture_output=True, + text=True, + check=True + ) + projects_data = json.loads(result.stdout) + projects = [] + for project in projects_data.get('projects', []): + projects.append({ + 'id': project.get('id'), + 'title': project.get('title'), + 'number': project.get('number') + }) + return projects + except subprocess.CalledProcessError as e: + print(f"⚠️ Impossible de récupérer les projets: {e.stderr}", file=sys.stderr) + return [] + except json.JSONDecodeError as e: + print(f"❌ Erreur parsing JSON projets: {e}", file=sys.stderr) + return [] + + +def get_projects_cached(owner): + """Récupère projets avec cache""" + cache = ProjectCache() + if not cache.cache.get("projects"): + projects = get_projects_list(owner) + cache.refresh_from_api(projects) + return projects + return cache.cache["projects"] + + +def find_project(owner, query): + """Cherche projet par query (exact ou alias)""" + cache = ProjectCache() + result = cache.find(query) + if result: + return result + projects = get_projects_list(owner) + cache.refresh_from_api(projects) + return cache.find(query) + + + + +def assign_pr_to_project(pr_number, project_id, owner, repo): + """Assigne la PR au projet via GraphQL""" + # Récupérer l'ID de la PR + try: + pr_result = subprocess.run( + ["gh", "pr", "view", str(pr_number), "--json", "id"], + capture_output=True, + text=True, + check=True + ) + pr_data = json.loads(pr_result.stdout) + pr_id = pr_data['id'] + except Exception as e: + print(f"❌ Erreur récupération ID PR: {e}", file=sys.stderr) + return False + + # Ajouter la PR au projet + mutation = """ + mutation($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { + item { + id + } + } + } + """ + + try: + subprocess.run( + ["gh", "api", "graphql", + "-f", f"query={mutation}", + "-f", f"projectId={project_id}", + "-f", f"contentId={pr_id}"], + capture_output=True, + text=True, + check=True + ) + return True + except subprocess.CalledProcessError as e: + print(f"❌ Erreur assignation projet: {e.stderr}", file=sys.stderr) + return False + + +def main(): + parser = argparse.ArgumentParser(description="Assigne une PR à un projet GitHub") + parser.add_argument("pr_number", type=int, help="Numéro de la PR") + parser.add_argument("--project", help="Nom du projet à assigner") + args = parser.parse_args() + + owner, repo = get_repo_info() + projects = get_projects_cached(owner) + + if not projects: + print("ℹ️ Aucun projet trouvé - ignoré") + print("ignored") + sys.exit(0) + + # Si --project fourni, utiliser find_project pour recherche intelligente + if args.project: + try: + project = find_project(owner, args.project) + if not project: + print(f"❌ Projet '{args.project}' non trouvé", file=sys.stderr) + available = [p['title'] for p in projects] + print(f"Projets disponibles: {', '.join(available)}", file=sys.stderr) + sys.exit(1) + + if assign_pr_to_project(args.pr_number, project['id'], owner, repo): + print(f"✅ Projet '{project['title']}' assigné") + print(project['title']) + sys.exit(0) + else: + sys.exit(1) + except Exception as e: + print(f"❌ Erreur: {e}", file=sys.stderr) + sys.exit(1) + + # Sinon, retourner JSON pour AskUserQuestion + projects_list = [ + { + "id": p["id"], + "title": p["title"], + "number": p.get("number") + } + for p in projects + ] + + output = { + "projects": projects_list, + "needs_user_input": True + } + + print(json.dumps(output, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/git-pr/scripts/auto_review.sh b/skills/git-pr/scripts/auto_review.sh new file mode 100755 index 0000000..8de0e13 --- /dev/null +++ b/skills/git-pr/scripts/auto_review.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Récupère les données de la PR pour analyse intelligente par Claude +# Usage: auto_review.sh +# Sortie: JSON avec toutes les informations nécessaires pour la review +# Exit 0 si OK, Exit 1 si échec + +set -euo pipefail + +PR_NUMBER="$1" + +if [ -z "$PR_NUMBER" ]; then + echo "❌ PR_NUMBER requis" >&2 + exit 1 +fi + +# Récupérer les informations de la PR +PR_INFO=$(gh pr view "$PR_NUMBER" --json title,body,files,additions,deletions,commits,baseRefName,headRefName 2>/dev/null) + +if [ -z "$PR_INFO" ]; then + echo "❌ Impossible de récupérer les informations de la PR" >&2 + exit 1 +fi + +# Récupérer le diff complet +DIFF=$(gh pr diff "$PR_NUMBER" 2>/dev/null || echo "") + +# Récupérer le template PR s'il existe +PR_TEMPLATE="" +if [ -f ".github/pull_request_template.md" ]; then + PR_TEMPLATE=$(cat .github/pull_request_template.md) +fi + +# Générer sortie JSON pour Claude +cat <&1 | grep "Token scopes" | sed 's/.*Token scopes: //' | tr -d "'" | tr ',' ' ') + +if [ -z "$CURRENT_SCOPES" ]; then + echo "❌ Impossible de récupérer les scopes GitHub" + echo " Vérifier l'authentification avec: gh auth status" + exit 1 +fi + +# Vérifier chaque scope requis +MISSING_SCOPES=() +for scope in "${REQUIRED_SCOPES[@]}"; do + if ! echo "$CURRENT_SCOPES" | grep -qw "$scope"; then + MISSING_SCOPES+=("$scope") + fi +done + +# Rapport +if [ ${#MISSING_SCOPES[@]} -gt 0 ]; then + echo "❌ Scopes GitHub manquants: ${MISSING_SCOPES[*]}" + echo "" + echo "🔄 Pour renouveler l'authentification:" + echo " bash $(dirname "$0")/gh_auth_setup.sh" + exit 1 +fi + +echo "✅ Scopes GitHub valides" +exit 0 diff --git a/skills/git-pr/scripts/cleanup_branch.sh b/skills/git-pr/scripts/cleanup_branch.sh new file mode 100755 index 0000000..b6ec6ac --- /dev/null +++ b/skills/git-pr/scripts/cleanup_branch.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Proposer/exécuter suppression branche locale +# Usage: cleanup_branch.sh [--delete] +# Exit: 0=supprimée, 1=conservée + +set -euo pipefail + +BRANCH_BASE="$1" +BRANCH_NAME="$2" +DELETE_FLAG="${3:-}" + +# Si --delete fourni, suppression auto +if [ "$DELETE_FLAG" = "--delete" ]; then + echo "🗑️ Suppression automatique de la branche $BRANCH_NAME..." + git checkout "$BRANCH_BASE" + git branch -D "$BRANCH_NAME" + echo "✅ Branche $BRANCH_NAME supprimée" + exit 0 +fi + +# Sinon, retourner JSON pour AskUserQuestion +cat <] +Output: Nom branche validé (stdout) ou JSON options +""" + +import argparse +import json +import subprocess +import sys +import re + + +def get_remote_branches(): + """Récupère les branches remote et filtre develop/main/master/release/*""" + try: + result = subprocess.run( + ["git", "branch", "-r"], + capture_output=True, + text=True, + check=True + ) + + branches = [] + for line in result.stdout.strip().split('\n'): + branch = line.strip() + # Retirer origin/ + branch = re.sub(r'^origin/', '', branch) + # Ignorer HEAD + if 'HEAD' in branch: + continue + # Filtrer develop/main/master/release/*/hotfix/* + if branch in ['develop', 'main', 'master'] or branch.startswith('release/') or branch.startswith('hotfix/'): + branches.append(branch) + + return sorted(set(branches)) + except subprocess.CalledProcessError as e: + print(f"❌ Erreur lors de la récupération des branches: {e}", file=sys.stderr) + sys.exit(1) + + +def validate_branch(branch_name, available_branches): + """Valide qu'une branche existe""" + if branch_name in available_branches: + return True + print(f"❌ Branche '{branch_name}' non trouvée", file=sys.stderr) + print(f"Branches disponibles: {', '.join(available_branches)}", file=sys.stderr) + return False + + +def main(): + parser = argparse.ArgumentParser(description="Confirme la branche de base pour la PR") + parser.add_argument("--branch", help="Nom de la branche de base") + args = parser.parse_args() + + branches = get_remote_branches() + + if not branches: + print("❌ Aucune branche develop/main/master/release/hotfix trouvée", file=sys.stderr) + sys.exit(1) + + # Si --branch fourni, valider et retourner + if args.branch: + if validate_branch(args.branch, branches): + print(args.branch) + sys.exit(0) + else: + sys.exit(1) + + # Sinon, retourner JSON pour AskUserQuestion + suggested = "develop" if "develop" in branches else branches[0] + + output = { + "branches": branches, + "suggested": suggested, + "needs_user_input": True + } + + print(json.dumps(output, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/git-pr/scripts/create_pr.sh b/skills/git-pr/scripts/create_pr.sh new file mode 100755 index 0000000..75e83cf --- /dev/null +++ b/skills/git-pr/scripts/create_pr.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Création complète de PR: lit template, génère titre, crée PR +# Usage: create_pr.sh +# Output: PR_NUMBER (stdout) ou exit 1 + +set -euo pipefail + +BRANCH_BASE="$1" +PR_TEMPLATE_PATH="$2" +SCRIPTS_DIR="$(dirname "$0")" + +# Récupérer branche courante +BRANCH_NAME=$(git branch --show-current) + +# Lire template PR +if [ ! -f "$PR_TEMPLATE_PATH" ]; then + echo "❌ Template PR absent: $PR_TEMPLATE_PATH" >&2 + exit 1 +fi +PR_TEMPLATE=$(cat "$PR_TEMPLATE_PATH") + +# Détecter issue depuis nom de branche (ex: feat/123-description, fix/456-bug) +ISSUE_NUMBER=$(echo "$BRANCH_NAME" | grep -oE '[0-9]+' | head -1 || echo "") + +# Générer titre PR +if [ -n "$ISSUE_NUMBER" ]; then + # Vérifier que l'issue existe et récupérer son titre + ISSUE_TITLE=$(gh issue view "$ISSUE_NUMBER" --json title -q '.title' 2>/dev/null || echo "") + if [ -n "$ISSUE_TITLE" ]; then + PR_TITLE="$ISSUE_TITLE / Issue #$ISSUE_NUMBER" + echo "✅ Titre PR basé sur issue #$ISSUE_NUMBER" >&2 + else + # Issue non trouvée, utiliser nom de branche + echo "⚠️ Issue #$ISSUE_NUMBER non trouvée" >&2 + # Nettoyer le nom de branche pour créer un titre + PR_TITLE=$(echo "$BRANCH_NAME" | sed 's|^[^/]*/||' | sed 's/-/ /g' | sed 's/[0-9]* *//') + PR_TITLE="${PR_TITLE^}" # Capitalize first letter + fi +else + # Pas de numéro d'issue, utiliser nom de branche + echo "ℹ️ Pas d'issue détectée dans '$BRANCH_NAME'" >&2 + PR_TITLE=$(echo "$BRANCH_NAME" | sed 's|^[^/]*/||' | sed 's/-/ /g') + PR_TITLE="${PR_TITLE^}" +fi + +# Créer fichier temporaire avec le body +PR_BODY_FILE="/tmp/pr_body_$(date +%s).md" +echo "$PR_TEMPLATE" > "$PR_BODY_FILE" + +# Appeler le script de push sécurisé +PR_NUMBER=$(bash "$SCRIPTS_DIR/safe_push_pr.sh" "$BRANCH_BASE" "$BRANCH_NAME" "$PR_TITLE" "$PR_BODY_FILE") +EXIT_CODE=$? + +# Nettoyer +rm -f "$PR_BODY_FILE" + +if [ $EXIT_CODE -ne 0 ]; then + exit 1 +fi + +echo "$PR_NUMBER" diff --git a/skills/git-pr/scripts/final_report.sh b/skills/git-pr/scripts/final_report.sh new file mode 100755 index 0000000..21f6e7d --- /dev/null +++ b/skills/git-pr/scripts/final_report.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Génère le rapport final YAML de la PR +# Usage: final_report.sh +# Output: Rapport YAML formaté + +set -euo pipefail + +PR_NUMBER="$1" +START_TIME="$2" + +if [ -z "$PR_NUMBER" ] || [ -z "$START_TIME" ]; then + echo "❌ Usage: final_report.sh " >&2 + exit 1 +fi + +# Récupérer infos de la PR +PR_INFO=$(gh pr view "$PR_NUMBER" --json title,url,baseRefName,headRefName,additions,deletions,changedFiles 2>/dev/null || echo "{}") + +PR_TITLE=$(echo "$PR_INFO" | jq -r '.title // "N/A"') +PR_URL=$(echo "$PR_INFO" | jq -r '.url // "N/A"') +BRANCH_BASE=$(echo "$PR_INFO" | jq -r '.baseRefName // "N/A"') +BRANCH_NAME=$(echo "$PR_INFO" | jq -r '.headRefName // "N/A"') +ADDITIONS=$(echo "$PR_INFO" | jq -r '.additions // 0') +DELETIONS=$(echo "$PR_INFO" | jq -r '.deletions // 0') +FILES_CHANGED=$(echo "$PR_INFO" | jq -r '.changedFiles // 0') + +# Calculer durée +END_TIME=$(date +%s) +DURATION=$((END_TIME - START_TIME)) + +# Formater durée +if [ $DURATION -lt 60 ]; then + DURATION_STR="${DURATION}s" +elif [ $DURATION -lt 3600 ]; then + MINUTES=$((DURATION / 60)) + SECONDS=$((DURATION % 60)) + DURATION_STR="${MINUTES}m ${SECONDS}s" +else + HOURS=$((DURATION / 3600)) + MINUTES=$(((DURATION % 3600) / 60)) + SECONDS=$((DURATION % 60)) + DURATION_STR="${HOURS}h ${MINUTES}m ${SECONDS}s" +fi + +# Formater timestamps +START_DATE=$(date -d "@$START_TIME" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -r "$START_TIME" '+%Y-%m-%d %H:%M:%S') +END_DATE=$(date '+%Y-%m-%d %H:%M:%S') + +# Générer rapport YAML +cat < 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}" diff --git a/skills/git-pr/scripts/project_cache.py b/skills/git-pr/scripts/project_cache.py new file mode 100644 index 0000000..744e093 --- /dev/null +++ b/skills/git-pr/scripts/project_cache.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Module de cache pour projets GitHub""" + +import json +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-projects.json" + + +class ProjectCache: + """Gestion cache projets""" + + 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, "projects": []} + try: + with open(CACHE_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + return {"updated_at": None, "projects": []} + + 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 projet par titre exact ou alias (case-insensitive)""" + query_lower = query.lower() + for project in self.cache.get("projects", []): + if project["title"].lower() == query_lower: + return project + if query_lower in [alias.lower() for alias in project.get("aliases", [])]: + return project + return None + + def add(self, project: dict): + """Ajoute projet au cache avec génération aliases""" + aliases = self.generate_aliases(project["title"]) + project["aliases"] = aliases + existing = [p for p in self.cache.get("projects", []) if p["id"] == project["id"]] + if not existing: + if "projects" not in self.cache: + self.cache["projects"] = [] + self.cache["projects"].append(project) + self.save() + + def generate_aliases(self, title: str) -> list[str]: + """Génère aliases depuis titre + + Exemples: + - "Project Alpha" → ["project", "alpha"] + - "Sprint 2024-Q1" → ["sprint", "2024", "q1"] + - "Bug Tracking" → ["bug", "tracking"] + + Logique: extraire mots-clés significatifs + """ + aliases = [] + words = re.findall(r'[\w]+', title.lower()) + for word in words: + if len(word) >= 2 and word not in ['the', 'and', 'for']: + aliases.append(word) + return list(set(aliases)) + + def refresh_from_api(self, projects: list[dict]): + """Remplace cache entièrement avec données API""" + enriched = [] + for project in projects: + aliases = self.generate_aliases(project["title"]) + enriched.append({ + "id": project["id"], + "title": project["title"], + "number": project.get("number"), + "aliases": aliases + }) + self.cache["projects"] = enriched + self.save() + + 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: + repo_full = match.group(1) + owner, _ = repo_full.split('/') + return owner + raise ValueError(f"Format URL invalide: {url}") + except Exception as e: + print(f"❌ Erreur récupération repo: {e}", file=sys.stderr) + raise diff --git a/skills/git-pr/scripts/safe_push_pr.sh b/skills/git-pr/scripts/safe_push_pr.sh new file mode 100755 index 0000000..08fff92 --- /dev/null +++ b/skills/git-pr/scripts/safe_push_pr.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# Vérifications sécurité + push + création PR +# Usage: safe_push_pr.sh +# Output: PR_NUMBER (stdout) ou exit 1 + +set -euo pipefail + +BRANCH_BASE="$1" +BRANCH_NAME="$2" +PR_TITLE="$3" +PR_BODY_FILE="$4" + +# Vérification 1: Branche courante +CURRENT_BRANCH=$(git branch --show-current) +if [ "$CURRENT_BRANCH" != "$BRANCH_NAME" ]; then + echo "❌ Branche courante ($CURRENT_BRANCH) != branche attendue ($BRANCH_NAME)" >&2 + exit 1 +fi + +# Vérification 2: Commits à pousser +COMMITS_TO_PUSH=$(git log --oneline "origin/$BRANCH_BASE..$BRANCH_NAME" 2>/dev/null | wc -l || echo "0") + +if [ "$COMMITS_TO_PUSH" -eq 0 ]; then + echo "❌ Aucun commit à pousser vers $BRANCH_BASE" >&2 + exit 1 +fi + +# Vérification 3: Afficher commits +echo "📋 Commits à pousser ($COMMITS_TO_PUSH):" +git log --oneline "origin/$BRANCH_BASE..$BRANCH_NAME" | sed 's/^/ /' + +# Vérification 4: Vérifier que le fichier body existe +if [ ! -f "$PR_BODY_FILE" ]; then + echo "❌ Fichier PR body absent: $PR_BODY_FILE" >&2 + exit 1 +fi + +# Push vers origin +echo "🚀 Push vers origin/$BRANCH_NAME..." +if ! git push -u origin "$BRANCH_NAME"; then + echo "❌ Échec du push" >&2 + exit 1 +fi + +# Création PR +echo "📝 Création de la Pull Request..." +PR_URL=$(gh pr create \ + --base "$BRANCH_BASE" \ + --title "$PR_TITLE" \ + --body-file "$PR_BODY_FILE" \ + 2>&1) + +if [ $? -ne 0 ]; then + echo "❌ Échec création PR: $PR_URL" >&2 + exit 1 +fi + +# Extraire le numéro de PR depuis l'URL +PR_NUMBER=$(echo "$PR_URL" | grep -oP '/pull/\K\d+' || echo "") + +if [ -z "$PR_NUMBER" ]; then + # Fallback: extraire depuis gh pr view + PR_NUMBER=$(gh pr view --json number -q .number 2>/dev/null || echo "") +fi + +if [ -z "$PR_NUMBER" ]; then + echo "❌ Impossible d'extraire le numéro de PR" >&2 + echo "URL: $PR_URL" >&2 + exit 1 +fi + +echo "✅ PR #$PR_NUMBER créée: $PR_URL" +echo "$PR_NUMBER" diff --git a/skills/git-pr/scripts/smart_qa.sh b/skills/git-pr/scripts/smart_qa.sh new file mode 100755 index 0000000..1202e38 --- /dev/null +++ b/skills/git-pr/scripts/smart_qa.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Détecte les fichiers PHP modifiés et lance make qa si nécessaire +# Usage: smart_qa.sh +# Exit: 0=ok/ignoré, 1=échec QA + +set -euo pipefail + +# Détecter fichiers PHP modifiés +PHP_FILES=$(git diff --name-only --cached | grep '\.php$' || true) + +if [ -z "$PHP_FILES" ]; then + echo "ℹ️ Aucun fichier PHP modifié - QA ignorée" + exit 0 +fi + +echo "🔍 Fichiers PHP détectés - Lancement de make qa..." +echo "$PHP_FILES" | sed 's/^/ - /' + +# Lancer QA avec timeout +if timeout 600 make qa; then + echo "✅ QA passée avec succès" + exit 0 +else + EXIT_CODE=$? + if [ $EXIT_CODE -eq 124 ]; then + echo "❌ QA timeout (>600s)" >&2 + else + echo "❌ QA échouée" >&2 + fi + exit 1 +fi diff --git a/skills/git-pr/scripts/verify_pr_template.sh b/skills/git-pr/scripts/verify_pr_template.sh new file mode 100755 index 0000000..487febf --- /dev/null +++ b/skills/git-pr/scripts/verify_pr_template.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Vérifie l'existence du template PR +# Usage: verify_pr_template.sh +# Exit: 0=ok, 1=absent + +set -euo pipefail + +TEMPLATE_PATH="${1:-.github/pull_request_template.md}" + +if [ ! -f "$TEMPLATE_PATH" ]; then + echo "❌ Template PR absent: $TEMPLATE_PATH" >&2 + exit 1 +fi + +echo "✅ Template PR trouvé: $TEMPLATE_PATH" +exit 0 diff --git a/skills/git-pr/tests/.gitignore b/skills/git-pr/tests/.gitignore new file mode 100644 index 0000000..eca0a88 --- /dev/null +++ b/skills/git-pr/tests/.gitignore @@ -0,0 +1,15 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Cache test local +.claude/ + +# Éditeurs +*~ +*.swp +*.swo +.DS_Store diff --git a/skills/git-pr/tests/run_tests.sh b/skills/git-pr/tests/run_tests.sh new file mode 100755 index 0000000..fcfac58 --- /dev/null +++ b/skills/git-pr/tests/run_tests.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Lance tous les tests unitaires du skill git-pr + +set -e + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$TEST_DIR" + +echo "🧪 Lancement tests git-pr..." +echo "" + +python3 test_milestone_cache.py -v +echo "" + +python3 test_project_cache.py -v +echo "" + +echo "✅ Tous les tests passés" diff --git a/skills/git-pr/tests/test_milestone_cache.py b/skills/git-pr/tests/test_milestone_cache.py new file mode 100644 index 0000000..a5bd77c --- /dev/null +++ b/skills/git-pr/tests/test_milestone_cache.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Tests unitaires pour milestone_cache""" + +import json +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch, MagicMock + +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) +from milestone_cache import MilestoneCache, CACHE_DIR, CACHE_FILE + + +class TestNormalizeSemver(unittest.TestCase): + """Test normalisation semver""" + + def test_normalise_un_chiffre_ajoute_zero_zero(self): + cache = MilestoneCache() + self.assertEqual(cache.normalize_semver("26"), "26.0.0") + + def test_normalise_deux_chiffres_ajoute_zero(self): + cache = MilestoneCache() + self.assertEqual(cache.normalize_semver("26.1"), "26.1.0") + + def test_normalise_trois_chiffres_reste_identique(self): + cache = MilestoneCache() + self.assertEqual(cache.normalize_semver("26.1.1"), "26.1.1") + + def test_normalise_conserve_suffixe(self): + cache = MilestoneCache() + self.assertEqual(cache.normalize_semver("26.0.0 (Avenant)"), "26.0.0 (Avenant)") + + +class TestGenerateAliases(unittest.TestCase): + """Test génération aliases stricte""" + + def test_genere_alias_pour_titre_avec_parentheses(self): + cache = MilestoneCache() + self.assertEqual(cache.generate_aliases("26.1.1 (Hotfix)"), ["26.1.1"]) + + def test_genere_alias_pour_titre_avec_avenant(self): + cache = MilestoneCache() + self.assertEqual(cache.generate_aliases("26.0.0 (Avenant)"), ["26.0.0"]) + + def test_genere_pas_alias_pour_titre_sans_parentheses(self): + cache = MilestoneCache() + self.assertEqual(cache.generate_aliases("26.1.0"), []) + + def test_genere_pas_alias_pour_titre_non_semver(self): + cache = MilestoneCache() + self.assertEqual(cache.generate_aliases("Release Candidate"), []) + + +class TestFindExactMatch(unittest.TestCase): + """Test recherche exacte""" + + def test_trouve_par_titre_exact(self): + cache = MilestoneCache() + cache.cache = { + "milestones": [ + {"number": 42, "title": "26.0.0 (Avenant)", "aliases": ["26.0.0"]} + ] + } + result = cache.find("26.0.0 (Avenant)") + self.assertIsNotNone(result) + self.assertEqual(result["number"], 42) + + def test_ne_trouve_pas_si_titre_different(self): + cache = MilestoneCache() + cache.cache = { + "milestones": [ + {"number": 42, "title": "26.0.0 (Avenant)", "aliases": ["26.0.0"]} + ] + } + result = cache.find("27.0.0") + self.assertIsNone(result) + + +class TestFindByAlias(unittest.TestCase): + """Test recherche par alias""" + + def test_query_trouve_via_alias(self): + cache = MilestoneCache() + cache.cache = { + "milestones": [ + {"number": 43, "title": "26.1.1 (Hotfix)", "aliases": ["26.1.1"]} + ] + } + result = cache.find("26.1.1") + self.assertIsNotNone(result) + self.assertEqual(result["title"], "26.1.1 (Hotfix)") + + def test_query_trouve_milestone_sans_parentheses(self): + cache = MilestoneCache() + cache.cache = { + "milestones": [ + {"number": 44, "title": "26.1.0", "aliases": []} + ] + } + result = cache.find("26.1.0") + self.assertIsNotNone(result) + self.assertEqual(result["number"], 44) + + +class TestNoCollisionMatch(unittest.TestCase): + """Test pas de collision entre versions""" + + def test_query_partielle_ne_trouve_pas_version_complete(self): + cache = MilestoneCache() + cache.cache = { + "milestones": [ + {"number": 43, "title": "26.1.1 (Hotfix)", "aliases": ["26.1.1"]} + ] + } + result = cache.find("26.1") + self.assertIsNone(result) + + def test_query_majeure_ne_trouve_pas_version_mineure(self): + cache = MilestoneCache() + cache.cache = { + "milestones": [ + {"number": 42, "title": "26.0.0 (Avenant)", "aliases": ["26.0.0"]} + ] + } + result = cache.find("26") + self.assertIsNone(result) + + +class TestFindWithNormalization(unittest.TestCase): + """Test recherche avec normalisation""" + + def test_query_normalisee_trouve_milestone(self): + cache = MilestoneCache() + cache.cache = { + "milestones": [ + {"number": 44, "title": "26.1.0", "aliases": []} + ] + } + normalized = cache.normalize_semver("26.1") + result = cache.find(normalized) + self.assertIsNotNone(result) + self.assertEqual(result["title"], "26.1.0") + + def test_query_majeure_normalisee_trouve_avenant(self): + cache = MilestoneCache() + cache.cache = { + "milestones": [ + {"number": 42, "title": "26.0.0 (Avenant)", "aliases": ["26.0.0"]} + ] + } + normalized = cache.normalize_semver("26") + result = cache.find(normalized) + self.assertIsNotNone(result) + self.assertEqual(result["title"], "26.0.0 (Avenant)") + + +class TestCachePersistence(unittest.TestCase): + """Test sauvegarde/chargement""" + + def test_charge_cache_vide_si_fichier_inexistant(self): + cache = MilestoneCache() + cache.cache = {"milestones": []} + self.assertEqual(cache.cache["milestones"], []) + + +class TestCreateMilestone(unittest.TestCase): + """Test création milestone inexistant""" + + @patch('milestone_cache.subprocess.run') + def test_cree_milestone_via_api(self, mock_run): + mock_run.side_effect = [ + MagicMock(stdout="git@github.com:owner/repo.git\n"), + MagicMock(stdout='{"number": 50, "title": "99.0.0"}') + ] + cache = MilestoneCache() + result = cache.create("99.0.0") + self.assertEqual(result["number"], 50) + self.assertEqual(result["title"], "99.0.0") + + +class TestCreateWithNormalization(unittest.TestCase): + """Test création avec normalisation semver""" + + @patch('milestone_cache.subprocess.run') + def test_cree_milestone_normalise_depuis_majeure(self, mock_run): + mock_run.side_effect = [ + MagicMock(stdout="git@github.com:owner/repo.git\n"), + MagicMock(stdout='{"number": 51, "title": "26.0.0"}') + ] + cache = MilestoneCache() + normalized = cache.normalize_semver("26") + result = cache.create(normalized) + self.assertEqual(result["title"], "26.0.0") + + @patch('milestone_cache.subprocess.run') + def test_cree_milestone_normalise_depuis_mineure(self, mock_run): + mock_run.side_effect = [ + MagicMock(stdout="git@github.com:owner/repo.git\n"), + MagicMock(stdout='{"number": 52, "title": "26.1.0"}') + ] + cache = MilestoneCache() + normalized = cache.normalize_semver("26.1") + result = cache.create(normalized) + self.assertEqual(result["title"], "26.1.0") + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/git-pr/tests/test_project_cache.py b/skills/git-pr/tests/test_project_cache.py new file mode 100644 index 0000000..4dabe18 --- /dev/null +++ b/skills/git-pr/tests/test_project_cache.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Tests unitaires pour project_cache""" + +import json +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch, MagicMock + +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) +from project_cache import ProjectCache, CACHE_DIR, CACHE_FILE + + +class TestGenerateAliases(unittest.TestCase): + """Test génération aliases""" + + def test_genere_alias_pour_titre_simple(self): + cache = ProjectCache() + aliases = cache.generate_aliases("Project Alpha") + self.assertIn("project", aliases) + self.assertIn("alpha", aliases) + + def test_genere_alias_pour_titre_avec_tirets(self): + cache = ProjectCache() + aliases = cache.generate_aliases("Bug Tracking") + self.assertIn("bug", aliases) + self.assertIn("tracking", aliases) + + def test_genere_alias_pour_titre_avec_chiffres(self): + cache = ProjectCache() + aliases = cache.generate_aliases("Sprint 2024-Q1") + self.assertIn("sprint", aliases) + self.assertIn("2024", aliases) + self.assertIn("q1", aliases) + + def test_genere_pas_alias_pour_mots_courts(self): + cache = ProjectCache() + aliases = cache.generate_aliases("The Big Project") + self.assertNotIn("the", aliases) + self.assertIn("big", aliases) + self.assertIn("project", aliases) + + +class TestFindExactMatch(unittest.TestCase): + """Test recherche exacte""" + + def test_trouve_par_titre_exact(self): + cache = ProjectCache() + cache.cache = { + "projects": [ + {"id": "PVT_123", "title": "Project Alpha", "number": 1, "aliases": ["project", "alpha"]} + ] + } + result = cache.find("Project Alpha") + self.assertIsNotNone(result) + self.assertEqual(result["id"], "PVT_123") + + def test_trouve_par_titre_exact_case_insensitive(self): + cache = ProjectCache() + cache.cache = { + "projects": [ + {"id": "PVT_123", "title": "Project Alpha", "number": 1, "aliases": ["project", "alpha"]} + ] + } + result = cache.find("project alpha") + self.assertIsNotNone(result) + self.assertEqual(result["id"], "PVT_123") + + def test_ne_trouve_pas_si_titre_different(self): + cache = ProjectCache() + cache.cache = { + "projects": [ + {"id": "PVT_123", "title": "Project Alpha", "number": 1, "aliases": ["project", "alpha"]} + ] + } + result = cache.find("Project Beta") + self.assertIsNone(result) + + +class TestFindByAlias(unittest.TestCase): + """Test recherche par alias""" + + def test_query_trouve_via_alias(self): + cache = ProjectCache() + cache.cache = { + "projects": [ + {"id": "PVT_456", "title": "Bug Tracking", "number": 2, "aliases": ["bug", "tracking"]} + ] + } + result = cache.find("bug") + self.assertIsNotNone(result) + self.assertEqual(result["title"], "Bug Tracking") + + def test_query_trouve_via_alias_case_insensitive(self): + cache = ProjectCache() + cache.cache = { + "projects": [ + {"id": "PVT_456", "title": "Bug Tracking", "number": 2, "aliases": ["bug", "tracking"]} + ] + } + result = cache.find("BUG") + self.assertIsNotNone(result) + self.assertEqual(result["title"], "Bug Tracking") + + def test_query_trouve_projet_sans_alias(self): + cache = ProjectCache() + cache.cache = { + "projects": [ + {"id": "PVT_789", "title": "Main", "number": 3, "aliases": ["main"]} + ] + } + result = cache.find("Main") + self.assertIsNotNone(result) + self.assertEqual(result["number"], 3) + + +class TestCachePersistence(unittest.TestCase): + """Test sauvegarde/chargement""" + + def test_charge_cache_vide_si_fichier_inexistant(self): + cache = ProjectCache() + cache.cache = {"projects": []} + self.assertEqual(cache.cache["projects"], []) + + +class TestRefreshFromApi(unittest.TestCase): + """Test refresh depuis API""" + + def test_refresh_remplace_cache_complet(self): + cache = ProjectCache() + projects = [ + {"id": "PVT_111", "title": "Project A", "number": 1}, + {"id": "PVT_222", "title": "Project B", "number": 2} + ] + cache.refresh_from_api(projects) + self.assertEqual(len(cache.cache["projects"]), 2) + self.assertEqual(cache.cache["projects"][0]["id"], "PVT_111") + self.assertEqual(cache.cache["projects"][1]["id"], "PVT_222") + + def test_refresh_genere_aliases(self): + cache = ProjectCache() + projects = [ + {"id": "PVT_333", "title": "Sprint Planning", "number": 3} + ] + cache.refresh_from_api(projects) + self.assertIn("sprint", cache.cache["projects"][0]["aliases"]) + self.assertIn("planning", cache.cache["projects"][0]["aliases"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/release-notes/SKILL.md b/skills/release-notes/SKILL.md new file mode 100644 index 0000000..7d82c12 --- /dev/null +++ b/skills/release-notes/SKILL.md @@ -0,0 +1,302 @@ +--- +name: release-notes +description: > + Génère des notes de release HTML orientées utilisateurs finaux. + Transforme les commits techniques en descriptions accessibles sans jargon. +allowed-tools: [Bash, Read, Write, Grep, Glob, AskUserQuestion] +model: sonnet +--- + +# Release Notes Skill + +Génère un document HTML orienté utilisateurs finaux, sans jargon technique. + +## Variables + +```bash +REPORT_PATH=".claude/reports" +TARGET="$ARGUMENTS" # Format: "branche-source branche-cible [nom-release]" +``` + +## Workflow + +### Étape 0: Extraction et validation des arguments + +```bash +# Parser les arguments +BRANCH_SOURCE=$(echo "$TARGET" | awk '{print $1}') +BRANCH_TARGET=$(echo "$TARGET" | awk '{print $2}') +RELEASE_NAME=$(echo "$TARGET" | awk '{print $3}') +``` + +**AVANT TOUTE EXÉCUTION**, vérifier que les arguments obligatoires sont fournis : + +1. Si `$BRANCH_SOURCE` est manquant ou vide : + - Lister les branches disponibles : `git branch -r --list 'origin/release/*' --list 'origin/feature/*' | head -10` + - Utiliser `AskUserQuestion` pour demander la branche source + - Question : "Quelle est la branche source à analyser ?" + - Proposer les branches récentes de type `release/*` ou `feature/*` + +2. Si `$BRANCH_TARGET` est manquant ou vide : + - Utiliser `AskUserQuestion` pour demander la branche cible + - Question : "Quelle est la branche de référence ?" + - Options suggérées : `main`, `develop`, `master` + +**Ne pas continuer** tant que les deux arguments obligatoires ne sont pas fournis. + +Si `$RELEASE_NAME` n'est pas fourni, utiliser le nom de `$BRANCH_SOURCE` en retirant le préfixe "release/" ou "feature/". + +### Étape 1: Validation des branches + +```bash +# Vérifier que les branches existent +git rev-parse --verify $BRANCH_SOURCE +git rev-parse --verify $BRANCH_TARGET + +# Vérifier qu'il y a des différences +DIFF_COUNT=$(git rev-list --count $BRANCH_TARGET..$BRANCH_SOURCE) +if [ $DIFF_COUNT -eq 0 ]; then + echo "Aucune différence entre les branches" + exit 0 +fi + +echo "Commits à analyser: $DIFF_COUNT" +``` + +### Étape 2: Collecte des commits + +```bash +# Récupérer tous les commits avec leur message complet +git log $BRANCH_TARGET..$BRANCH_SOURCE --oneline --no-merges + +# Pour plus de contexte si nécessaire +git log $BRANCH_TARGET..$BRANCH_SOURCE --pretty=format:"%h|%s|%b" --no-merges +``` + +### Étape 3: Analyse et catégorisation + +Analyser chaque commit et le catégoriser selon son **impact utilisateur** : + +#### Catégories + +1. **Nouveautés** (icône étoile) + - Nouvelles fonctionnalités visibles par l'utilisateur + - Nouveaux écrans, boutons, options + - Mots-clés : feat, feature, ✨, 🚀, nouveau, ajout + +2. **Améliorations** (icône flèche vers le haut) + - Améliorations de fonctionnalités existantes + - Meilleure ergonomie, rapidité + - Mots-clés : improve, enhance, ♻️, ⚡, amélioration, optimisation, perf + +3. **Corrections** (icône coche) + - Bugs corrigés impactant l'utilisateur + - Problèmes résolus + - Mots-clés : fix, 🐛, correction, résolution, bug + +4. **Sécurité** (icône bouclier) - si applicable + - Améliorations de sécurité + - Mots-clés : security, 🔒, sécurité + +#### Commits à IGNORER (ne pas inclure dans les notes) + +- `refactor:` - Refactorisation interne +- `test:` / `✅` - Tests +- `chore:` / `🔧` - Maintenance technique +- `ci:` / `👷` - CI/CD +- `docs:` / `📝` - Documentation technique (sauf si user-facing) +- `style:` / `💄` - Formatage code +- Commits de merge +- Mises à jour de dépendances + +### Étape 4: Rédaction des notes + +Pour chaque commit retenu, rédiger une description **orientée utilisateur** : + +**Règles de rédaction STRICTES :** + +1. **ZÉRO jargon technique** + - ❌ "Refactoring du composant UserController" + - ✅ "Amélioration de la gestion de votre compte" + - ❌ "Ajout d'un endpoint API REST" + - ✅ "Nouvelle fonctionnalité disponible" + +2. **Bénéfice utilisateur en premier** + - ❌ "Ajout d'un cache sur les requêtes API" + - ✅ "L'application est maintenant plus rapide" + - ❌ "Optimisation des requêtes SQL" + - ✅ "Les pages se chargent plus rapidement" + +3. **Verbes d'action simples** + - Vous pouvez maintenant... + - Il est désormais possible de... + - Nous avons corrigé... + - Nous avons amélioré... + +4. **Phrases courtes et claires** + - Max 1-2 phrases par élément + - Pas d'acronymes sans explication (pas de API, SQL, REST, etc.) + +5. **Ton positif et professionnel** + - Éviter les formulations négatives + - Focus sur ce qui est possible/amélioré + +### Étape 5: Génération du HTML + +Utiliser le template suivant avec CSS inline : + +```html + + + + + + Notes de version - [RELEASE_NAME] + + + +
+

Quoi de neuf ?

+

Version [RELEASE_NAME]

+

[DATE du jour]

+
+ +
+ +
+

⭐ Nouveautés

+
    +
  • Description orientée utilisateur...
  • +
+
+ + +
+

📈 Améliorations

+
    +
  • Description orientée utilisateur...
  • +
+
+ + +
+

✅ Corrections

+
    +
  • Description orientée utilisateur...
  • +
+
+ + +
+

🔒 Sécurité

+
    +
  • Description orientée utilisateur...
  • +
+
+
+ + + + +``` + +**Notes :** +- Ne pas inclure les sections vides +- Remplacer [RELEASE_NAME] par le nom de la release +- Remplacer [DATE] par la date du jour au format "26 novembre 2025" + +### Étape 6: Sauvegarde du fichier + +```bash +OUTPUT_FILE="$REPORT_PATH/release_notes_${RELEASE_NAME}.html" +mkdir -p "$REPORT_PATH" +``` + +Utiliser le tool `Write` pour écrire le contenu HTML dans `$OUTPUT_FILE`. + +Afficher le résumé : +``` +Notes de release générées : $OUTPUT_FILE + +Résumé : +- X nouveautés +- Y améliorations +- Z corrections +``` + +## Exemples de transformation + +| Commit technique | Note utilisateur | +|------------------|------------------| +| `✨ feat: implémenter cache Redis sur endpoint /api/users` | L'affichage de la liste des utilisateurs est maintenant plus rapide | +| `🐛 fix: corriger validation email dans le formulaire d'inscription` | Nous avons corrigé un problème qui empêchait certaines adresses email d'être acceptées lors de l'inscription | +| `⚡ perf: optimiser requêtes N+1 sur la page dashboard` | Le tableau de bord se charge maintenant plus rapidement | +| `✨ feat: ajouter export CSV des factures` | Vous pouvez maintenant exporter vos factures au format Excel | +| `🐛 fix: résoudre crash sur iOS 16 lors de l'upload` | Nous avons corrigé un problème qui pouvait faire fermer l'application lors de l'envoi de fichiers | + +## Règles importantes + +1. **ZÉRO jargon technique** - L'utilisateur final ne doit pas voir de termes comme "API", "refactoring", "backend", "cache", "endpoint", "requête" +2. **Bénéfice utilisateur** - Chaque item doit répondre à "Qu'est-ce que ça change pour moi ?" +3. **Ignorer l'invisible** - Ne pas mentionner les changements internes (tests, CI, deps, refactoring) +4. **Ton positif** - Utiliser un ton accueillant et positif +5. **Accessibilité** - HTML sémantique, contrastes suffisants, responsive +6. **Concision** - Max 1-2 phrases par item, pas de détails superflus