Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:54 +08:00
commit 4d6408436e
32 changed files with 3539 additions and 0 deletions

View File

@@ -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"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# git
Workflow Git complet : branches, commits, conflits, PR avec automation QA

152
commands/branch.md Normal file
View File

@@ -0,0 +1,152 @@
---
model: claude-sonnet-4-5-20250929
allowed-tools: Bash
argument-hint: <source-branch> [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`

161
commands/commit.md Normal file
View File

@@ -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 <type>: <description> 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

303
commands/conflit.md Normal file
View File

@@ -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: <branche-destination>
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

6
commands/pr.md Normal file
View File

@@ -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.

View File

@@ -0,0 +1,6 @@
---
description: Génère des notes de release HTML orientées utilisateurs finaux
argument-hint: "<branche-source> <branche-cible> [nom-release]"
---
You must use the Skill tool to invoke the "git:release-notes" skill.

270
commands/release-report.md Normal file
View File

@@ -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 <branche-source> <branche-cible> [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_<nom-release>.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

157
plugin.lock.json Normal file
View File

@@ -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": []
}
}

View File

@@ -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 <scope>` 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

211
skills/git-pr/README.md Normal file
View File

@@ -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 <branche-base> [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 <pr-number> --project <nom>
```

326
skills/git-pr/SKILL.md Normal file
View File

@@ -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é.

View File

@@ -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 <<EOF
{
"files_changed": $FILES_CHANGED,
"additions": $ADDITIONS,
"deletions": $DELETIONS,
"modified_files": $MODIFIED_FILES,
"has_php_files": $HAS_PHP_FILES
}
EOF

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""
Lister milestones et gérer assignation
Usage: assign_milestone.py <pr_number> [--milestone <name>]
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()

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""
Assigner PR à projet GitHub
Usage: assign_project.py <pr_number> [--project <name>]
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()

View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Récupère les données de la PR pour analyse intelligente par Claude
# Usage: auto_review.sh <pr_number>
# 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 <<EOF
{
"pr_number": $PR_NUMBER,
"pr_info": $PR_INFO,
"diff": $(echo "$DIFF" | jq -Rs .),
"template": $(echo "$PR_TEMPLATE" | jq -Rs .)
}
EOF

View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Vérifie que tous les scopes GitHub requis sont présents
# Exit 0 si OK, Exit 1 si scopes manquants
set -e
REQUIRED_SCOPES=(repo read:org project gist)
# Récupérer scopes actuels
CURRENT_SCOPES=$(gh auth status 2>&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

View File

@@ -0,0 +1,28 @@
#!/bin/bash
# Proposer/exécuter suppression branche locale
# Usage: cleanup_branch.sh <branch_base> <branch_name> [--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 <<EOF
{
"branch_name": "$BRANCH_NAME",
"branch_base": "$BRANCH_BASE",
"needs_user_input": true
}
EOF

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
Détecte les branches disponibles et gère la confirmation
Usage: confirm_base_branch.py [--branch <name>]
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()

View File

@@ -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 <branch_base> <pr_template_path>
# 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"

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# Génère le rapport final YAML de la PR
# Usage: final_report.sh <pr_number> <start_time>
# 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 <pr_number> <start_time>" >&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 <<EOF
task: "Pull Request créée avec succès"
status: "completed"
details:
pr_number: $PR_NUMBER
pr_title: "$PR_TITLE"
pr_url: "$PR_URL"
branch_source: "$BRANCH_NAME"
branch_base: "$BRANCH_BASE"
stats:
files_changed: $FILES_CHANGED
additions: $ADDITIONS
deletions: $DELETIONS
timing:
start: "$START_DATE"
end: "$END_DATE"
duration: "$DURATION_STR"
EOF

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Configuration authentification GitHub avec tous les scopes requis
# Usage: bash gh_auth_setup.sh
set -e
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
)
echo "🔐 Configuration authentification GitHub"
echo ""
echo "Scopes requis:"
for scope in "${REQUIRED_SCOPES[@]}"; do
echo " - $scope"
done
echo ""
# Construire la commande avec tous les scopes
CMD="gh auth refresh --hostname github.com"
for scope in "${REQUIRED_SCOPES[@]}"; do
CMD="$CMD -s $scope"
done
echo "🔄 Exécution: $CMD"
echo ""
eval "$CMD"
echo ""
echo "✅ Authentification configurée avec succès"
echo ""
echo "Vérification des scopes:"
gh auth status

View File

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

View File

@@ -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

View File

@@ -0,0 +1,73 @@
#!/bin/bash
# Vérifications sécurité + push + création PR
# Usage: safe_push_pr.sh <branch_base> <branch_name> <pr_title> <pr_body_file>
# 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"

View File

@@ -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

View File

@@ -0,0 +1,16 @@
#!/bin/bash
# Vérifie l'existence du template PR
# Usage: verify_pr_template.sh <chemin_template>
# 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

15
skills/git-pr/tests/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Cache test local
.claude/
# Éditeurs
*~
*.swp
*.swo
.DS_Store

View File

@@ -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"

View File

@@ -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()

View File

@@ -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()

View File

@@ -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
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notes de version - [RELEASE_NAME]</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background: #f8f9fa;
}
header {
text-align: center;
margin-bottom: 3rem;
padding: 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
color: white;
}
header h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
.version { font-size: 1.2rem; opacity: 0.9; }
.date { font-size: 0.9rem; opacity: 0.7; }
main { display: flex; flex-direction: column; gap: 2rem; }
.category {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.category h2 {
font-size: 1.3rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #eee;
}
.category.new h2 { border-color: #ffd700; }
.category.improved h2 { border-color: #4caf50; }
.category.fixed h2 { border-color: #2196f3; }
.category.security h2 { border-color: #ff5722; }
.category ul { list-style: none; }
.category li {
padding: 0.75rem 0;
border-bottom: 1px solid #f0f0f0;
}
.category li:last-child { border-bottom: none; }
footer {
text-align: center;
margin-top: 3rem;
padding: 1rem;
color: #666;
font-size: 0.9rem;
}
@media (max-width: 600px) {
body { padding: 1rem; }
header h1 { font-size: 1.8rem; }
}
</style>
</head>
<body>
<header>
<h1>Quoi de neuf ?</h1>
<p class="version">Version [RELEASE_NAME]</p>
<p class="date">[DATE du jour]</p>
</header>
<main>
<!-- Section Nouveautés (si non vide) -->
<section class="category new">
<h2>⭐ Nouveautés</h2>
<ul>
<li>Description orientée utilisateur...</li>
</ul>
</section>
<!-- Section Améliorations (si non vide) -->
<section class="category improved">
<h2>📈 Améliorations</h2>
<ul>
<li>Description orientée utilisateur...</li>
</ul>
</section>
<!-- Section Corrections (si non vide) -->
<section class="category fixed">
<h2>✅ Corrections</h2>
<ul>
<li>Description orientée utilisateur...</li>
</ul>
</section>
<!-- Section Sécurité (si applicable et non vide) -->
<section class="category security">
<h2>🔒 Sécurité</h2>
<ul>
<li>Description orientée utilisateur...</li>
</ul>
</section>
</main>
<footer>
<p>Merci d'utiliser notre application !</p>
</footer>
</body>
</html>
```
**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