Le Blog technique, scientifique et humain

Les 7 différents types de merge en Git, partie 1/2

Share

Bastien Bonnet

Passionné par le développement logiciel de haute qualité, agiliste convaincu et adepte du mouvement Software Craftsmanship

Les 7 différents types de merge en Git, partie 1/2

02/06/2023
5 min
Git logo

Introduction

On ne présente plus Git, devenu incontournable dans la gestion de version. Allons donc droit au but : quel que soit votre flux de travail avec Git et ses branches (Trunk-based development, Git Flow, Gitlab Flow, Github Flow…), vous avez régulièrement des merge de branche à effectuer.

Quand vous effectuez des merge Git, que ce soit en ligne de commande, avec des outils graphiques locaux, ou via les interfaces Github (Pull Request) et Gitlab (Merge Request), vous avez peut-être remarqué que les merge, en apparence similaires, peuvent produire des résultats différents au niveau de l’historique de vos révisions.

Par exemple, pour un même état de départ avec 2 branches F1 et F2, le résultat dans votre historique après merge peut être très variable :

Dans cet article, nous allons étudier les différentes possibilités offertes par la ligne de commande, Github et Gitlab. Puis nous prendrons un peu de recul et évaluerons les avantages et inconvénients de chaque possibilité.

Il sera publié en 2 parties :

  • Partie 1 (cette page) : Notions importantes de merge & Github
  • Partie 2 (une semaine après publication de cet article) : Gitlab & étude comparative de toutes les possibilités vues précédemment, avec en bonus une cheat sheet bien pratique.

Notions importantes

Dans cette section, nous allons rapidement mentionner quelques notions indispensables pour la suite de l’article.

Pourquoi s’embêter ?

Bonne question, après tout un merge c’est un merge, dans tous les cas l’état des fichiers sera le même dans la branche cible.

Oui, c’est vrai, mais la manière dont le merge est enregistré dans Git aura une grande influence sur plusieurs aspects :

  • Compréhension de l’historique : si votre graphe d’historique ressemble à un plat de spaghettis, lorsque vous allez avoir besoin de comprendre dans quel ordre les fonctionnalités ont été développées et intégrées, vous allez verser quelques larmes :

  • Analyse / debug : il sera toujours plus facile de trouver quel commit a introduit une anomalie lorsque l’historique est linéaire, notamment grâce à l’excellent outil git bisect
  • Limiter les erreurs de manipulation : si vous ne comprenez pas de quelle manière le merge sera enregistré dans Git, il y a de fortes chances que vous ne puissiez pas vérifier que votre merge s’est passé comme il faudrait.

Le CLI, c’est la vie

Dans cet article, lorsque nous montrerons une manipulation, nous préciserons la commande Git associée, telle qu’elle serait écrite en ligne de commande. Peu importe si vous utilisez la ligne de commande ou un outil graphique, si vous comprenez la ligne de commande, vous serez capable d’utiliser n’importe quel outil. L’inverse n’est pas vrai, car les outils graphiques, qui ne sont que des surcouches à la ligne de commande, utilisent des terminologies hétérogènes et surtout, ont la fâcheuse habitude de masquer certaines options (flags) qu’ils vont passer au CLI.

Le CLI est la référence sur le nommage et le fonctionnement.

Les différentes manières d’intégrer les changements d’une branche dans une autre branche

Merge

Le résultat d’un merge classique peut être le suivant :

git merge

Ici, c’est un merge effectué sur la branche de gauche avec git merge. Jusque-là, je pense que personne ne sera surpris : on merge une branche dans une autre, ça crée un nouveau commit (de merge).

Merge fast-forward

Ici, nous sommes toujours, comme dans la section précédente, sur un merge effectué sur la branche de gauche, toujours avec la même commande git merge :

Pourtant, dans ce cas aucun commit de merge n’est créé. Il s’agit d’un merge de type fast-forward, un terme que vous pourrez apercevoir lorsque vous faites un merge ou un pull en ligne de commande.

La différence ici est que la branche de gauche n’avait aucun commit « divergent », c’est-à-dire qui ne soit pas sur la branche de droite. Il existe donc un chemin direct (sur le graphique) entre le dernier commit de la branche de gauche et le dernier commit de la branche de droite. Dans ce cas, Git comprend qu’il n’y a pas besoin de créer un commit de merge et qu’il suffit d’« avancer » la branche de gauche au niveau du dernier commit de la branche de droite. C’est ce qu’on appelle un fast-forward, et en Git, c’est le comportement de merge par défaut.

