Séparer les changements enchevêtrés en PRs propres
Problème
L'arborescence de travail accumule des changements non commitées provenant de plusieurs fonctionnalités au fil du temps. Quand vous devez créer des PRs ciblées, les changements sont enchevêtrés ensemble — certains fichiers ont des changements de plusieurs fonctionnalités, les fichiers générés (comme .mocks.dart) reflètent toutes les fonctionnalités combinées, et les hooks de pre-commit échouent sur du code WIP non lié.
Contexte / Conditions déclenchantes
- L'arborescence de travail a 50+ fichiers modifiés/non suivis couvrant plusieurs fonctionnalités
git diff --name-onlyaffiche des fichiers provenant clairement de différentes fonctionnalités- Vous devez créer une PR pour la Fonctionnalité A mais les fichiers de la Fonctionnalité B causent des erreurs d'analyse
- Les hooks de pre-commit exécutent
dart analyze/eslint/ etc. sur TOUS les fichiers (pas seulement ceux en staging), donc les fichiers WIP non suivis bloquent les commits - Certains fichiers ont des changements de DEUX fonctionnalités (fichiers à changements mixtes)
Solution
Étape 1 : Stasher tout en sécurité
git stash -u -m "descriptive-name-for-safety"
Le flag -u inclut les fichiers non suivis. C'est votre filet de sécurité.
Étape 2 : Créer une branche de fonctionnalité propre
git checkout main # ou la base appropriée
git pull
git checkout -b feature/clean-branch
Étape 3 : Récupérer le stash et gérer les conflits
git stash pop
Problème courant : « already exists, no checkout » — se produit quand les fichiers non suivis du stash existent déjà dans l'arborescence de travail. Les changements des fichiers suivis s'appliquent toujours, mais le stash est préservé (non supprimé). C'est sûr ; les fichiers dont vous avez besoin sont déjà là.
Conflits de fusion : Résolvez chacun. Pour les fichiers appartenant à D'AUTRES fonctionnalités, prenez la version de base :
git checkout --theirs path/to/other-feature-file.dart
Pour les fichiers appartenant à CETTE fonctionnalité, prenez la version du stash ou résolvez manuellement.
Étape 4 : Gérer les fichiers à changements mixtes
Certains fichiers ont des changements de DEUX fonctionnalités. Pour ceux-ci :
- Restaurez à la version de base :
git checkout main -- path/to/file.dart - Réappliquez UNIQUEMENT les changements de votre fonctionnalité manuellement (généralement juste des changements d'import ou quelques lignes)
Étape 5 : Gérer les hooks de pre-commit qui vérifient tous les fichiers
Si votre hook de pre-commit analyse TOUS les fichiers (pas seulement ceux en staging), les fichiers WIP non suivis d'autres fonctionnalités bloqueront votre commit.
Contournement : Déplacez temporairement les fichiers non suivis problématiques :
mkdir -p /tmp/other-features-backup
mv lib/unrelated_feature/ /tmp/other-features-backup/
mv test/unrelated_feature/ /tmp/other-features-backup/
Committez, puis restaurez :
cp -r /tmp/other-features-backup/* .
Étape 6 : Régénérer les fichiers générés
Si le projet utilise la génération de code (Mockito, Riverpod, Freezed, etc.), restaurez les fichiers générés à la base et régénérez :
git checkout main -- **/*.mocks.dart **/*.g.dart
dart run build_runner build --delete-conflicting-outputs
Cela garantit que les fichiers générés ne reflètent que les changements de VOTRE fonctionnalité.
Étape 7 : Corriger les cascades de rupture d'interface
Quand la Fonctionnalité A change une interface (renomme une méthode, change les types de paramètres), les fichiers consommateurs qui n'ont pas été modifiés par la Fonctionnalité A seront cassés. L'analyseur détecte cela. Corrigez-le avant de committer.
Motif courant :
- L'état du Provider a changé
CompleteParameters?enMap<String, dynamic> - Le widget Canvas appelle toujours
.colorFilterssur ce qui est maintenant une Map - Correction : Mettez à jour le consommateur pour utiliser la nouvelle interface
Étape 8 : NE PAS empiler les PRs — Combiner les fonctionnalités dépendantes en une PR
Ne créez jamais une PR qui cible la branche d'une autre PR au lieu de main.
Les PRs empilées causent de vrais problèmes dans ce projet : les rebases se propagent, le contexte du relecteur se fragmente, l'ordre de fusion devient critique, CI s'exécute contre la mauvaise base, et « fix on parent » force une re-review de l'enfant.
Si la Fonctionnalité B dépend de la Fonctionnalité A, livrez-les comme UNE SEULE PR combinée.
# Les deux fonctionnalités vivent sur une seule branche, ciblant main
git checkout main && git pull
git checkout -b feature-a-and-b
# ... committez les changements de la Fonctionnalité A ...
# ... committez les changements de la Fonctionnalité B (des commits séparés sont OK) ...
gh pr create --base main # une PR, deux fonctionnalités
Écrivez la description de PR de façon que les deux fonctionnalités soient clairement délimitées — sections séparées, plans de test séparés. Les relecteurs peuvent la lire comme une unité unique, et l'histoire de fusion est un clic. C'est la valeur par défaut quand les changements sont interdépendants.
La seule fois où scinder est quand les fonctionnalités sont vraiment indépendantes — auquel cas elles obtiennent chacune leur propre PR ciblant main, dans n'importe quel ordre. Si vous vous trouvez à vouloir « B basé sur A », c'est le signal de les combiner, pas de les empiler.
Vérification
git diff main...HEAD --stataffiche uniquement les fichiers de VOTRE fonctionnalitédart analyze(ou équivalent) passe- Les tests passent
- Le stash original existe toujours comme filet de sécurité (si pop a échoué partiellement) :
git stash list
Exemple
Session où les changements de model-dedup et les changements de fonctionnalité collaborateur/watermark étaient enchevêtrés dans 140+ fichiers modifiés. Le travail model-dedup était indépendant de la fonctionnalité collab/watermark, donc ils se sont séparés proprement :
- Ont stashé tout de
fix/profile-copy-shares-url - Créé
chore/model-deduplicationdepuis main, récupéré le stash - Séparé les fichiers model-dedup, déplacé 16 fichiers de fonctionnalité non suivis vers
/tmp/ - Committé, poussé, créé la PR #1411 (cible : main)
- Créé
feat/collabs-watermark-inspired-bydepuis main, récupéré le stash - Corrigé 3 ruptures d'interface dans video_editor_canvas.dart et video_metadata_preview_thumbnail.dart
- Committé, poussé, créé la PR #1412 (cible : main — indépendante de #1411)
Si les deux fonctionnalités avaient été interdépendantes, elles auraient été livrées comme une seule PR combinée — jamais empilées.
Alternative : Cherry-pick de commits + stash appliqué sélectif
Quand les changements sont répartis entre commits ET stash (pas seulement le stash), utilisez plutôt cette approche :
# 1. Créer les branches depuis origin/main
git branch feature-a origin/main
git branch feature-b origin/main
# 2. Cherry-pick les commits vers les branches appropriées
git checkout feature-a
git cherry-pick <commit-hash-for-feature-a>
# 3. Appliquer les fichiers stashés sélectif (voir les pièges ci-dessous)
PIÈGE : Fichiers supprimés dans le stash
git checkout stash@{0} -- <path> ÉCHOUE pour les fichiers qui ont été SUPPRIMÉS dans le stash — ils n'existent pas dans l'arborescence du stash. Gérez les suppressions séparément :
# Pour les fichiers MODIFIÉS — checkout depuis le stash fonctionne
git checkout stash@{0} -- path/to/modified_file.dart
# Pour les fichiers SUPPRIMÉS — doit utiliser git rm à la place
git rm path/to/deleted_file.dart
# Vérifiez d'abord lesquels sont des suppressions vs modifications :
git stash show stash@{0} --name-status
# D = deleted (utiliser git rm), M = modified (utiliser checkout)
PIÈGE : Préfixes de chemin du stash dans un mono-repo
Quand la racine git est un répertoire parent mais que vous travaillez dans un sous-répertoire, les chemins du stash incluent le préfixe du sous-répertoire :
# Travail dans : /project/mobile/
# Racine git : /project/
# Le stash affiche : mobile/lib/file.dart (PAS lib/file.dart)
# Doit exécuter checkout depuis la racine du repo :
cd $(git rev-parse --show-toplevel)
git checkout stash@{0} -- mobile/lib/file.dart
PIÈGE : Cherry-pick produit un commit vide
Si un commit a déjà été fusionné à main via une autre PR (hash différent), cherry-pick produira un commit vide :
# "The previous cherry-pick is now empty"
git cherry-pick --skip # L'ignorer, il est déjà sur main
PIÈGE : Hooks de pre-commit sur les branches cherry-pickées
Quand vous cherry-pickez sur des branches depuis origin/main, l'analyseur peut trouver des erreurs préexistantes sur main non liées à vos changements. Utilisez --no-verify seulement quand vous avez confirmé que les erreurs ne sont pas les vôtres :
git commit --no-verify -m "feat: your changes"
Vérifier l'exhaustivité
Comparez le contenu du stash contre tous les diffs de branche :
git stash show stash@{0} --name-only | sort > /tmp/stash_files.txt
git diff --name-only origin/main..feature-a
git diff --name-only origin/main..feature-b
# Chaque fichier du stash doit apparaître dans exactement une branche
Notes
- Toujours stasher avec
-upour capturer les fichiers non suivis - Ne jamais force-drop un stash tant que vous n'avez pas vérifié que tout est sûr
- Les fichiers à changements mixtes sont la partie la plus difficile — identifiez-les tôt en vérifiant quels fichiers ont des changements de plusieurs fonctionnalités
- Les hooks de pre-commit qui vérifient tous les fichiers (pas seulement ceux en staging) sont le plus gros blocage. Connaissez le comportement des hooks de votre projet avant de commencer.
- Ne jamais empiler les PRs. Toujours cibler
main. Si les fonctionnalités dépendent les unes des autres, combinez-les en une PR plus grande — voir l'Étape 8. - Le stash est préservé quand
popéchoue sur des fichiers non suivis — ne paniquez pas, vos données sont sûres