Git
Chapters ▾ 2nd Edition

3.6 Les branches avec Git - Rebaser (Rebasing)

Rebaser (Rebasing)

Dans Git, il y a deux façons d’intégrer les modifications d’une branche dans une autre : en fusionnant (merge) et en rebasant (rebase). Dans ce chapitre, vous apprendrez la signification de rebaser, comment le faire, pourquoi c’est un outil incroyable et dans quels cas il est déconseillé de l’utiliser.

Les bases

Si vous revenez à un exemple précédent du chapitre Fusions (Merges), vous remarquerez que votre travail a divergé et que vous avez ajouté des commits sur deux branches différentes.

Historique divergeant simple.
Figure 35. Historique divergeant simple

Comme nous l’avons déjà expliqué, le moyen le plus simple pour intégrer ces branches est la fusion via la commande merge. Cette commande réalise une fusion à trois branches entre les deux derniers instantanés (snapshots) de chaque branche (C3 et C4) et l’ancêtre commun le plus récent (C2), créant un nouvel instantané (et un commit).

Fusion pour intégrer des travaux aux historiques divergeants.
Figure 36. Fusion pour intégrer des travaux aux historiques divergeants

Cependant, il existe un autre moyen : vous pouvez prendre le patch de la modification introduite en C4 et le réappliquer sur C3. Dans Git, cette action est appelée "rebaser" (rebasing). Avec la commande rebase, vous pouvez prendre toutes les modifications qui ont été validées sur une branche et les rejouer sur une autre.

Dans cet exemple, vous lanceriez les commandes suivantes :

$ git checkout experience
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

Cela fonctionne en cherchant l’ancêtre commun le plus récent des deux branches (celle sur laquelle vous vous trouvez et celle sur laquelle vous rebasez), en récupérant toutes les différences introduites par chaque commit de la branche courante, en les sauvant dans des fichiers temporaires, en réinitialisant la branche courante sur le même commit que la branche de destination et en appliquant finalement chaque modification dans le même ordre.

Rebasage des modifications introduites par `C4` sur `C3`.
Figure 37. Rebasage des modifications introduites par C4 sur C3

À ce moment, vous pouvez retourner sur la branche master et réaliser une fusion en avance rapide (fast-forward merge).

Avance rapide de la branche `master`.
Figure 38. Avance rapide de la branche master

À présent, l’instantané pointé par C4' est exactement le même que celui pointé par C5 dans l’exemple de fusion. Il n’y a pas de différence entre les résultats des deux types d’intégration, mais rebaser rend l’historique plus clair. Si vous examinez le journal de la branche rebasée, elle est devenue linéaire : toutes les modifications apparaissent en série même si elles ont eu lieu en parallèle.

Vous aurez souvent à faire cela pour vous assurer que vos commits s’appliquent proprement sur une branche distante — par exemple, sur un projet où vous souhaitez contribuer mais que vous ne maintenez pas. Dans ce cas, vous réaliseriez votre travail dans une branche puis vous rebaseriez votre travail sur origin/master quand vous êtes prêt à soumettre vos patchs au projet principal. De cette manière, le mainteneur n’a pas à réaliser de travail d’intégration — juste une avance rapide ou simplement une application propre.

Il faut noter que l’instantané pointé par le commit final, qu’il soit le dernier des commits d’une opération de rebasage ou le commit final issu d’une fusion, sont en fait le même instantané — c’est juste que l’historique est différent. Rebaser rejoue les modifications d’une ligne de commits sur une autre dans l’ordre d’apparition, alors que la fusion joint et fusionne les deux têtes.

Rebases plus intéressants

Vous pouvez aussi faire rejouer votre rebasage sur autre chose qu’une branche. Prenez un historique tel que Un historique avec deux branches thématiques qui sortent l’une de l’autre par exemple. Vous avez créé une branche thématique (serveur) pour ajouter des fonctionnalités côté serveur à votre projet et avez réalisé un commit. Ensuite, vous avez créé une branche pour ajouter des modifications côté client (client) et avez validé plusieurs fois. Finalement, vous avez rebasculé sur la branche serveur et avez réalisé quelques commits supplémentaires.

Un historique avec deux branches thématiques qui sortent l'une de l'autre.
Figure 39. Un historique avec deux branches thématiques qui sortent l’une de l’autre

Supposons que vous décidez que vous souhaitez fusionner vos modifications du côté client dans votre ligne principale pour une publication (release) mais vous souhaitez retenir les modifications de la partie serveur jusqu’à ce qu’elles soient un peu mieux testées. Vous pouvez récupérer les modifications du côté client qui ne sont pas sur le serveur (C8 et C9) et les rejouer sur la branche master en utilisant l’option --onto de git rebase :

$ git rebase --onto master serveur client

Cela signifie en substance "Extraire la branche client, déterminer les patchs depuis l’ancêtre commun des branches client et serveur puis les rejouer sur master ". C’est assez complexe, mais le résultat est assez impressionnant.

