22/10/2024

Fusionner des branches dans Git

Une fusion (ou « merge ») dans Git permet de combiner les modifications de différentes branches en une seule, le plus souvent « master » (ou « main »). Cette opération permet d'intégrer les lignes de développement parallèles créées avec la commande « git branch » dans une branche cible, créant ainsi un historique des commits unifié. Il existe plusieurs formes de « merge » dont les trois principaux sont le fast-forward merge, le three-way merge et le squash and merge.

Pour explorer ces différents modes de fusion, commençons par supprimer et recréer notre branche « feature » pour voir le processus depuis le début.

# Se déplacer (faire un "checkout") sur une autre branche
$ git checkout main
Switched to branch 'main'

# Supprimer une branche
$ git branch -d feature
Deleted branch feature (was eb41526).

# Créer une branche et se déplacer dessus
$ git checkout -b feature

Modifiez vos fichiers à quelques reprises et faites 2 ou 3 commits selon le workflow que nous avons vu précédemment pour mieux visualiser l'opération de fusion. L'état de votre dépôt Git ressemblera à l'illustration suivante :

I. Le fast-forward merge

Un fast-forward merge (« fusion vers l'avant ») se produit lorsque la branche principale (« master » ou « main ») vers laquelle on veut faire une fusion n’a pas eu de nouveaux commits. Dans ce cas, Git déplace simplement le pointeur de la branche cible (« master » ou « main ») vers le dernier commit de la branche source (ici : « feature »).

Pour faire notre fast-forward merge (et de manière générale, toute opération de fusion), nous devons d'abord nous positionner sur la branche vers laquelle la fusion sera effectuée, c'est-à-dire « master » (ou « main »). Il faut donc faire un « git checkout » en un premier temps :

# Se déplacer (faire un "checkout") sur une autre branche
$ git checkout main
Switched to branch 'main'

C'est ce que nous pouvons voir dans l'illustration suivante qui montre également que 2 commits ont été effectués sur la branche « feature » :

Le pointeur HEAD indique que nous sommes bien sur la branche « master » (ou « main »), celle vers laquelle nous voulons fusionner les changements de la branche secondaire « feature ». Nous pouvons maintenant faire notre fast-forward merge avec la commande « git merge <branche_source> » :

# Faire un "fast-forward merge"
$ git merge feature
Updating eb41526..e136792
Fast-forward
Fast-forward

Tout dépendant des modifications que vous avez faites dans votre dépôt, la sortie dans le terminal de votre VS Code devrait ressembler à ceci :

Sur l'illustration qui suit, nous pouvons voir que la branche « master » (ou « main ») s'est mise à jour en avançant au dernier commit de la branche « feature ». Ce type de fusion conserve un historique des commits linéaire et ne nécessite pas de créer un « merge commit » ou « commit de fusion » pour compléter l'opération.

Après avoir effectué ce merge, vous verrez que la branche « master » (ou « main ») aura intégré toutes les modifications effectuées dans « feature ». Vous pouvez maintenant supprimer votre branche secondaire avec la commande « git branch -d <nom_de_la_branche> ».

# Récapitulatif des commandes vues dans ce chapitre

git merge <nom branche>   Fusionner une branche avec la branche en cours

# Dans ce type de fusion, il y a eu des commits sur la branche secondaire seulement

II. Le three-way merge

Le three-way merge (ou « fusion à trois voies ») est un autre mode de fusion standard qui crée un nouveau commit pour combiner les modifications de deux branches. Il utilise l’ancêtre commun des branches pour intégrer les changements.

Commençons par un exemple qui simule le travail de deux contributeurs, l'un sur la branche « feature » et l'autre sur la branche « master » (ou « main »), mais chacun d'entre eux ne travaille pas sur le même fichier (autrement, nous aurions un conflit lors du merge, nous verrons dans la suite de ce cours comment les résoudre).

Faites quelques commits sur la branche « master » (ou « main ») et recréez votre branche « feature » pour que nous puissions avoir un début d'historique linéaire pour les deux branches. Ensuite, faites des modifications pour avoir 3 commits sur la branche « master » (ou « main »), puis déplacez-vous (« git checkout ») sur « feature ». Créez un nouveau fichier qui n'existe pas sur la branche principale et faites des modifications pour qui vous permettront de faire 3 commits.

Votre dépôt pourra alors être représenté ainsi :

Comme nous pouvons le voir dans l'illustration précédente, nous avons déjà fait un checkout vers la branche « master » (ou « main ») (voir la position du pointeur HEAD). Nous pouvons effectuer un three-way merge avec la même commande que le fast-forward merge :

# Faire un "three-way merge"
$ git merge feature

Après avoir exécuté la commande, Git ouvrira un éditeur de texte pour que nous puissions écrire le message du nouveau commit qui permettra de compléter l'opération (vous pouvez laisser le message « Merge branch 'feature' » par défaut) :

Voici la sortie de ce type de fusion :

$ git merge feature
Merge made by the 'ort' strategy.
 test.sh | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 test.sh

Nous n'insisterons pas ici sur la stratégie ORT, mais il s'agit d'un three-way merge où Git combine automatiquement les modifications de deux branches en utilisant trois éléments (d'où le nom de ce type de merge) : la branche actuelle (1), la branche à fusionner (2) et leur ancêtre commun (3) :

