git rebase est souvent perçue comme une commande redoutable que les débutants doivent éviter à tout prix. Cependant, si elle est utilisée à bon escient, elle peut véritablement faciliter la vie des développeurs. Dans cet article, nous comparerons la commande git rebase à la commande git merge qui lui est associée, et nous identifierons toutes les possibilités d'intégration du rebase à un workflow Git classique.

Présentation des concepts

La première chose à savoir sur la commande git rebase est qu'elle poursuit le même objectif que git merge. Ces deux commandes permettent d'intégrer des changements d'une branche dans une autre. Seule la manière de procéder diffère.

Songez à ce qu'il se produit si vous commencez à travailler sur une nouvelle fonctionnalité dans une branche dédiée, puis que l'un des membres de l'équipe met à jour la branche master avec de nouveaux commits. Résultat ? Vous obtenez un historique forké, un élément bien connu de tout développeur ayant déjà utilisé l'outil de collaboration Git.

A forked commit history

À présent, imaginez que les nouveaux commits de la branche master soient pertinents pour la fonctionnalité sur laquelle vous travaillez. Deux méthodes s'offrent à vous pour ajouter les nouveaux commits dans votre branche feature : le merge ou le rebase.

L'option Merge

La solution la plus simple consiste à faire un merge de la branche master dans la branche de fonctionnalité en procédant de la manière suivante :

git checkout feature
git merge master

Vous pouvez également condenser cette commande en une ligne :

git merge feature master

Un nouveau « commit de merge » est créé dans la branche feature. Il relie les historiques des deux branches. La structure de branche est alors redéfinie comme suit :

Merging master into the feature branch

Le merge est une opération intéressante, car elle est non destructive. Les branches existantes ne sont aucunement altérées. Cela permet d'éviter les pièges potentiels du rebase (abordés ci-dessous).

Par ailleurs, cela signifie également qu'un commit de merge extérieur sera généré dans la branche feature dès que vous intégrez des changements en amont. Une branche master très active peut grandement contribuer à polluer l'historique de votre branche de fonctionnalité. Si vous tentez d'atténuer ce problème à l'aide des options git log avancées, les autres développeurs auront des difficultés pour suivre l'historique du projet.

L'option Rebase

Plutôt que de faire un merge, vous pouvez faire un rebase de la branche feature sur la branche master en exécutant les commandes suivantes :

git checkout feature
git rebase master

Toute la branche feature sera ainsi déplacée sur la pointe de la branche master, et tous les nouveaux commits seront intégrés à master. Cependant, au lieu d'utiliser un commit de merge, le rebase consiste à réécrire l'historique du projet en créant de nouveaux commits pour chaque commit de la branche d'origine.

Rebasing the feature branch onto master

Le principal avantage du rebase est que l'historique de votre projet sera nettement plus propre. Premièrement, cette opération permet de supprimer les commits de merge superflus requis par la commande git merge. Deuxièmement, comme l'illustre le schéma ci-dessus, vous obtenez un historique de projet parfaitement linéaire, qui vous permettra de suivre la pointe de la branche feature à toutes les étapes du projet à partir de son commencement, sans fork. Ainsi, vous pourrez naviguer dans votre projet plus librement grâce à des commandes comme git log, git bisect et gitk.

Mais deux caractéristiques sont essentielles pour cet historique de commit épuré : la sécurité et la traçabilité. Si vous ne suivez pas la règle d'or du rebase, la réécriture de l'historique de projet peut potentiellement se révéler catastrophique pour votre workflow de collaboration. En outre et dans une moindre mesure, le rebase perd le contexte fourni par un commit de merge : vous ne pouvez pas voir à quel moment des changements apportés au dépôt upstream ont été intégrés à la branche de fonctionnalité.

Rebase interactif

Le rebase interactif vous donne la possibilité de modifier des commits lorsqu'ils sont déplacés vers la nouvelle branche. Cette opération est plus efficace qu'un rebase automatisé, puisqu'elle permet de contrôler l'intégralité de l'historique des commits de la branche. Le plus souvent, le rebase interactif est utilisé pour nettoyer un historique désordonné avant de merger branche de fonctionnalité dans master.

Pour lancer une session de rebase interactif, ajoutez l'option i à la commande git rebase :

git checkout feature
git rebase -i master

Cette commande ouvre un éditeur de texte répertoriant tous les commits qui seront déplacés :

pick 33d5b7a Message de commit n° 1
pick 9480b3d Message de commit n° 2
pick 5c67e61 Message de commit n° 3

Cette liste détermine précisément quelle sera la structure de la branche après le rebase. En modifiant la commande pick et/ou en réorganisant les entrées, vous pouvez agencer l'historique de la branche comme bon vous semble. Par exemple, si le deuxième commit résout un petit problème dans le premier, vous pouvez les merger en un seul grâce à la commande fixup :