Rebaser deux branches thématiques l'une sur l'autre.
Figure 40. Rebaser deux branches thématiques l’une sur l’autre

Maintenant, vous pouvez faire une avance rapide sur votre branche master (cf. Avance rapide sur votre branche master pour inclure les modifications de la branche client):

$ git checkout master
$ git merge client
Avance rapide sur votre branche `master` pour inclure les modifications de la branche client.
Figure 41. Avance rapide sur votre branche master pour inclure les modifications de la branche client

Supposons que vous décidiez de tirer (pull) votre branche serveur aussi. Vous pouvez rebaser la branche serveur sur la branche master sans avoir à l’extraire avant en utilisant git rebase [branchedebase] [branchethematique] — qui extrait la branche thématique (dans notre cas, serveur) pour vous et la rejoue sur la branche de base (master) :

$ git rebase master serveur

Cette commande rejoue les modifications de serveur sur le sommet de la branche master, comme indiqué dans Rebasage de la branche serveur sur le sommet de la branche master..

Rebasage de la branche serveur sur le sommet de la branche `master`.
Figure 42. Rebasage de la branche serveur sur le sommet de la branche master.

Vous pouvez ensuite faire une avance rapide sur la branche de base (master) :

$ git checkout master
$ git merge serveur

Vous pouvez effacer les branches client et serveur une fois que tout le travail est intégré et que vous n’en avez plus besoin, éliminant tout l’historique de ce processus, comme visible sur Historique final des commits :

$ git branch -d client
$ git branch -d serveur
Historique final des _commits_.
Figure 43. Historique final des commits

Les dangers du rebasage

Ah… mais les joies de rebaser ne viennent pas sans leurs contreparties, qui peuvent être résumées en une ligne :

Ne rebasez jamais des commits qui ont déjà été poussés sur un dépôt public.

Si vous suivez ce conseil, tout ira bien. Sinon, de nombreuses personnes vont vous haïr et vous serez méprisé par vos amis et votre famille.

Quand vous rebasez des données, vous abandonnez les commits existants et vous en créez de nouveaux qui sont similaires mais différents. Si vous poussez des commits quelque part, que d’autres les tirent et se basent dessus pour travailler, et qu’après coup, vous réécrivez ces commits à l’aide de git rebase et les poussez à nouveau, vos collaborateurs devront re-fusionner leur travail et les choses peuvent rapidement devenir très désordonnées quand vous essaierez de tirer leur travail dans votre dépôt.

Examinons un exemple expliquant comment rebaser un travail déjà publié sur un dépôt public peut générer des gros problèmes. Supposons que vous clonez un dépôt depuis un serveur central et réalisez quelques travaux dessus. Votre historique de commits ressemble à ceci :

Cloner un dépôt et baser du travail dessus.
Figure 44. Cloner un dépôt et baser du travail dessus

À présent, une autre personne travaille et inclut une fusion, puis elle pousse ce travail sur le serveur central. Vous le récupérez et vous fusionnez la nouvelle branche distante dans votre copie, ce qui donne l’historique suivant :

Récupération de _commits_ et fusion dans votre copie.
Figure 45. Récupération de commits et fusion dans votre copie

Ensuite, la personne qui a poussé le travail que vous venez de fusionner décide de faire marche arrière et de rebaser son travail. Elle lance un git push --force pour forcer l’écrasement de l’historique sur le serveur. Vous récupérez alors les données du serveur, qui vous amènent les nouveaux commits.

Quelqu'un pousse des _commits_ rebasés, en abandonnant les _commits_ sur lesquels vous avez fondé votre travail.
Figure 46. Quelqu’un pousse des commits rebasés, en abandonnant les commits sur lesquels vous avez fondé votre travail

Vous êtes désormais tous les deux dans le pétrin. Si vous faites un git pull, vous allez créer un commit de fusion incluant les deux historiques et votre dépôt ressemblera à ça :

Vous fusionnez le même travail une nouvelle fois dans un nouveau _commit_ de fusion.
Figure 47. Vous fusionnez le même travail une nouvelle fois dans un nouveau commit de fusion

Si vous lancez git log lorsque votre historique ressemble à ceci, vous verrez deux commits qui ont la même date d’auteur et les mêmes messages, ce qui est déroutant. De plus, si vous poussez cet historique sur le serveur, vous réintroduirez tous ces commits rebasés sur le serveur central, ce qui va encore plus dérouter les autres développeurs. C’est plutôt logique de présumer que l’autre développeur ne souhaite pas voir apparaître C4 et C6 dans l’historique. C’est la raison pour laquelle il avait effectué un rebasage initialement.

Rebaser quand vous rebasez

Si vous vous retrouvez effectivement dans une situation telle que celle-ci, Git dispose d’autres fonctions magiques qui peuvent vous aider. Si quelqu’un de votre équipe pousse de force des changements qui écrasent des travaux sur lesquels vous vous êtes basés, votre challenge est de déterminer ce qui est à vous et ce qui a été réécrit.

