Comment résoudre les conflits de fusion Git difficiles étape par étape

Résolvez les conflits de fusion Git difficiles en lisant les versions ours, leur et base, en gérant les renommages, les rebases, les fichiers binaires et les tests.

Comment résoudre les conflits de fusion Git difficiles étape par étape

Un conflit de fusion Git difficile est rarement difficile à cause des marqueurs de conflit eux-mêmes. Il est difficile parce que vous devez préserver l'intention de deux modifications différentes en même temps. Une branche a renommé une fonction tandis qu'une autre a modifié son comportement. Une branche a déplacé un fichier tandis qu'une autre l'a édité. Une branche a modifié une séquence de migration de base de données. Git peut vous montrer le chevauchement, mais il ne peut pas décider ce que le logiciel doit signifier après.

Lorsqu'une fusion s'arrête avec des conflits, ne commencez pas à supprimer les marqueurs immédiatement. D'abord, orientez-vous.

git status

Git listera les chemins non fusionnés. Il peut dire both modified, deleted by us, deleted by them, both added, ou quelque chose de similaire. Ces phrases vous indiquent la forme du conflit.

Si la fusion semble erronée ou si vous n'êtes pas prêt à la résoudre, annulez avant de faire d'autres modifications :

git merge --abort

Pour un conflit de rebase, l'équivalent est :

git rebase --abort

Ce n'est pas un échec. C'est une réinitialisation propre à l'état avant l'opération, ce qui est souvent la décision la plus intelligente lorsque vous réalisez que vous avez besoin de plus de contexte.

Lire le conflit comme trois versions

Un marqueur de conflit normal ressemble à ceci :

<<<<<<< HEAD
version de la branche actuelle
=======
version de la branche entrante
>>>>>>> feature-branch

Lors d'une fusion, HEAD est la branche que vous aviez extraite lorsque vous avez exécuté git merge. Le côté inférieur est la branche en cours de fusion.

Pour les conflits difficiles, utilisez les trois étapes que Git conserve dans l'index :

git show :1:path/to/file   # ancêtre commun
git show :2:path/to/file   # nôtre
git show :3:path/to/file   # leur

L'ancêtre commun est la version à partir de laquelle les deux branches ont commencé. Il est utile car il montre ce que chaque branche a réellement changé. Sans lui, vous pourriez comparer deux versions finales et manquer la raison derrière elles.

Vous pouvez également utiliser :

git diff
git diff --ours -- path/to/file
git diff --theirs -- path/to/file
git diff --base -- path/to/file

C'est là que beaucoup de gens vont trop vite. Le but n'est pas de choisir "nôtre" ou "leur" comme un vote de loyauté d'équipe. Le but est de produire le fichier final correct.

Un flux de travail manuel sûr

Utilisez cette routine pour chaque fichier en conflit :

  1. Ouvrez le fichier et trouvez chaque marqueur de conflit.
  2. Lisez le code environnant, pas seulement les lignes marquées.
  3. Vérifiez les commits qui ont touché le fichier sur les deux branches.
  4. Éditez le fichier dans la version finale prévue.
  5. Supprimez tous les marqueurs de conflit.
  6. Exécutez le test ou la vérification de construction la plus petite pertinente.
  7. Staguez le fichier.

Commandes utiles pendant cela :

git log --oneline --left-right --merge -- path/to/file
git diff --check
git grep -n '<<<<<<<\|=======\|>>>>>>>'

git diff --check détecte les problèmes d'espaces résiduels. git grep détecte les marqueurs de conflit oubliés avant qu'ils n'atteignent l'intégration continue.

Après avoir résolu un fichier :

git add path/to/file

Lorsque tous les conflits sont stagés :

git status
git commit

Lors d'un rebase, utilisez :

git rebase --continue

Quand les deux branches ont modifié la même fonction

C'est le cas courant. Supposons qu'une branche ajoute une validation et l'autre renomme un paramètre :

<<<<<<< HEAD
function createUser(email) {
  return db.users.insert({ email });
}
=======
function createUser(rawEmail) {
  const email = rawEmail.trim().toLowerCase();
  return db.users.insert({ email });
}
>>>>>>> normalize-email

La bonne réponse peut combiner les deux :

function createUser(rawEmail) {
  const email = rawEmail.trim().toLowerCase();
  return db.users.insert({ email });
}

Mais seulement si les appelants ont été mis à jour pour passer rawEmail, et seulement si la normalisation est toujours souhaitée. Recherchez la fonction :

git grep -n 'createUser'

Les conflits difficiles nécessitent souvent de vérifier les fichiers voisins. Un conflit de signature de fonction dans un fichier peut nécessiter des mises à jour dans les tests, les routes, les types, les mocks ou la documentation.

Conflits de renommage et d'édition

Les conflits de renommage sont ennuyeux car le fichier que vous voulez peut ne pas être là où vous l'attendez. Commencez par le statut :

git status --short

Ensuite, inspectez les informations de nom-statut :

git diff --name-status --diff-filter=R