pick 33d5b7a Message de commit n° 1
fixup 9480b3d Message de commit n° 2
pick 5c67e61 Message de commit n° 3

Lorsque vous enregistrez et fermez le fichier, Git effectue le rebase conformément à vos instructions, et vous obtenez un historique de projet semblable au suivant :

Squashing a commit with an interactive rebase

En supprimant les commits superflus de cette manière, l'historique de votre fonctionnalité sera nettement plus lisible. La commande git merge ne vous permet pas d'obtenir ce résultat.

Règle d'or du rebase

Une fois que vous avez compris en quoi consiste le rebase, vous devez avant tout savoir dans quels cas ne pas l'utiliser. N'utilisez jamais la commande git rebase sur les branches publiques. C'est la règle d'or !

Par exemple, imaginez ce qu'il se produirait si vous effectuiez un rebase de master sur votre branche feature :

Rebasing the master branch

Le rebase déplace tous les commits de master sur la pointe de feature. Le problème, c'est que le changement n'a lieu que dans votre dépôt. Tous les autres développeurs travaillent toujours avec la branche master d'origine. Comme le rebase génère de nouveaux commits, Git va penser que l'historique de votre branche master diffère des autres.

La seule manière de synchroniser les deux branches master consiste à les merger de nouveau, ce qui générera un commit de merge supplémentaire et deux ensembles de commits contenant les mêmes changements (les commits d'origine et ceux issus de votre branche rebasée). De quoi compliquer les choses, non ?

Ainsi, avant d'exécuter la commande git rebase, pensez à toujours vérifier si quelqu'un d'autre travaille sur la branche concernée. Si oui, ôtez tout de suite vos mains du clavier et réfléchissez à une manière moins radicale d'effectuer vos changements (p. ex., en utilisant la commande git revert). Dans le cas contraire, vous pouvez réécrire l'historique à votre guise.

Force-push

Si vous tentez de faire un push de la branche master rebasée vers un dépôt distant, Git vous en empêchera, car elle entre en conflit avec la branche master distante. Vous pouvez toutefois forcer ce push en lançant le flag --force comme suit :

# Soyez très prudent avec cette commande !
git push --force

La branche master distante sera remplacée de manière à correspondre à la branche rebasée issue de votre dépôt. Résultat : vos collègues auront peine à s'y retrouver ! Veillez donc à utiliser cette commande uniquement quand vous êtes sûr de ce que vous faites.

Vous devriez uniquement forcer un push lorsque vous effectuez un nettoyage local après avoir fait un push d'une branche de fonctionnalité privée vers un dépôt distant (p. ex., à des fins de sauvegarde). C'est comme dire : « Oups, je ne voulais pas vraiment faire un push de cette version d'origine de la branche de fonctionnalité. Prenez plutôt la branche actuelle. » À nouveau, il est important que personne ne travaille sur les commits de la version d'origine de la branche de fonctionnalité.

Description du workflow

Le rebase peut être intégré à votre workflow Git existant dans la mesure qui convient à votre équipe. Dans cette section, nous examinerons les avantages du rebase aux différentes étapes du développement d'une fonctionnalité.

Dans tout workflow qui utilise la commande git rebase, la première chose à faire est de créer une branche pour chaque fonctionnalité. Ainsi, vous obtiendrez la structure adéquate pour une utilisation optimale du rebase :

Developing a feature in a dedicated branch

Nettoyage local

L'un des meilleurs moyens d'intégrer le rebase à votre workflow consiste à nettoyer les fonctionnalités locales en cours. En réalisant périodiquement un rebase interactif, vous pouvez vous assurer que chaque commit de votre fonctionnalité est ciblé et sensé. Cela vous permet d'écrire votre code sans avoir à vous préoccuper de le séparer en commits isolés : vous pouvez par la suite le réparer.

Lorsque vous appelez la commande git rebase, vous avez le choix entre deux bases : la branche parente de la fonctionnalité (p. ex., master) ou un commit précédent de votre fonctionnalité. Nous avons examiné un exemple de la première option dans la section Rebase interactif. Privilégiez cette solution si vous devez seulement réparer les quelques commits les plus récents. Par exemple, la commande suivante lance un rebase interactif uniquement pour les trois derniers commits.

git checkout feature
git rebase -i HEAD~3

En définissant HEAD~3 comme la nouvelle base, vous ne déplacez pas la branche : vous réécrivez les trois commits qui la suivent de manière interactive. Remarque : cette solution ne vous permet pas d'intégrer les changements en amont à la branche feature.

Rebasing onto Head~3

Si vous souhaitez réécrire toute la fonctionnalité dans son ensemble en utilisant cette méthode, la commande git merge-base vous sera utile pour retrouver la base d'origine de la branche feature. La commande suivante vous permet de récupérer l'ID de commit de la base d'origine, puis de le transmettre à git rebase :

git merge-base feature master

Cette utilisation du rebase interactif est une excellente manière d'intégrer git rebase à votre workflow, puisque cette opération concerne uniquement les branches locales. La seule chose que les autres développeurs verront sera votre résultat final, c'est-à-dire un historique de branche de fonctionnalité propre et clair.

Mais à nouveau, cela ne fonctionne que pour les branches de fonctionnalité privées. Si vous collaborez avec d'autres développeurs dans la même branche de fonctionnalité, cette branche est publique, et vous n'êtes pas autorisé à réécrire son historique.

En cas de rebase interactif, git merge est la seule commande qui vous permettra de nettoyer les commits locaux.

Incorporer les changements en amont dans une fonctionnalité

Dans la section Présentation des concepts, nous avons vu comment une branche de fonctionnalité peut intégrer des changements en amont issus de master grâce à la commande git merge ou git rebase. Le merge est une solution sûre, qui préserve tout l'historique de votre dépôt, tandis que le rebase génère un historique linéaire en déplaçant votre branche de fonctionnalité sur la pointe de la branche master.

Cette utilisation de la commande git rebase s'apparente à un nettoyage en local (qui peut être réalisé simultanément), à cela près que les commits en amont sont intégrés à partir de master.

Gardez à l'esprit que rien ne vous interdit d'effectuer un rebase sur une branche distante plutôt que sur master. Cette méthode vous sera utile si vous collaborez sur une même fonctionnalité avec un autre développeur et si vous souhaitez intégrer ses changements à votre dépôt.

Par exemple, si votre collègue Jean et vous avez ajouté des commits dans la branche feature, votre dépôt présentera la structure suivante une fois que vous aurez fait un fetch de la branche feature dans le dépôt de Jean :

Collaborating on the same feature branch

Vous pouvez résoudre ce fork exactement de la même manière que lorsque vous intégrez des changements en amont à partir de master : soit vous mergez votre branche feature locale avec john/feature, soit vous rebasez cette branche feature locale sur la pointe de john/feature.

Merging vs. rebasing onto a remote branch

Remarque : cette opération ne viole en rien la règle d'or du rebase, puisque seuls vos commits locaux sur feature sont déplacés. C'est un peu comme si vous souhaitiez intégrer vos changements à ce que Jean a déjà réalisé. Dans la plupart des cas, cette opération sera plus intuitive qu'une synchronisation avec la branche distante au moyen d'un commit de merge.

Par défaut, la commande git pull fait un merge, mais vous pouvez la forcer à intégrer la branche distante à un rebase en y ajoutant l'option --rebase.

Revoir une fonctionnalité avec une pull request

Si vous faites des pull requests dans le cadre de la revue de code, évitez d'utiliser git rebase après avoir créé une pull request. Dès que vous aurez fait la pull request, les autres développeurs pourront examiner vos commits. En d'autres termes, cette branche sera publique. Si vous réécrivez son historique, Git et vos collègues n'auront plus la possibilité d'examiner les commits de suivi ajoutés à la fonctionnalité.

Tout changement apporté par les autres développeurs devra être intégré avec git merge et non git rebase.

Pour cette raison, il est généralement judicieux de nettoyer votre code avec un rebase interactif avant de soumettre votre pull request.

Intégration d'une fonctionnalité approuvée

Une fois qu'une fonctionnalité a été approuvée par votre équipe, vous pouvez la rebaser sur la pointe de la branche master avant d'exécuter git merge pour intégrer la fonctionnalité à la base de code principale.

La situation est la même que lorsque vous intégrez des changements en amont à une branche de fonctionnalité. Cependant, comme vous n'êtes pas autorisé à réécrire des commits dans la branche master, vous devrez utiliser git merge pour intégrer la fonctionnalité. En réalisant un rebase avant un merge, vous effectuerez à coup sûr un fast-forward merge, et votre historique sera parfaitement linéaire. Par ailleurs, cette opération vous permet d'écraser tout commit de suivi ajouté lors d'une pull request.

Integrating a feature into master with and without a rebase

Si vous n'êtes pas tout à fait à l'aise avec la commande git rebase, vous pouvez toujours réaliser le rebase dans une branche temporaire. Ainsi, si vous semez le chaos sans le vouloir dans l'historique de votre fonctionnalité, il vous suffira de récupérer la branche d'origine pour réessayer. Par exemple :

git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Nettoyage de l'historique]
git checkout master
git merge temporary-branch

Résumé

À présent, vous savez tout ce qu'il faut savoir sur le rebase des branches. Si vous souhaitez obtenir un historique propre, linéaire et sans commits de merge superflus, utilisez git rebase plutôt que git merge au moment d'intégrer vos changements à partir d'une autre branche.

Par ailleurs, si vous souhaitez conserver l'historique complet de votre projet sans devoir réécrire vos commits publics, optez pour git merge. Les deux solutions se valent, mais, au moins, vous pouvez maintenant profiter des avantages de git rebase.