Il se trouve qu’en plus de l’empreinte SHA du commit, Git calcule aussi une empreinte qui est uniquement basée sur le patch introduit avec le commit. Ceci est appelé un "identifiant de patch" (patch-id).

Si vous tirez des travaux qui ont été réécrits et les rebasez au-dessus des nouveaux commits de votre collègue, Git peut souvent déterminer ceux qui sont uniquement les vôtres et les réappliquer au sommet de votre nouvelle branche.

Par exemple, dans le scénario précédent, si au lieu de fusionner quand nous étions à l’étape Quelqu’un pousse des commits rebasés, en abandonnant les commits sur lesquels vous avez fondé votre travail nous exécutons la commande git rebase equipe1/master, Git va :

  • Déterminer quels travaux sont uniques à notre branche (C2, C3, C4, C6, C7)

  • Déterminer ceux qui ne sont pas des commits de fusion (C2, C3, C4)

  • Déterminer ceux qui n’ont pas été réécrits dans la branche de destination (uniquement C2 et C3 puisque C4 est le même patch que C4')

  • Appliquer ces commits au sommet de equipe1/master

Ainsi, au lieu du résultat que nous avons observé au chapitre Vous fusionnez le même travail une nouvelle fois dans un nouveau commit de fusion, nous aurions pu finir avec quelque chose qui ressemblerait davantage à Rebaser au-dessus de travaux rebasés puis que l’on a poussé en forçant..

Rebaser au-dessus de travaux rebasés puis que l'on a poussé en forçant.
Figure 48. Rebaser au-dessus de travaux rebasés puis que l’on a poussé en forçant.

Cela fonctionne seulement si les commits C4 et C4' de votre collègue correspondent presque exactement aux mêmes modifications. Autrement, le rebasage ne sera pas capable de déterminer qu’il s’agit d’un doublon et va ajouter un autre patch similaire à C4 (ce qui échouera probablement puisque les changements sont au moins partiellement déjà présents).

Vous pouvez également simplifier tout cela en lançant un git pull --rebase au lieu d’un git pull normal. Vous pouvez encore le faire manuellement à l’aide d’un git fetch suivi d’un git rebase team1/master dans le cas présent.

Si vous utilisez git pull et voulez faire de --rebase le traitement par défaut, vous pouvez changer la valeur du paramètre de configuration pull.rebase par git config --global pull.rebase true.

Si vous considérez le fait de rebaser comme un moyen de nettoyer et réarranger des commits avant de les pousser et si vous vous en tenez à ne rebaser que des commits qui n’ont jamais été publiés, tout ira bien. Si vous tentez de rebaser des commits déjà publiés sur lesquels les gens ont déjà basé leur travail, vous allez au devant de gros problèmes et votre équipe vous en tiendra rigueur.

Si vous ou l’un de vos collègues y trouve cependant une quelconque nécessité, assurez-vous que tout le monde sache lancer un git pull --rebase pour essayer de rendre les choses un peu plus faciles.

Rebaser ou Fusionner

Maintenant que vous avez vu concrètement ce que signifient rebaser et fusionner, vous devez vous demander ce qu’il est préférable d’utiliser. Avant de pouvoir répondre à cela, revenons quelque peu en arrière et parlons un peu de ce que signifie un historique.

On peut voir l’historique des commits de votre dépôt comme un enregistrement de ce qu’il s’est réellement passé. Il s’agit d’un document historique qui a une valeur en tant que tel et ne doit pas être altéré. Sous cet angle, modifier l’historique des commits est presque blasphématoire puisque vous mentez sur ce qu’il s’est réellement passé. Dans ce cas, que faire dans le cas d’une série de commits de fusions désordonnés ? Cela reflète ce qu’il s’est passé et le dépôt devrait le conserver pour la postérité.

Le point de vue inverse consiste à considérer que l’historique des commits est le reflet de la façon dont votre projet a été construit. Vous ne publieriez jamais le premier brouillon d’un livre et le manuel de maintenance de votre projet mérite une révision attentive. Ceci constitue le camp de ceux qui utilisent des outils tels que le rebasage et les branches filtrées pour raconter une histoire de la meilleure des manières pour les futurs lecteurs.

Désormais, nous espérons que vous comprenez qu’il n’est pas si simple de répondre à la question portant sur le meilleur outil entre fusion et rebasage. Git est un outil puissant et vous permet beaucoup de manipulations sur et avec votre historique mais chaque équipe et chaque projet sont différents. Maintenant que vous savez comment fonctionnent ces deux outils, c’est à vous de décider lequel correspond le mieux à votre situation en particulier.

De manière générale, la manière de profiter au mieux des deux mondes consiste à rebaser des modifications locales que vous avez effectuées mais qui n’ont pas encore été partagées avant de les pousser de manière à obtenir un historique propre mais sans jamais rebaser quoi que ce soit que vous ayez déjà poussé quelque part.