Dans l'image précédente, (1) le commit 8f3cb0a (où la branche « feature » a été créée), (2) le commit e733d3b (où la branche « feature » se termine) et (3) le commit 85dc0c8 (où la branche « main » se termine) seront fusionnés pour créer un nouveau commit de merge : cba44b1 (où se trouve maintenant le pointeur HEAD).

Lorsqu’il n’y a pas de chemin linéaire de la branche source vers la branche cible, Git n’a d’autre choix que de les combiner avec une fusion à trois voies qui utilise un commit dédié pour lier les deux historiques.

Dans VS Code, vous pouvez utiliser la commande « git log --graph --decorate --oneline » pour avoir une représentation ASCII semblable à celle de Vizualizing Git du three-way merge que vous venez de faire :

Comme le montre cette commande, le three-way merge ne maintient pas un historique linéaire, mais il conserve tous les commits de la branche « feature ». Dans certains cas, il peut être souhaitable de ne pas garder l'historique des commits de la branche secondaire, c'est ce que permet de faire le prochain type de fusion.

# Récapitulatif des commandes vues dans ce chapitre

git merge <nom branche>   Fusionner une branche avec la branche en cours 

# Dans ce type de fusion, il y a eu des commits sur les 2 branches

III. Le squash and merge

Le squash and merge (« écrasement et fusion ») fonctionne de la manière que le three-way merge, mais il ne conserve pas l'historique des commits de la branche secondaire. Ce type de fusion fonctionne en deux temps (squash/merge) :

  • 1. Squash : Git confirme qu'il a combiné tous les commits de la branche source (« feature ») en un seul.
  • 2. Merge : Un nouveau commit doit être effectué pour fusionner ces changements avec la branche cible « master » (ou « main »).

Continuons avec un exemple où nous avons fait 3 nouveaux commits sur les deux branches (« feature » et « master » ou « main »), comme la représentation initiale du three-way merge que nous avons vu dans le chapitre précédent :

Dans VS Code, nous pouvons voir, en effet, que 3 commits ont été effectués sur « feature ». Dans les modifications apportées, nous avons ajouté un fichier « test2.sh » qui n'existe pas dans « main » .

Pour ce qui est de la branche « main », nous voyons que nous avons fait 4 commits depuis le merge précédent.

Tentons maintenant de faire un « git merge --squash feature » en faisant d'abord un checkout sur « main » :

Nous voyons ici que la branche « main » s'est mise à jour car le fichier « test2.sh » s'est ajouté (A) à celle-ci et Git indique qu'il a arrêté avant de faire le commit ce qui nous permet de confirmer que l'opération va fonctionner sans problème. C'est la partie squash (1) : Git a combiné tous les commits de la branche source « feature » en un seul commit. Nous devons maintenant faire un commit final pour compléter le merge (2), c'est-à-dire pour intégrer ce commit unique dans la branche cible « main ».

Avec ce type de fusion, les 3 commits ont été rassemblés en un seul nommé que nous avons nommé « Squash and merge », mais l'historique (voir avec la commande « git log --graph --decorate --oneline ») n'indique pas qu'un merge a eu lieu comme dans le cas précédent au commit 41697e4 (au bas de la capture).

Dans la représentation Visualizing Git, nous voyons que nous n'avons plus de trace des commits de la branche « feature ».

Vous vous demandez peut-être quel est l'intérêt de faire un squash and merge ? En fait, si nous nous plaçons dans le contexte d'une équipe de développement, il peut être souhaitable de ne pas polluer l'historique des commits avec ceux qui ont été faits dans une branche secondaire. Ceci permet de conserver un historique linéaire et plus facile à consulter par la suite.

Dans notre cas, nous n'avons qu'une seule branche « feature » avec quelques commits, mais une équipe peut avoir des dizaines de branches secondaires et même des sous-branches, ce qui rend l'avancement du développement beaucoup plus difficile à suivre. Dans l'illustration suivante de VS Code, nous n'avons qu'un seul three-way merge, mais imaginez qu'il y en a des centaines dont certains sont imbriqués...

