Publié le 07/01/2020 Par Jason Marechal

Git rebase et git merge sont deux commandes utilisées pour synchroniser le travail entre deux branches. Il existe toutefois une certaine confusion entre les deux, notamment concernant leur usage. Comment fonctionne alors chacune de ces deux commandes, quels sont leurs effets et quelles sont utilités respectives ? C’est ce que nous allons voir dans cet article.

Git est un outil de gestion de version qui permet, dans un environnement collaboratif, de partager des versions de fichier avec les collaborateurs et de récupérer leurs versions. Dans cette optique collaborative, nous souhaitons donc synchroniser notre travail, c’est-à-dire mettre à jour notre version du travail par rapport à un état autre ou, à l’inverse, publier nos changements et mettre à jour la référence pour incorporer nos changements.

Une opération de mise à jour qui peut s’effectuer de diverses manières. Mais dans cet article, nous insisterons deux d’entre elles : git merge et git rebase.

Il semble exister dans l’usage une certaine confusion entre ces deux commandes ou au moins un manque de connaissance quant à ce que chacune d’elle fait. Or ces commandes n’ont pas la même utilité et, ne faisant pas la même chose, ne devraient pas être utilisées de manière interchangeable, sinon à quoi bon avoir deux commandes différentes ?

Nous allons donc voir ensemble ce qu’elles font réellement avant de décrire leurs cas d’usage spécifiques et les avantages qui y sont liés. En découlera alors une proposition de méthode de travail.

Mais tout d’abord, ces deux commandes consistant à mettre à jour une version de travail, la meilleure façon d’en visualiser le résultat est de consulter l’historique de travail. Analysons son utilité et son importance pour notre sujet.

L’historique Git : un outil pour vous aider

Un outil est là pour aider celui qui l’utilise, c’est-à-dire pour VOUS aider. Git, en tant que gestionnaire de version, est déjà un outil en soi, mais on peut aussi le considérer comme une boîte à outils, l’un d’entre eux étant l’historique des commits et des branches.

L’historique Git vous permet de suivre la chronologie des développements d’un produit, quand chaque fonctionnalité a été introduite dans le code, etc. Dans un environnement collaboratif, l’historique vous permet également de savoir ce qu’il s’est passé sur les autres branches -notamment master- pendant que vous développiez sur votre branche locale et, ainsi, d’en prévoir les conséquences. Enfin, un des avantages majeurs est qu’il peut être très simple de trouver le moment (commit) d’introduction d’une régression dans un produit.

En revanche, pour profiter de ces avantages, il faut un minimum de rigueur, et garder un historique clair et propre pour qu’il soit exploitable.

Les impacts majeurs sur l’historique surviennent lors de la fusion de branche : cette opération peut être réalisée via deux commandes, à savoir git merge et git rebase qui non seulement fonctionnent différemment mais qui, de plus, n’ont pas la même finalité.

Un peu de vocabulaire

Histoire de rester clair, il est nécessaire de définir les termes suivants :

  • Branche réceptrice : la branche qui reçoit le merge/rebase/etc. C’est celle depuis laquelle on invoque les commandes.
  • Branche topic : la branche qui contient les commit à intégrer dans la branche réceptrice.

Explication des commandes

Git merge : Fusionner des historiques

Extrait de la documentation : “git-merge – Join two or more development histories together”. On voit bien qu’il s’agit de joindre deux branches. Cette jointure se fait deux manières : Fast-forward ou par commit (de merge).

– Fast-forward

Si la branche topic (ici featureA) ne diffère de la branche réceptrice (ici master) que par l’ajout de nouveaux commits, alors on peut considérer qu’il existe un chemin linéaire entre ces deux branches.

Dans ce cas, le merge s’effectue simplement par déplacement du pointeur de branche. Dans notre cas, master pointe initialement sur « Initial commit ». Après le merge, elle pointera sur « Step 4 ».

$ git merge feature/featureA 
Updating 3ac0150..7ce15a7 
Fast-forward 

On remarque qu’à la simple lecture du graphe, il n’est plus possible de distinguer les commits qui proviennent de feature/featureA de ceux de master.

– Commit de merge

Si on le demande expressément ou si master a divergé de topic, c’est-à-dire qu’il contient des commits absents de topic, la jointure est réalisée par l’introduction d’un commit qui aura deux ancêtres, un sur chaque branche à joindre.

Par exemple, en demandant expressément cette opération :

