Les 7 différents types de merge en Git, partie 1/2
Les 7 différents types de merge en Git, partie 1/2
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 :
Dans cette section, nous allons rapidement mentionner quelques notions indispensables pour la suite de l’article.
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 :
git bisect
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.
Le résultat d’un merge classique peut être le suivant :
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).
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.
Une opération de rebase, sous sa forme la plus commune, peut ressembler à ceci :
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 :
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
.
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
:
À 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.
Les réglages se trouvent dans l’onglet Settings
, section Pull requests
:
Puis, lors du merge de la Pull Request, nous pouvons voir les possibilités qui ont été autorisées au niveau projet :
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.
Le résultat produit par l’option Create a merge commit
sera le suivant dans notre cas :
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.
Le résultat produit par l’option Rebase and merge
sera le suivant dans notre cas :
On observe ici 2 choses notables :
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).
Le résultat produit par l’option Squash and merge
sera le suivant dans notre cas :
On observe 3 choses notables :
La commande Git équivalente serait git merge --squash
.
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 :