# Récapitulatif des commandes vues dans ce chapitre

git merge --squash <nom branche>   Fusionner une branche avec la branche en cours (sans conserver l'historique de la branche secondaire) 

# Dans ce type de fusion, il y a eu des commits sur les 2 branches

IV. Le rebase (and merge)

Le rebase (ou rebase and merge) est une opération qui permet de réappliquer les commits d'une branche à la suite des commits d'une autre branche. Durant un rebase, la branche cible devient la nouvelle « base » de la branche source. Voyons un exemple pour mieux comprendre.

Dans l'illustration qui suit, nous avons un scénario semblable à ce que nous avons vu précédemment : une branche (« feature ») avec quelques commits (2) et la branche « master » (ou « main ») qui a aussi 2 commits depuis la création de la branche secondaire.

Pour faire un rebase, nous devons rester sur la branche source (contrairement aux merge précédents) et indiquer la branche qui sera sa nouvelle « base » avec la commande « git rebase <branche_cible> ». Ici, notre branche source est « feature » et nous voulons la « rebaser » sur « master » (ou « main ») :

# Faire un rebase de "feature" sur "main" ou "master"
$ git rebase main

Ici, deux choses se sont produites :

  • 1. Relecture des commits : Git va prendre tous les commits de la branche « feature » et les appliquer un par un sur la branche « master ». Dans notre exemple, nous avons 2 commits qui ont disparu de « feature » pour s'ajouter à « master ». Ils sont représentés par les points mauve et bleu.
  • 2. Réécriture de l’historique : L'historique des commits de la branche « feature » sera réécrit pour apparaître comme s'ils avaient été créés après le dernier commit de la branche « master ».

Le point mauve désigne le premier commit de « feature » qui a été « rebasé » sur l'historique de « master ». Nous voyons que le pointeur HEAD s'est placé vis-à-vis de « feature » qui a maintenant un historique linéaire avec « master » qui n'a pas eu de nouveaux commits encore. Si vous faites un commit sur « master », la branche bifurquer et commencer un nouvel historique en parallèle.

Le rebase réécrit l’historique des commits, ce qui change les hashes des commits. Chaque commit a un hash unique basé notamment sur son contenu et son parent. En changeant l’ordre ou le parent des commits, leurs hashes changent également, comme nous pouvons le voir dans l'illustration précédente.

Maintenant, si nous voulons que la branche « master » soit identique à « feature », il suffit de faire un fast-forward merge et nos deux branches auront le même historique de commits : c'est le rebase and merge. Dans l'illustration suivante, les commandes suivantes ont été effectuées :

git checkout master
git merge feature

 Immédiatement après, HEAD pointe sur « master » et les deux branches sont au même point.

Encore une fois, comme pour le squash and merge, nous pouvons nous demander pourquoi avoir recours à un rebase ? C'est parce que cette opération réécrit l’historique des commits pour qu’ils apparaissent comme s’ils avaient été créés après le dernier commit de la branche cible. Cela rend l’historique plus linéaire, plus facile à lire et plus détaillé que le squash and merge qui rassemble tous les commits de la branche secondaire en un seul.

Pour bien comprendre les différents types de fusion, il faut toujours se replacer dans le contexte d'une équipe qui comprend de nombreux contributeurs à un même dépôt partagé. L'évolution du développement doit demeurer claire et lisible pour tous, en particulier pour les responsables d'un projet. En général, des règles sur le type de merge à effectuer sont déjà établies d'avance pour que tous les participants s'y conforment. Il peut y avoir aussi des nomenclatures à respecter lors de la création de nouvelles branches et des types de messages spécifiques pour les commits.

# Récapitulatif des commandes vues dans ce chapitre

git rebase <nom_branche_cible>   Faire un "rebase" d'une branche sur une branche cible (va réappliquer les commits sur cette dernière)

# Dans ce type d'opération, il y a eu des commits sur les 2 branches
author avatar
Luc BRETON Administrateur système et cloud
Administrateur système et cloud avec une orientation DevOps pour une grande chaîne de pharmacies québécoise. Je suis plutôt généraliste avec une forte expérience côté virtualisation, stockage, cloud hybride et un intérêt particulier pour l'automatisation. J'aime le transfert de connaissances et il me fait plaisir d'être la première voix nord-américaine d'IT-Connect !
Partagez cet article Partager sur Twitter Partager sur Facebook Partager sur Linkedin Envoyer par mail

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.