Panier !

J'ai déjà clamé haut et fort à la face du monde mon amour pour Git. Git est beau, Git est grand, Git est puissant ! Gloire à Git ! Gloire à Git… Hum… D'ailleurs, vous voyez avec quelle facilité je me laisse emporter.

Pourtant, si j'en crois ce que je vois passer sur les réseaux sociaux, Git reste une sacrée source d'arrachage de cheveux pour les malheureux qui n'ont pas encore vu la lumière.

L'une de mes fonctionnalités préférée de Git est également l'une de celle qui provoque les réactions les plus virulentes : il s'agit de git rebase. Avec un rebase, on peut tout faire. Un historique pourri ? Git rebase et c'est reparti !

Pourtant, je le constate quand j'anime des formations sur la prise en main de Git, la commande rebase est souvent mal comprise et mal utilisée — du moins, avant la formation :). Et même ceux qui ont bien compris comment ça marche se demandent encore « à quoi ça sert ? ».

Alors parce que je me sens d'humeur particulièrement généreuse, je vais tenter de vous faire partager mon amour pour git rebase.

Prérequis

Ce billet présuppose que vous avez déjà une certaine connaissance des fondamentaux et du fonctionnement de Git, notamment sur le fonctionnement des branches. Si ce n'est pas votre cas, vous pouvez commencer par lire cet autre billet qui devrait vous mettre le Git à l'étrier.

Vous êtes également sensible aux bonnes pratiques en ce qui concerne la gestion de votre historique : commits atomiques, bonne gestion des branches, etc.

Git rebase, qu'est-ce que c'est ?

Imaginez un arbre. Un arbre a des branches (normal). Imaginez que vous teniez un sécateur entre vos mains. D'un geste plein de prestance, vous coupez une branche juste à l'endroit ou elle se sépare du tronc. Aussitôt, vous la re-greffez sur ce même tronc, mais à un emplacement différent (férent). Vous obtenez donc le même arbre, avec la même quantité totale de bois, mais vous avez simplement déplacé une branche.

Voilà, c'est tout simplement ça, git rebase. Sauf qu'au lieu de déplacer du bois, on déplace des listes de commits.

Git rebase avec un dessin

Ok, pour que ça soit plus clair, je vais vous faire un dessin. Je vais partir du cas classique ou, depuis ma branche de développement principale (master), j'ai créé une branche discussion, parce que je dois implémenter un système de discussion (scussion) quelconque.

Voici à quoi peut ressembler l'historique en question, d'un point de vue schématique (visible très facilement grâce à git lg).

A---B---C---D ← master
     \
      E---F---G ← discussion

Si je réalise une fusion classique au moyen de la commande git merge, j'obtiendrai le nouvel historique suivant :

A---B---C---D---H ← master
     \         /
      E---F---G ← discussion

Une autre solution est de transplanter ma branche discussion sur la pointe de master. Je prends la branche et je la recolle plus loin. On obtient alors :

A---B---C---D ← master
             \
              E---F---G ← discussion

Notez que rebaser discussion n'a strictement aucun effet sur master. En revanche, la fusion de discussion dans master est maintenant triviale (fast forward). On obtient enfin :

A---B---C---D---E---F---G ← master
                         \
                          discussion

Avant de se demander à quoi ça sert, soyez certain de bien comprendre ce que ça fait. On prend une branche, et on la fout autre part. Pas très compliqué, finalement, n'est-ce pas ?

Bon, ok, mais à quoi ça sert ?

C'est là ou le rebase est sous employé. Nous venons de voir un exemple d'utilisation trivial. Étudions quelques cas pratiques.

Éviter les commits de fusion pour les branches triviales

Quand on travaille avec Git, on a tendance à créer beaucoup de petites branches, ce qui est une bonne chose. Par contre, fusionner des branches créé des commits de fusion, comme dans notre exemple précédent.

Si vous créez beaucoup de petites branches, vous allez obtenir beaucoup de commits de fusion. Dans la mesure ou ces commits n'apportent pas d'information utile, ils polluent votre historique.

En rebasant (vous devrez me pardonner les anglicismes barbares) vos branches avant de les fusionner, vous obtiendrez un historique tout plat et bien plus agréable à parcourir. Prenons un exemple.

          F---G ← bug2
         /
A---B---E---H---I ← master
     \
      C---D ← bug1

