Minimisez les collisions

Publiez le plus tôt possible

En publiant votre version le plus tôt possible, vous réduisez la probabilité de collision avec les autres, car vous rendez votre version disponible aux autres. Par exemple en mergeant votre branche dans le master ou équivalent. Cependant, il est parfois difficile de livrer rapidement car le chantier est trop volumineux. Si c’est le cas, commencez par découper celui-ci en sous-tâches pour pouvoir livrer le code partiellement.

Livraisons partielles

La livraison partielle peut prendre différentes formes :

  • un refactoring ou un nettoyage de code avant de commencer une fonctionnalité n’a pas besoin d’attendre la fin du développement pour être publié
  • une fonctionnalité inachevée peut être publiée via feature-toggle, c’est à dire que vous envoyez une fonctionnalité en mode désactivée
  • une fonctionnalité incomplète peut parfois être publiée telle quelle
  • vivre avec deux versions du code, en faisant pointer les zones non conflictuelles sur la nouvelle version (par exemple en Java faire usage de l’annotation @Deprecated)

Pour résumer, ce qui peut être publié doit être publié.

Conservez un historique propre

Si votre historique est lisible, avec des commits unitaires représentants des étapes logiques et un message de commit compréhensible, une résolution de conflit ne posera aucun problème car le diff apporté par le commit conflictuel sera compréhensible.

Détectez et résolvez les conflits le plus tôt possible

Avec ce qui est publié

À chaque commit, il est important de réaliser un « merge check » avec ce qui a été publié (par exemple via un merge ou rebase du master). Si un conflit est détecté, il faut donc merger la version publiée (ou rebaser, uniquement si votre travail est privé, qu’il n’est regardé/utilisé par personne d’autre).

Il est tentant d’utiliser rerere pour minimiser la quantité de commits de merge dans vos branches et tout rejouer dans un seul merge à la fin. Cependant ce merge final risque d’être un condensé de plusieurs résolutions et sera peu lisible, contrairement à une résolution entre deux commits compréhensibles.

Avec l’en-cours

Un outillage tel que git-octopus permet de détecter rapidement qu’un conflit va se produire. Il s’agit tout simplement de vérifier à chaque commit que celui-ci merge avec tous les développements en cours sur d’autres branches.

Avec un historique de commit propre il est donc facile d’identifier la source du problème et de prendre une action :

  • Il est parfois possible d’éviter tout simplement le conflit
  • Livrez partiellement de la source de conflit. Vous pouvez par exemple extraire une branche livrable (qui résolve le conflit) qui permette aux deux versions de poursuivre sans lier les deux

À éviter

L’utilisation du rebase est à proscrire dès que votre branche est utilisée par d’autres personnes, que ce soit par une autre branche ou pour une revue de code. Il faut donc éviter de résoudre un conflit via un rebase dans ce cas là.

Résoudre les conflits avant leur livraison en mergeant entre elles des branches indépendantes est aussi une pratique à minimiser dans la mesure du possible. Elles ne pourront plus être livrées indépendamment, ce qui ralentira leur livraison et donc augmente les probabilités de nouveaux conflits.

Stratégie de résolution de conflit

Les avantages du rebase

Nous venons de voir que l’usage de git rebase est une mauvaise pratique dès lors que son contenu est public, pourtant celui-ci est bien pratique pour simplifier une résolution de conflit.

En effet, il joue les commits un par un, ainsi lorsqu’il provoque un conflit celui-ci est relativement simple à résoudre : nous nous plaçons dans l’état avant de jouer le commit conflictuel, et nous appliquons l’action faite par le commit conflictuel.

Cette action est simple uniquement si l’historique est intelligible. Si ce n’est pas le cas, la première étape sera de le redécouper en commits compréhensible.

Utiliser le rebase pour réaliser un merge

Dans le cas où l’usage de git rebase n’est pas une option, il est possible de passer par celui-ci, tout simplement en créant des branches temporaires à partir des anciennes, et d’employer le rebase interactif à souhait pour réécrire l’historique.

Vérifiez toujours que git diff branche_initiale..branche_rebase est vide quand vous réécrivez l’historique. Le contenu final doit rester identique, seul l’historique change.

Une fois l’historique rendu compréhensible, effectuez un rebase sur la branche provoquant un conflit pour n’avoir que des conflits provoqués par des commits compréhensibles.

Comme nous ne voulions pas utiliser un rebase mais un merge, il suffira ensuite de revenir sur les branches initiales, d’utiliser git merge et de résoudre le conflit en faisant un checkout de la branche produite via rebase.

Exemple

Plaçons nous dans le pire scénario : nous souhaitons merger une branche A dans une branche B, mais le conflit est incompréhensible, ainsi que l’historique des branches.

  • Créez une branche temporaire
    git checkout -b temp_A A
  • Détectez les commits ayant provoqué le conflit et réécrivez l’historique
    git log -- nom/de/fichier/conflictuel
    git rebase -i sha1~1 # un sha1 à réécrire détécté
    git reset HEAD~1 # annule le commit mais conserve le diff
    git add -p # par exemple, pour add qu'une partie compréhensible
    git commit # avec un message de commit compréhensible
  • Une fois l’historique de temp_A réécrit, nous pouvons faire un rebase
    git checkout temp_A
    git rebase B
  • Un conflit se produit (répétez autant de fois que de conflits se produisent)
    git log sha1 # sha1 correspond au commit provoquant le conflit 
    git reset --hard HEAD # nous annulons la tentative d'application du commit
    # nous réappliquons le patch du commit manuellement
    git add .
    git rebase --continue
    Il existe des moyens de résolution moins manuels, tel que des instructions pour garder une version ou une autre (–ours et –theirs) ou l’utilisation d’un merge-tool. Cependant ceux-ci ne permettent souvent que de régler des cas triviaux.
  • Nous récupérons temp_A avec les conflits résolu, nous pouvons maintenant en extraire un merge
    git checkout B
    git merge A # un conflit illisible se produit
    git checkout temp_A # nous résolvons le conflit en prenant la version déjà résolue
    git add .
    git commit # historique non réécrit avec conflit résolu
    git diff A..temp_A # doit être vide
    

Ce procédé est fastidieux mais permet de démêler un cas extrème. Les pratiques citées précédement permettent d’éviter de tomber dans un tel scénario. Avec un historique lisible et une détection rapide de conflit, les résolutions se retrouvent fortement simplifiées.

Quelques bonnes pratiques

  • Créez des backups : n’hésitez pas à créer des branches temporaires, en particulier lorsque vous retravaillez l’historique.
  • Testez : utilisez git diff entre deux états pour vous assurer que le résultat est celui voulu. Par exemple git diff avant..apres lorsque vous souhaitez réécrire l’historique permet de vérifier que l’état final n’a pas changé (doit être vide).