À chaque fois qu’on fast-forward sera possible, git merge l’effectuera, à moins qu’une option forçant le comportement contraire soit donnée. La raison de cela est de garder l’arbre de révision, ou graphe, le plus simple possible pour en faciliter la lecture. Inutile de créer un commit de merge qui n’apporte aucune différence et complexifie le graphe.

Rebase

Une opération de rebase, sous sa forme la plus commune, peut ressembler à ceci :

git rebase

Ici on a rebase « la branche de droite sur la branche de gauche ». Les commits uniquement présents sur la branche de droite ont été « décrochés » de leur emplacement initial pour être replacés au-dessus de l’emplacement cible.

Note : les commits sont déplacés un par un, ce qui explique que vous pouvez avoir des conflits intermédiaires (et parfois répétés) lors d’une opération de rebase.

Cela n’est qu’une des nombreuses formes de rebase, qui peut être utilisé pour effectuer différents types de manipulations :

  • Déplacer des morceaux de branches / groupes de commits
  • Réordonner des commits dans une branche
  • Supprimer des commits
  • Fusionner des commits
  • Modifier des commits
  • Renommer des commits

Le squash

Squash est un terme générique (pas une commande), qui désigne le concept de « compresser » plusieurs commits en un seul. On peut squash lors d’un merge ou d’un rebase.

Comparaison des différents modes de merge : Github & Pull Requests

Pour le reste de cet article, nous utiliserons un état de départ, c’est-à-dire avant merge, constitué d’une branche principale, main, et de 2 branches de fonctionnalité, F1 et F2:

État de départ

À chaque fois, l’ordre de merge sera le suivant : F2 est mergée en premier, puis F1.

Sur Github, le fonctionnement est le suivant : au niveau projet, les possibilités de merge autorisées sont sélectionnées. Seules les possibilités ainsi sélectionnées seront accessibles au moment du merge de la Pull Request. C’est à ce moment que le mode de merge pour la PR concernée sera choisi.

Réglages niveau projet et choix lors de la Pull Request

Les réglages se trouvent dans l’onglet Settings, section Pull requests :

Github - Project settings for merges

Puis, lors du merge de la Pull Request, nous pouvons voir les possibilités qui ont été autorisées au niveau projet :

Github - Pull Request merge selection

L’option sélectionnée par défaut est la dernière que vous avez utilisée. Le nom sur le bouton est celui de l’option sélectionnée.

Étudions maintenant le résultat de chacune de ces possibilités.

« Create a merge commit »

Le résultat produit par l’option Create a merge commit sera le suivant dans notre cas :

Github - Create a merge commit result

On observe notamment qu’un commit de merge est créé à chaque fois, même quand ce ne serait pas nécessaire, c’est-à-dire qu’un fast-forward serait possible, comme vu dans la première partie. On force donc Git à faire un commit de merge, ce qui est différent de son comportement par défaut.

La commande Git équivalente serait un git merge --no-ff, où l’option --no-ff indique de ne pas faire de fast-forward même si c’est possible.

« Rebase and merge »

Le résultat produit par l’option Rebase and merge sera le suivant dans notre cas :

Github - Rebase and merge result

On observe ici 2 choses notables :

  • Chaque commit non-présent sur la branche cible du merge a été déplacé au sommet actuel de celle-ci
  • L’historique résultant est linéaire

La commande Git équivalente serait un git rebase (pour déplacer les commits au sommet de la branche cible) suivi d’un git merge (pour intégrer lesdits commits en fast-forward, à présent possible).

« Squash and merge »

Le résultat produit par l’option Squash and merge sera le suivant dans notre cas :

Github - Squash and merge result

On observe 3 choses notables :

  • Les commits des branches ont été squash (quand il y en avait plusieurs)
  • Chaque commit non-présent sur la branche cible du merge a été déplacé au sommet actuel de celle-ci
  • L’historique résultant est linéaire et simple (1 commit par fonctionnalité)

La commande Git équivalente serait git merge --squash.

Conclusion

Dans cette première partie, nous avons vu pourquoi il est important de se soucier du résultat de merge, ainsi que les différentes possibilités offertes par Github.

Dans la seconde partie, publiée une semaine après cet article, nous verrons les possibilités offertes par Gitlab, puis nous comparerons toutes ces possibilités et verrons les avantages et inconvénients de chacune, avec mes recommandations suivant le cas de figure.

N’hésitez pas à poser des questions et à lancer des discussions en réaction à cet article !


Sources images :

  • Logo Git : par Jason Long — http://git-scm.com/downloads/logos, CC BY 3.0, https://commons.wikimedia.org/w/index.php?curid=19329352

Une réaction ? Contribuez à cet article en laissant un commentaire :

Aucune réaction à cet article pour l'instant. Et si vous étiez le premier ?

Vous pourriez aimer