En utilisant un rebase avant chaque fusion, on obtient l'historique suivant :

A---B---E---H---I---C---D---F---G ← master

Les commandes pour parvenir à ce résultat sont les suivantes, explications juste après.

  1. git rebase master bug1
  2. git checkout master
  3. git merge bug1
  4. git branch -d bug1
  5. git rebase master bug2
  6. git checkout master
  7. git merge bug2
  8. git branch -d bug2

Et le détail des commandes.

  1. Transplante bug1 sur l'actuelle branche master. Si on est déjà en train de bosser sur bug1 on peut se contenter de taper git rebase master
  2. Switche sur master
  3. Fusionne bug1 dans master
  4. Supprime la branche bug1 devenue inutile
  5. Transplante bug2 sur la branche master
  6. Switche sur master
  7. Fusionne bug2 dans master
  8. Supprime bug2 devenue inutile.

Et voilà un bel historique bien propre, exempt de commits de fusion inutiles.

Ça paraît laborieux mais avec l'habitude, ça se fait tout seul et c'est même plutôt amusant (je sais, un rien m'amuse).

Fusionner les branches en série

Prenons exactement le même exemple que précédemment, sauf que cette fois, nous ne fusionnons pas des petites branches triviales mais de vraies fonctionnalités.

          F---G ← newsletter
         /
A---B---E---H---I ← master
     \
      C---D ← password_reset

Un historique plat, c'est bien, mais on perd de l'information. Plus moyen de savoir en un coup d'œil que telle liste de commits a été réalisée sur une branche spécifique.

Pour pallier à ce problème, on va utilise une option de merge : --no-ff (pour « no fast forward »).

D'abord, les commandes.

git rebase master password_reset
git checkout master
git merge password_reset --no-ff
git branch -d password_reset
git rebase master newsletter
git checkout master
git merge newsletter --no-ff
git branch -d newsletter

On obtient alors l'historique suivant, bien plus clair. De plus, les commits « J » et « K » afficheront un message « branch machin was merged into master », ce qui fait que, même si les branches ont effectivement été supprimées, l'historique conserve une trace de leur existence.

A---B---E---H---I-------J-------K ← master
                 \     / \     /
                  C---D   F---G

Voici une capture d'un vrai projet, pour que vous puissiez vous extasier sur la beauté de la chose.

Un historique Git bien propre

C'est-y pas beau ? Pour un peu, je me ferais tatouer ça sur le corps.

Éviter les commits de fusion de git pull

Lorsque vous tapez git pull pour mettre à jour votre dépôt avec les derniers commits présents sur le serveur, Git va réaliser un merge pour fusionner vos modifications et celles que vous venez de récupérer.

Sur le serveur.

A---B---C---D---E ← master

Sur votre machine.

          origin/master
         /
A---B---C---F ← master

Après un git pull.

          D---E ← origin/mastel
         /     \
A---B---C---F---G ← master

Vous pourrez alors envoyer votre travail sur le serveur avec un git push.

Imaginez maintenant dix personnes qui travaillent sur la même branche (c'est très mal) et qui pushent et pullent toutes les cinq minutes. Vous imaginez la gueule de l'historique ?

Pour éviter ce problème, on va utiliser l'option git pull --rebase, qui produira le résultat suivant.

                  origin/master
                 /
A---B---C---D---E---F ← master

Et hop ! Encore une fois, un bel historique nickel. Merci Git !

Réparer un mauvais historique

Scénario : j'ai créé une branche newsletter pour travailler sur la fonctionnalité correspondante. J'ai également créé une branche bug_urgent pour corriger un bug qui doit être fixé urgemment, comme son nom l'indique.

Sauf que, au moment de fusionner ma branche bug_urgent, horreur ! malheur ! je m'aperçois que je n'ai pas créé ma branche au bon endroit. Mon historique ressemble à ça.

A---B---H---I---J ← master
     \
      C---D---G ← newsletter
           \
            E---F ← bug_urgent

Catastrophe ! Ma branche newsletter est un travail en cours, mais bug_urgent doit absolument être fusionnée dans master, le commercial a une démo dans 5 minutes. Comment faire ?!

Git rebase à la rescousse ! Nous allons simplement transplanter bug_urgent sur master, et le tour est joué.

git rebase newsletter bug_urgent --onto master
git checkout master
git merge bug_urgent
A---B---H---I---J---E---F ← master
     \
      C---D---G ← newsletter

Bingo ! Alors, merci qui ?

Vous noterez que l'appel de la commande rebase est ici un poil plus compliqué. Si nous nous étions contenté de la syntaxe habituelle git rebase master, nous aurions transplanté tous les commits de la branche bug_urgent en remontant jusqu'à master, c'est à dire les commits E et F, mais aussi C et D, ce qui n'est clairement pas le but.

La commande git rebase newsletter bug_urgent --onto master signifie « arrache la branche qui part de newsletter jusqu'à bug_urgent, et fous la sur master », ou encore « transplante sur master tous les commits qui sont sur bug_urgent:postlink: mais pas sur newsletter ». Pfiou !

Réparer un mauvais historique, bis

Autre exemple d'historique généré un soir de bourre.

A---B---H---I ← master
     \
      C---D---G ← bug1
       \
        E---F ← bug2

Or, il se trouve que les branches bug1 et bug2 sont totalement indépendantes. L'une sera peut-être fusionnée avant l'autre, ou abandonnée, on ne sait pas. Nous allons donc réparer cette bévue prestement.

git rebase bug1 bug2 --onto B
      E---F ← bug2
     /
A---B---H---I ← production
     \
      C---D---G ← bug1

Et voilà ! Notez qu'on peut transplanter à n'importe quel endroit, pas forcément sur une branche (ce qui est normal, puisqu'une branche n'est rien d'autre qu'une étiquette pointant sur un commit).

Faciliter l'intégration de branches

Scénario : je travaille sur une fonctionnalité qui nécessite plusieurs semaines de dev. J'ai donc une branche qui va évoluer pendant un loooooooooooong moment avant d'être fusionnée.

A---B---H---I--- … ---J---E---F ← master
      \
       C---D---G--- … ---H---I ← newsletter

Si les branches divergent (hum…) suffisamment, il est probable que le moment de la fusion va être assez pénible, avec des conflits à résoudre en pagaille. Une mauvaise journée en perspective.

Sauf si, malin, j'ai rebasé ma branche tous les matins en buvant mon café. Je corrige ainsi les conflits au fil de l'eau. Au bout de trois mois, voici mon historique :

A---B---H---I--- … ---J---E---F ← master
                               \
                                C---D---G--- … ---H---I ← newsletter

J'ai beau avoir trois mois de dev dans les pattes, la fusion est triviale et ne prends pas plus d'un quart de secondes. Merci git rebase.

Rebase interactif

Allez, on va arrêter de rigoler et sortir la grosse artillerie. Parce que git rebase dispose d'une petite option sympathique : l'option interactive. Quand je lui passe cette option, l'éditeur s'ouvre et je peux éditer un fichier en précisant ce que je veux faire de chaque commit en moment de son application. Exemple.

Voici un exemple d'historique standard (les commits les plus récents en haut).

* 4baf2db - Write tests for discussion
* 0fadd04 - Implement discussion
* 8be3c7e - Write tests for newsletter
* bce2851 - Implement newsletter
* 6477e21 - …

Tout va bien dans le meilleur des mondes. Quand tout à coup ! on m'annonce qu'un audit va être réalisé pour vérifier si les employé·e·s respectent bien la politique qualité de l'entreprise. Cette politique stipule que je suis censé commiter les tests d'une feature avant de commiter le code correspondant.

Et mince ! adieu ma prime ! Git rebase interactif à la rescousse.

git rebase -i HEAD~4

Cette commande signifie « arrache les quatre derniers commits et transplante les au même endroit ». En théorie, c'est une opération nulle SAUF qu'on va le faire de manière interactive.

Immédiatement après avoir tapé la commande, l'éditeur s'ouvre et affiche quelque chose comme ça :

pick 4baf2db Write tests for discussion
pick 0fadd04 Implement discussion
pick 8be3c7e Write tests for newsletter
pick bce2851 Implement newsletter

Les modifications que je vais réaliser dans ce fichier vont influer sur la manière dont mon rebase va se passer. En l'occurence, je vais effectuer cette simple modification.

pick 0fadd04 Implement discussion
pick 4baf2db Write tests for discussion
pick bce2851 Implement newsletter
pick 8be3c7e Write tests for newsletter

J'ai simplement modifié l'ordre des lignes. J'enregistre et quitte l'éditeur. Et magie ! Mes commits ont été réappliqués dans l'ordre indiqué. À moi la prime ! Notez que de nombreuses possibilités s'offrent à moi. J'aurais pu découper un commit en plusieurs ou au contraire en rassembler plusieurs en un seul ; ignorer des commits ; récupérer des commits tels quels mais modifier le message ; etc.

Pièges à éviter

WARNING ATTENTION ACHTUNG

Si vous voulez vous essayer au git rebase, lisez bien attentivement les paragraphes qui suivent. À peu près aucune commande de l'environnement Git n'a un plus grand pouvoir de nuisance que git rebase.

Parce qu'un rebase réécrit votre historique, et que ce n'est pas toujours une opération triviale. Si vous foirez votre coup, vous avez les moyens de franchement flinguer votre historique — rien d'irrécupérable, mais quand même de quoi passer un mauvais moment.

Git rebase ne déplace pas vraiment des commits

Quand je vous ai dit que rebase déplaçait des commits, j'ai menti (c'était pour votre bien). Parce que si vous avez lu l'article précédent, vous savez qu'il est strictement impossible de modifier un commit existant, pour la pure et simple raison qu'un commit est indexé par le hash de son contenu.

Reprenons la métaphore de l'arbre, toujours avec ses branches. Scannez une branche et utilisez une imprimante 3d pour en effectuer une copie la plus fidèle possible. Collez cette copie quelque part sur le tronc. Utilisez une cape d'invisibilité pour masquer la branche d'origine. Voilà, en vrai, c'est ça git rebase.

Ce que fait git rebase, c'est qu'il copie tous les commits transplantés et les réapplique un par un à l'endroit indiqués. Mais il s'agit bel et bien de nouveaux commits, avec des identifiants différents, même si le contenu est le même.

Reprenons notre exemple de tout à l'heure.

A---B---C---D ← master
     \
      E---F---G ← discussion

Si je rebase discussion sur master, ce qu'il se passe réellement ressemble plutôt à ça :

              E'---F'---G' ← discussion
             /
A---B---C---D ← master
     \
      E---F---G

Ainsi, les commits de l'ancienne branche discussion existent toujours, même s'ils sont invisibles car pointés par aucune branche. Les commits de la nouvelle branche discussion sont bels et biens différents.

L'historique partagé jamais tu ne rebaseras

Git rebase disaster girl

Tant que vous rebasez vos petites branches en local, tout va bien. Mais attention, si vous rebasez une branche qui se trouve déjà sur le serveur, c'est la catastrophe. Vous allez pourrir l'historique de tous vos coworkeurs, qui s'empresseront de vous couvrir de goudron et de plume avant d'essayer de vous vendre aux abattoirs d'un KFC. Illustration.

Sur le serveur.

A---B---C---D---E ← master
         \
          F---G---H ← feature

Sur votre machine.

A---B---C---D---E ← master

Vous rebasez les 4 derniers commits de master (comble de l'horreur).

A---B'---C'---D'---E' ← master

Vous poushez. Git affiche un message d'erreur, mais vous utilisez l'option « force » pour pousher quand même. Ni Dieu Ni Maître !

Sur le serveur.

  B'---C'---D'---E' ← master
 /
A---B---C---D---E
         \
          F---G---H ← feature

Wat ?! Imaginez le résultat quand vos collègues essayeront de récupérer les données du serveur. Ça ne va pas être joli et très franchement, je vous souhaite bon courage.

Notez donc bien soigneusement cette règle d'or de l'utilisation de rebase :

L'historique partagé jamais tu ne modifieras, sinon l'ire de ton équipe tu subiras.

Essayer

Pour ceux qui veulent s'amuser, voici le dépôt Git que j'utilise pour mes formations.

git clone https://github.com/thibault/iwantyoursocks.git

Vous pouvez par exemple essayer de rebaser puis fusionner les branches colors et material sur master.

Conclusion

Et voilà. Si vous êtes déjà un aficionado de Git, j'espère vous avoir communiqué un peu de mon enthousiasme pour git rebase, et si vous êtes novice, j'espère vous avoir mis le pied à l'étrier. Sur ce, je vous laisse, j'ai un historique à réécrire.