Si une branche a renommé src/user.js en src/account.js et l'autre a édité src/user.js, vous voulez généralement le contenu édité appliqué au nouveau chemin. Un outil de fusion visuel peut aider, mais le concept est simple : préservez le renommage et préservez les modifications significatives.

Après avoir décidé du chemin final, supprimez le chemin obsolète si nécessaire et staguez le chemin final :

git rm old/path.js
git add new/path.js

Ne staguez pas les deux fichiers à moins que le projet final ne doive vraiment contenir les deux.

Supprimé par nous ou supprimé par eux

Un conflit de suppression/modification signifie qu'une branche a supprimé un fichier tandis que l'autre l'a modifié. Git ne peut pas savoir si la suppression a rendu la modification non pertinente.

Si le fichier doit rester supprimé :

git rm path/to/file

Si le fichier doit rester, choisissez la version que vous voulez et staguez-la :

git checkout --theirs path/to/file
git add path/to/file

ou :

git checkout --ours path/to/file
git add path/to/file

Soyez prudent avec --ours et --theirs lors d'un rebase. Dans un rebase, les étiquettes peuvent sembler inversées car Git rejoue vos commits sur une autre base. En cas de doute, inspectez les étapes :

git show :2:path/to/file
git show :3:path/to/file

Conflits de fichiers binaires

Git ne peut pas fusionner la plupart des fichiers binaires. Si deux branches ont modifié la même image, archive, document ou actif compilé, vous devez choisir une version ou créer un nouveau fichier manuellement.

Pour prendre notre version :

git checkout --ours path/to/file.bin
git add path/to/file.bin

Pour prendre leur version :

git checkout --theirs path/to/file.bin
git add path/to/file.bin

Si le binaire est généré, la meilleure réponse peut être de le régénérer à partir de la source après avoir résolu les fichiers texte. Si le binaire est un actif de conception ou un document, parlez à la personne qui a modifié l'autre côté. Deviner peut détruire le travail.

Utilisez un outil de fusion lorsque le fichier est trop difficile à lire

Un bon outil de fusion montre quatre choses : la version de base, votre version, leur version et le résultat. Configurez-en un que vous aimez vraiment. Visual Studio Code est courant :

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

Ensuite, exécutez :

git mergetool

D'autres équipes préfèrent Meld, KDiff3, Beyond Compare ou des outils intégrés à l'IDE. L'outil importe moins que la compréhension des trois versions. Ne cliquez pas sur "accepter entrant" à travers un conflit complexe juste pour faire disparaître les marqueurs rouges.

Après avoir utilisé un mergetool, vérifiez les fichiers de sauvegarde tels que .orig :

git status --short

Vous pouvez désactiver globalement les fichiers de sauvegarde du mergetool si vous ne les voulez pas :

git config --global mergetool.keepBackup false

Les options de stratégie ne sont pas magiques

Vous pouvez voir des conseils comme :

git merge -X theirs feature

Cela ne signifie pas "remplacer ma branche par feature." Cela signifie que lorsque la stratégie de fusion de Git voit des morceaux en conflit, elle doit préférer l'autre côté pour ces morceaux. Les modifications non conflictuelles des deux branches sont toujours fusionnées. Cela peut être utile pour les fichiers de verrouillage générés ou les conflits de formatage mécaniques, mais c'est risqué pour la logique métier.

-X ours et -X theirs sont des options de stratégie. La stratégie de fusion ours est différente :

git merge -s ours old-branch

Cela enregistre une fusion tout en conservant l'arbre actuel. C'est un outil spécialisé, souvent utilisé pour marquer une branche comme fusionnée sans prendre son contenu. Ne l'utilisez pas pour une résolution de conflit normale à moins d'être très sûr.

Conflits de rebase

Lors d'un rebase, Git rejoue les commits un par un. Cela signifie que vous pouvez résoudre plusieurs petits conflits au lieu d'un grand conflit de fusion.

La boucle est :

git status
# éditer les fichiers
git add resolved-file
git rebase --continue

Si un commit en cours de rejeu n'est plus nécessaire parce que la nouvelle base contient déjà le changement, utilisez :

git rebase --skip

Utilisez skip avec précaution. Cela supprime ce commit de la branche rebasée. Lisez d'abord le commit :

git show

Encore une fois, --ours et --theirs peuvent être déroutants dans un rebase. Inspectez :2: et :3: en cas de doute.

Testez la résolution, pas seulement la fusion

Une fusion peut être syntaxiquement résolue et toujours être erronée. Après avoir stagé les fichiers, exécutez des tests qui touchent la zone modifiée. Pour un conflit frontend, cela peut être une vérification de type et un test de composant ciblé. Pour un conflit backend, cela peut être un test de service ou une vérification de migration. Pour un conflit de fichier de verrouillage, réinstallez les dépendances et exécutez la commande de vérification du gestionnaire de paquets.

Au minimum :

git diff --check
git grep -n '<<<<<<<\|=======\|>>>>>>>'

Ensuite, exécutez la vérification spécifique au projet qui aurait détecté une mauvaise combinaison.

Réduire les futurs conflits