git merge feature/featureA --no-ff 
Already up to date! 
Merge made by the 'recursive' strategy. 

Exemple lorsqu’il y a divergence :

git merge feature/featureA 
Already up to date! 
Merge made by the 'recursive' strategy.  

On constate dans les deux cas l’apparition d’un commit décrivant le merge. Si des conflits étaient apparus sur des fichiers, ce commit contiendrait leur résolution. Contrairement au premier cas, il est ici très simplement possible de comprendre l’historique des différents commits.

– Git pull

Pourquoi parler de git pull dans une section sur merge ? Parce que git pull n’est ni plus ni moins qu’un raccourci pour git fetch, suivi de git merge avec les mêmes comportements et options que ce que l’on a déjà décrit précédemment.

Git rebase : Réécrire la base historique

L’opération de rebase consiste à réécrire l’historique de la branche réceptrice pour correspondre à celui de la branche topic plus tout nouveau commit de la branche réceptrice. Cette opération est réalisée en trois temps :

  • Retirer les commits de la branche réceptrice qui n’appartiennent pas à la branche topic (pas d’inquiétude, ils ne sont pas perdus).
  • Réinitialiser la branche réceptrice pour être identique à la branche topic.
  • Réappliquer les commits précédemment retirés.

Dans l’exemple suivant, nous pouvons voir que featureB a divergé de featureA par deux commits (Dev1 et Dev2) et est « en retard » de deux commits par rapport à featureA (Step6 et Step 7) :

Voyons ce qu’il se passe pas à pas si nous voulons mettre à jour featureB par rapport à featureA :

      1. 1- Les nouveaux commits de featureB sont retirés :

      1. 2- featureB est réinitialisée à l’état de featureA :

      1. 3- Les commit précédemment retirés de featureB sont réappliqués :

On constate que featureB intègre bien tous les commits de featureA. En revanche, il est important de noter que l’historique de featureB a été réécrit. En effet, Dev1 avait pour parent Step4 avant l’opération mais a pour parent Step7 après l’opération.

– Git pull –rebase

Encore lui, cette fois avec l’argument — rebase. Dans ce cas, git pull n’effectue pas un merge mais un rebase, aussi simplement que cela.

Les usages

Il n’existe pas de façon absolue de travailler sur un repos git, ni avec les outils merge et rebase. En effet, un outil est là pour servir l’utilisateur, pas pour le contraindre. Il convient donc de choisir une stratégie qui correspond le mieux à la situation. En revanche, il existe certainement des tendances dans l’industrie, voir des conventions. L’usage que je vais décrire est l’usage couramment admis.

En cours de développement, nous pouvons définir deux cas principaux :

  • Une branche partagée qui doit recevoir les changements d’une branche de développement.
    Exemple : intégrer une fonctionnalité développée sur une branche topic dans la branche partagée master.
  • Une branche de développement/topic qui doit être mise à jour par rapport à une branche partagée pour en suivre les évolutions.
    Exemple : mettre à jour sa branche de développement/topic local par rapport à la branche partagée Master pour accueillir les derniers développements livrés par des collaborateurs.

Dans les deux cas, on peut résumer la situation par « mettre à jour le contenu d’une branche par rapport à une autre ». En revanche, ces deux situations présentent des contraintes et des objectifs différents.

Mise à jour d’une branche partagée : utiliser merge

– Préserver l’historique partagé

Une branche partagée ne doit pas voir son historique modifié. En effet, si un utilisateur modifie l’historique d’une branche et le (force) pousse, tous les collaborateurs qui travaillent à partir de cette branche vont voir leur travail perturbé par cette modification d’historique. Il faut donc préserver l’historique et le plus adapté pour y parvenir reste l’utilisation de merge.

– Préserver l’historique de travail

Comme nous l’avons vu plus tôt, merge a créé un nouveau commit qui possède deux parents. À ce titre, nous avons vu qu’il était facile de lire le travail accompli dans une branche topic mergée dans une branche partagée. Tous cela a plusieurs utilités.

Tout d’abord, le commit de merge lui-même délimite clairement sur la branche partagée les diverses livraisons de fonctionnalité. En cas de régression, il est donc facile d’analyser quelle fonctionnalité est à l’origine du problème.

D’autre part, la conservation de l’historique de travail de la branche de topic permet, quant à elle, de comprendre les étapes du développement d’un topic. De la même manière, si une régression est identifiée dans un topic mergé, il est possible d’identifier précisément le commit qui introduit cette régression plutôt que d’avoir à analyser toutes les modifications d’un topic.

