Résolution des conflits
Un conflit signifie que Git a trouvé deux modifications au même endroit et refuse de deviner. Une résolution est une décision sur l'intention, et la valeur par défaut sûre est de conserver l'intention des deux côtés. Résoudre en supprimant un côté fait disparaître les marqueurs mais aggrave le problème : la compilation passe toujours, et le changement d'un collègue a disparu.
1. Savoir dans quelle opération vous êtes – ours et theirs en dépendent
git status # "rebase in progress" / "You have unmerged paths" / "cherry-pick in progress"
| Opération | Côté --ours / HEAD | Côté --theirs |
|---|---|---|
merge | la branche sur laquelle vous êtes | la branche en cours de fusion |
rebase | la branche sur laquelle vous rebasez (amont) | votre propre commit en cours de rejeu |
cherry-pick, revert, stash pop | la branche sur laquelle vous êtes | le commit en cours d'application |
La ligne rebase est le piège. Pendant git rebase main, "ours" est main et "theirs" est le travail de l'utilisateur, l'inverse d'une fusion. git checkout --theirs file dans un rebase conserve votre modification et jette celle de l'amont. Relisez ce tableau avant d'utiliser l'un ou l'autre drapeau.
2. Voir tout le conflit, pas seulement les marqueurs
git diff --name-only --diff-filter=U # fichiers non fusionnés
git status # le type : both modified / deleted by us / both added
git config merge.conflictStyle zdiff3 # affiche aussi l'ancêtre commun (git 2.35+ ; utilisez diff3 sur les versions plus anciennes)
git checkout --conflict=zdiff3 -- <file> # ré-affiche UN fichier avec la base visible (écarte les modifications déjà faites)
Avec la base visible, les marqueurs ont trois parties : votre côté, l'original, leur côté. La plupart des conflits apparents deviennent évidents quand on peut voir ce que chaque côté a changé à partir de.
Pour comprendre pourquoi chaque côté a changé :
git log --merge -p -- <file> # commits des deux côtés touchant le fichier (fusion ou rebase en cours)
git show REBASE_HEAD # pendant un rebase : le commit en cours de rejeu
git show :1:<file> :2:<file> :3:<file> # base, "ours", "theirs" sous forme de fichiers complets
Lisez les messages de commit. Une résolution que vous ne pouvez pas expliquer en une phrase par côté est une supposition.
3. Résoudre en combinant
- Les deux côtés ont modifié les mêmes lignes pour des raisons différentes – écrivez la version qui fait les deux. Par exemple, un côté a ajouté un paramètre
tax_rateet l'autre un paramètrediscount: la fonction résolue prend les deux et les applique tous les deux. - Un côté a déplacé ou renommé, l'autre a modifié – appliquez la modification au nouvel emplacement. Ne gardez pas l'ancien fichier et le nouveau.
- Supprimé par un côté, modifié par l'autre – c'est une véritable décision, pas mécanique. Découvrez pourquoi il a été supprimé ; si la modification compte toujours, elle doit être reportée, sinon la suppression gagne. Demandez à l'utilisateur si ce n'est pas clair.
- Fichiers générés et fichiers de verrouillage (
package-lock.json,poetry.lock,go.sum, snapshots, sortie compilée) – ne faites pas de fusion manuelle. Prenez un côté, puis régénérez avec l'outil (npm install,poetry lock,go mod tidy, la commande de snapshot). - Fichiers binaires – il n'y a pas de fusion ; choisissez un côté délibérément (gardez le tableau ci-dessus à l'esprit) et dites-le.
Le texte peut fusionner proprement et être quand même faux : si un côté a renommé une fonction et l'autre a ajouté un appel à l'ancien nom, Git ne voit pas de conflit. C'est pourquoi l'étape 4 inclut la compilation.
Les drapeaux globaux cachent des décisions. git checkout --ours ., -X ours / -X theirs (qui résolvent chaque bloc conflictuel vers un côté) et -s ours (qui jette complètement l'autre branche) jettent chacun du travail sans le regarder. Utilisez-les uniquement lorsque l'utilisateur a demandé exactement cela et sait ce qui est abandonné.
4. Vérifier, puis continuer
git diff --check # échoue sur les marqueurs de conflit restants et les erreurs d'espacement
grep -rnE '^(<<<<<<<|=======|>>>>>>>)' . # ceinture et bretelles, dans les fichiers que git ne vérifie pas avec diff-check
# compiler et exécuter les tests les plus rapides et pertinents
git add <fichiers-résolus>
Continuez avec la commande correspondante. Ajoutez GIT_EDITOR=true là où un éditeur s'ouvrirait, car un éditeur bloque dans un shell non interactif :
git merge --continue # ou : git commit --no-edit
GIT_EDITOR=true git rebase --continue
GIT_EDITOR=true git cherry-pick --continue
Un rebase rejoue les commits un par un, il peut donc s'arrêter à nouveau sur le commit suivant. Répétez la boucle. Résolvez au premier commit où un conflit apparaît ; les suivants peuvent être le même conflit qui se rejoue.
Si les tests échouent après la résolution, l'échec est une information sur la résolution. Ne continuez pas au-delà et ne "corrigez" pas en modifiant les tests tant que vous ne savez pas quel comportement le test protège.
5. Quand s'arrêter et demander
Arrêtez-vous plutôt que de deviner quand :
- les deux côtés ont modifié la logique métier de manière incompatible (deux règles fiscales différentes, deux politiques de réessai différentes) ;
- vous ne pouvez pas dire en une phrase pourquoi chaque côté a fait son changement ;
- la résolution fait échouer un test et la cause n'est pas claire.
Énoncez les deux intentions, dites ce que vous choisiriez et pourquoi, et laissez l'utilisateur décider. Tout avant --continue est réversible : git merge --abort, git rebase --abort, git cherry-pick --abort ramènent l'arbre à son état initial.
Conflits répétés
Pour un long rebase où le même conflit revient, ou une branche qui est fusionnée et abandonnée à plusieurs reprises :
git config rerere.enabled true # enregistre chaque résolution et la rejoue la prochaine fois
git rerere diff # ce qui a été enregistré
git rerere forget-path <file> # abandonne une résolution enregistrée qui était erronée
rerere réapplique une résolution sans demander, donc une mauvaise se propage. Après un conflit résolu par rerere, vérifiez toujours le résultat.
Quand une branche est ancienne et loin derrière, fusionner le tronc une fois signifie généralement un seul tour de conflits ; rebaser les rejoue par commit. Ce choix appartient à l'utilisateur.
Garde-fous
- Ne résolvez jamais en prenant un côté en entier pour faire disparaître les conflits. L'autre côté est le travail de quelqu'un.
- N'abandonnez pas un rebase que l'utilisateur vous a demandé de terminer juste parce qu'il est fastidieux ; abandonnez seulement si ça tourne mal, et dites-le.
- Ne poussez pas après avoir résolu sauf demande explicite. Un rebase nécessite un push forcé ; suivez
git-safe-history-rewrite. - Signalez chaque fichier en conflit : ce que les deux côtés ont fait, ce que vous avez conservé, et tout ce que vous n'avez pas pu vérifier.