Le meilleur conflit est celui que vous ne créez jamais. Gardez les branches de courte durée, rebasez ou fusionnez régulièrement à partir de la branche principale, et évitez de mélanger les modifications mécaniques avec les modifications de fonctionnalités. Une PR de formatage uniquement ne devrait pas non plus modifier la logique. Un déplacement de fichier ne devrait pas non plus réécrire le fichier si vous pouvez l'éviter.

Pour les fichiers qui sont toujours douloureux, envisagez des changements de propriété ou de structure. Les grands fichiers de configuration, les instantanés générés, les fichiers de verrouillage, les listes de migration et les registres de route centraux créent souvent des conflits répétés parce que tout le monde édite la même zone. Parfois, la solution est un processus. Parfois, la solution est de diviser le fichier ou de le générer à partir de sources plus petites.

Utilisez .gitattributes pour les fichiers qui nécessitent un comportement de fusion spécial. Par exemple, certains fichiers de verrouillage générés peuvent avoir des pilotes de fusion spécifiques au gestionnaire de paquets. N'en inventez pas un à la légère, mais vérifiez si votre écosystème a un pilote recommandé.

Les conflits de fusion sont en partie un travail technique et en partie une communication. Si vous ne comprenez pas l'intention de l'autre branche, demandez. Dix minutes avec l'auteur coûtent moins cher que de fusionner silencieusement du code qui passe les tests mais supprime la fonctionnalité qu'ils construisaient.

Fichiers de verrouillage, migrations et autres fichiers à forte friction

Certains fichiers entrent en conflit plus souvent parce que de nombreuses branches éditent la même petite zone. Les fichiers de verrouillage de dépendances en sont un exemple courant. Si deux branches ajoutent des paquets, le conflit du fichier de verrouillage peut être techniquement important mais conceptuellement simple : régénérez-le avec le gestionnaire de paquets après avoir résolu le fichier manifeste.

Pour un projet Node, cela pourrait signifier résoudre package.json, puis exécuter le gestionnaire de paquets qui possède le fichier de verrouillage :

npm install
# ou pnpm install
# ou yarn install

Ensuite, staguez à la fois le manifeste et le fichier de verrouillage. N'éditez pas manuellement un fichier de verrouillage complexe à moins de comprendre son format. Le gestionnaire de paquets est moins susceptible de faire une erreur subtile dans le graphe de dépendances.

Les migrations de base de données nécessitent plus de soin. Si deux branches créent des migrations avec des hypothèses d'ordre, accepter les deux fichiers peut ne pas suffire. Vérifiez les horodatages de migration, les numéros de séquence, les dépendances et si les deux migrations modifient la même table ou les mêmes données. Parfois, la bonne résolution est une nouvelle migration de suivi qui réconcilie les deux branches.

Les instantanés générés et les fichiers golden ont le même modèle : résolvez d'abord le changement source, régénérez la sortie, puis examinez le diff généré. Si le diff généré est énorme, demandez-vous s'il appartient au même commit de fusion. D'énormes changements générés peuvent cacher une mauvaise résolution manuelle.

Lorsqu'un conflit s'étend sur plusieurs fichiers, notez le comportement final prévu avant d'éditer. Une courte note comme "conserver la nouvelle validation de la fonctionnalité A, conserver le service renommé de la fonctionnalité B, régénérer les types clients" vous empêche de résoudre chaque fichier localement tout en perdant la conception globale.

Pour les fusions particulièrement risquées, créez une branche temporaire avant de commencer :

git switch -c merge-test/main-with-feature
git merge feature

Si la résolution devient désordonnée, vous pouvez abandonner la branche temporaire sans perturber votre branche d'origine. Cette petite habitude rend les conflits difficiles moins stressants car vous avez toujours un chemin de retour propre.

Examinez la fusion finale comme un changement en soi

Une résolution de conflit est un nouveau travail. Traitez-la comme telle lors de la révision. Le diff final devrait montrer non seulement les modifications des deux branches, mais aussi tout code de colle que vous avez écrit pour les faire fonctionner ensemble. Si le commit de fusion est volumineux, expliquez la résolution dans le message de commit ou le commentaire de pull request. Les réviseurs ne devraient pas avoir à rétro-ingénierer pourquoi un côté a été choisi.

Avant de pousser, comparez le résultat final avec les deux parents lorsque possible :

git diff HEAD^1..HEAD -- path/to/file
git diff HEAD^2..HEAD -- path/to/file

Pour une fusion non commitée, inspectez les modifications stagées :

git diff --cached

Recherchez la suppression accidentelle de tests, les imports qui ne sont plus utilisés, les entrées de configuration dupliquées et les chemins de code où les deux branches ont ajouté une logique similaire sous des noms différents. Ce sont les erreurs que Git ne peut pas identifier pour vous.

Si le conflit impliquait un comportement, ajoutez ou mettez à jour un test qui échouerait si vous aviez choisi le mauvais côté. Ce test fait plus que prouver la fusion d'aujourd'hui. Il protège la décision d'être annulée lors du prochain refactoring.