Mise à jour d’une branche de topic : utiliser rebase

Une branche de topic ne doit contenir que le sujet en cours de travail

Nous avons plusieurs fois vu jusqu’à présent que la clarté est un but que l’on doit s’efforcer d’atteindre. Ainsi, la plus grande clarté qu’une branche de travail, topic, puisse avoir, c’est de ne différer d’une branche partagée que par des commits liés au sujet de travail et excluant même d’éventuels commits de merge. En fait, si l’on part du principe qu’une branche topic sera mergée, alors sa mise à jour via des merges nuira aussi à la lisibilité de la branche partagée. En effet, tous les commits de merge présents sur la branche topic continueront d’exister après le merge. Voici un exemple où featureB a été mise à jour par rapport à featureA, puis l’on a continué à travailler dessus.

Cependant, featureA a elle aussi reçu de nouveaux développements après Step7. Ici, ce sont de simples commits, mais étant une branche partagée, on peut imaginer que tous ces commits pourraient en fait être des commits de merge issus d’autres topics que featureB.

Lorsque l’on merge B dans A, on obtient alors ce résultat :

Contrairement à l’exemple donné lors des explications sur merge, on voit ici que l’historique est moins clair. Voyons ce que l’on obtient en effectuant un rebase sur featureB à la place d’un merge :

(feature/featureB) 
$ git rebase feature/featureA 
First, rewinding head to replay your work on top of it... 
Fast-forwarded feature/featureB to feature/featureA. 

Et on merge le résultat dans featureA :

On retombe dans le cas décrit dans la partie merge avec un historique plus simple.

– Les commits de merge dans une branche topic sont-ils nécessaires ?

Outre cette question de simplicité d’historique, il faut aussi réfléchir à cette question : est-ce que les commits de merge dans featureB apportaient de l’information ? À part connaître la fréquence à laquelle un développeur se met à jour par rapport au reste, pas grand-chose.

Si dans le cas d’une branche partagée, les commits de merge permettent d’identifier rapidement quel topic a introduit une régression, ces commits n’apportent aucune information lorsqu’ils apparaissent sur une branche de topic. Ils ne font ainsi que générer du bruit et donc de la complexité inutile.

Cherry Pick

Je me permets un petit aparté sur le cherry-picking. Cette opération consiste à intégrer dans une branche courante un commit d’une autre branche. Cette opération est particulièrement utile lorsqu’un fix, une config, une feature… intéressante apparaît sur une branche et que vous ne souhaitez pas intégrer toute la branche dans votre branche courante (avec un merge ou un rebase). Ce cas de figure peut arriver lorsqu’un tel commit se trouve sur une branche « instable », une branche en cours de développement dont l’historique – et même le contenu – peut changer à l’avenir ou lorsque vous travaillez vous-même sur une telle branche et que vous n’êtes pas prêt à recevoir toutes les nouveautés d’une branche partagée. Dans cette situation, vous souhaitez prendre un unique commit précis. Cherry-pick sert à cela.

Proposition de méthode de travail

Il ne s’agit que d’une proposition mais comme dit précédemment, d’une méthode largement utilisée dans l’industrie.

  • Mettre à jour une branche topic, une branche de travail local : utilisez rebase.
    • Évite l’introduction de bruit.
    • Conserve un historique linéaire pour le topic.
  • Mettre à jour une branche partagée : utilisez merge sans fast-forward.
    • Tous les merges sont représentés par un commit.
    • Isolation simple des régressions.

Si vous respectez ces consignes, vous n’aurez aucun mal à conserver des historiques de travail propres pour vos collaborateurs et, ainsi, à faciliter le travail de tout le monde.

Pour aller plus loin

Comme nous l’avons déjà dit, les propositions de travail ci-dessus ne sont qu’un exemple et il existe d’autres pratiques. On peut citer le rebase interactif (rebase -i) qui permet de réordonner ou de fusionner des commits ou le merge squash (merge –squash) qui fusionne les commits d’une branche topic avant de les appliquer en un commit sur une branche partagée. Nous ne rentrerons pas en détails dans ces pratiques car elles nécessitent chacune un article dédié pour être expliquée correctement.

Références :

Pas encore de commentaires

Publier un commentaire

photo de Jason Marechal

Auteur

Jason Marechal