L'outil le plus évité de Git
Demandez à une salle remplie de développeurs quelle commande Git les rend nerveux, et git rebase -i arrivera en tête de liste. Le rebase interactif a une réputation : il réécrit l'historique, il ouvre une étrange todo list dans votre éditeur, et il semble dangereusement facile de casser quelque chose. Alors la plupart des gens l'évitent tout simplement -- et leurs pull requests arrivent chargées de commits comme "wip", "fix typo" et "actually fix it".
C'est dommage, car le rebase interactif est le meilleur outil qui soit pour transformer une branch désordonnée en un historique propre et facile à relire. Dans ce guide, nous allons le démystifier avec un exemple concret suivi de bout en bout : une branch de fonctionnalité avec 9 commits désordonnés que nous allons remodeler en 3 commits propres avant d'ouvrir une pull request. Si vous vous demandez encore si le rebase est même le bon choix pour votre workflow, commencez par notre comparatif rebase vs merge -- cet article part du principe que vous avez décidé de faire un rebase et que vous voulez bien le faire.
Pourquoi un historique propre est important
Nettoyer ses commits avant une pull request n'est pas une question d'esthétique. Un historique soigné est rentable de trois manières très concrètes :
- La relecture. Un relecteur qui lit trois commits logiques -- "Add login form with validation", "Add remember me option", "Add error messages" -- peut examiner chaque changement isolément. Le même code éparpillé sur neuf commits à moitié finis l'oblige à relire tout le diff d'un coup, et la qualité de la relecture s'effondre.
- git bisect. Quand un bug apparaît des semaines plus tard,
git bisectparcourt l'historique pour trouver le commit qui l'a introduit. Cela ne fonctionne que si chaque commit compile et a du sens par lui-même. Un historique rempli de commits "wip" qui ne compilent même pas rend bisect quasiment inutilisable. - L'annulation. Si une fonctionnalité doit être retirée, faire un revert d'un commit autonome est trivial. Faire un revert d'une fonctionnalité étalée sur neuf commits entrelacés, c'est un après-midi d'archéologie.
Notre exemple fil rouge : 9 commits désordonnés
Voici la branch que nous allons nettoyer. C'est un historique de travail parfaitement normal -- c'est comme cela que tout le monde code en réalité :
$ git log --oneline main..feature/login
c9d0e1f oops forgot the css file
b8c9d0e add error messages
a7b8c9d wip 2
f6a7b8c add remember me checkbox
e5f6a7b actually fix it
d4e5f6a fix typo
c3d4e5f add form validation
b2c3d4e wip
a1b2c3d add login form skeleton
Il n'y a rien de mal à committer ainsi pendant que vous travaillez. Le problème, c'est seulement si cela part en production tel quel. Notre objectif est d'aboutir à trois commits :
- Add login form with validation
- Add remember me option
- Add error messages with styling
Démarrer le rebase : git rebase -i HEAD~9
Pour réécrire les 9 derniers commits, lancez :
git rebase -i HEAD~9
HEAD~9 signifie "le commit situé 9 crans avant le commit actuel" -- tout ce qui suit ce point devient modifiable. Git ouvre votre éditeur avec la todo list :
pick a1b2c3d add login form skeleton
pick b2c3d4e wip
pick c3d4e5f add form validation
pick d4e5f6a fix typo
pick e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
pick a7b8c9d wip 2
pick b8c9d0e add error messages
pick c9d0e1f oops forgot the css file
Deux choses à comprendre avant de toucher à quoi que ce soit. Premièrement, ce n'est qu'un fichier texte : rien ne se passe tant que vous ne l'avez pas enregistré et fermé, et si vous supprimez toutes les lignes ou fermez sans enregistrer de changements significatifs, le rebase est annulé sans aucun dégât. Deuxièmement, l'ordre est du plus ancien au plus récent -- l'inverse de git log. Git va rejouer les commits de haut en bas, en appliquant le verbe placé au début de chaque ligne.
Les six verbes
pick
Conserver le commit exactement tel quel. C'est la valeur par défaut pour chaque ligne.
reword
Conserver les changements du commit, mais s'arrêter pour vous laisser modifier le message du commit. Parfait pour corriger "wip 2" sans toucher au code.
edit
Mettre le rebase en pause sur ce commit. Vous pouvez l'amender, le découper en plusieurs commits, ou le tester, puis continuer avec git rebase --continue. C'est le verbe le plus avancé et celui que vous utiliserez le moins souvent.
squash
Fusionner ce commit dans celui du dessus, et ouvrir l'éditeur pour que vous puissiez combiner les deux messages de commit en un seul.
fixup
Comme squash, mais en jetant entièrement le message de ce commit. Le commit du dessus conserve son message inchangé. C'est ce qu'il vous faut pour les commits "fix typo" -- leurs messages ne contiennent aucune information qui mérite d'être conservée.
drop
Supprimer complètement le commit et ses changements. Supprimer la ligne de la todo list a le même effet, mais écrire drop rend votre intention explicite.
Nettoyer la branch, étape par étape
Appliquons maintenant tout cela à nos 9 commits. Deux opérations sont nécessaires : réordonner et changer les verbes.
D'abord, le réordonnancement. En regardant les diffs, "oops forgot the css file" est en réalité la feuille de style de la case à cocher remember me, pas celle des messages d'erreur. Dans la todo list, réordonner consiste simplement à déplacer une ligne : coupez la ligne c9d0e1f et collez-la juste après le groupe remember me. Git rejouera les commits dans le nouvel ordre.
Ensuite, les verbes. Tout ce qui précède "actually fix it" inclus appartient au formulaire de connexion, donc tout cela se replie dans le premier commit. Nous utilisons squash sur "add form validation" parce que nous voulons fusionner son message dans le message final, et fixup sur les commits de bruit dont les messages méritent de disparaître. Voici la todo list après modification :
pick a1b2c3d add login form skeleton
fixup b2c3d4e wip
squash c3d4e5f add form validation
fixup d4e5f6a fix typo
fixup e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
fixup a7b8c9d wip 2
fixup c9d0e1f oops forgot the css file
reword b8c9d0e add error messages
Enregistrez et fermez. Git rejoue les commits et s'arrête deux fois : une fois pour vous laisser écrire le message combiné du premier groupe (tapez "Add login form with validation"), et une fois pour le reword (tapez "Add error messages with styling"). Pour le commit remember me, un rapide reword aurait aussi fait l'affaire -- ici nous l'avons laissé en pick et son message d'origine était déjà correct. Le résultat :
$ git log --oneline main..feature/login
3f4e5d6 Add error messages with styling
2e3d4c5 Add remember me option
1d2c3b4 Add login form with validation
Neuf commits sont devenus trois, chacun autonome et facile à relire. Remarquez que tous les hashes ont changé -- ce sont des commits flambant neufs. Gardez ce détail en tête ; c'est le cœur de la règle d'or ci-dessous.
Le geste de pro : --autosquash
Une fois à l'aise, vous pouvez préparer le nettoyage pendant que vous travaillez au lieu de tout démêler à la fin. Quand vous corrigez quelque chose appartenant à un commit antérieur, enregistrez-le ainsi :
git commit --fixup a1b2c3d
Git crée un commit dont le message est fixup! add login form skeleton. Plus tard, lancez :
git rebase -i --autosquash main
Git pré-remplit la todo list avec chaque commit fixup! déjà déplacé sous sa cible et marqué comme fixup. Vous n'avez plus qu'à enregistrer et fermer. Pour en faire le comportement par défaut, configurez-le une fois pour toutes :
git config --global rebase.autosquash true
La règle d'or : ne jamais faire de rebase sur des commits partagés
Le rebase ne modifie pas les commits -- il les remplace par de nouveaux. Si des coéquipiers ont déjà récupéré les anciens commits et basé leur travail dessus, réécrire ces commits crée deux historiques divergents, et le merge suivant devient un chaos de doublons et de conflits.
La règle est simple : ne faites un rebase que sur des commits qui n'ont pas été poussés, ou qui vivent sur une branch sur laquelle vous êtes le seul à travailler. Nettoyer votre propre branch de fonctionnalité avant d'ouvrir une pull request est le cas sûr par excellence. Si vous avez déjà poussé la branch et que vous en êtes toujours le seul auteur, poussez l'historique réécrit avec git push --force-with-lease, qui refuse d'écraser le travail que quelqu'un d'autre aurait poussé entre-temps. Ne réécrivez jamais main ni aucune branch depuis laquelle votre équipe fait des pulls.
Quand ça tourne mal
En plein rebase, deux choses peuvent arriver. Si un commit rejoué entre en conflit avec un changement antérieur, Git fait une pause et vous demande de résoudre le conflit, puis de faire git add sur les fichiers et de lancer git rebase --continue. Et si à un moment quelconque vous vous sentez perdu, il existe un bouton d'éjection complet :
git rebase --abort
Cela ramène la branch exactement dans l'état où elle était avant que vous ne commenciez, sans poser de questions. Tant que vous vous souvenez que --abort existe, un rebase en cours ne peut jamais vous piéger.
Et si le rebase est terminé et que vous réalisez que le résultat est mauvais ? Même dans ce cas, rien n'est perdu. Les anciens commits existent toujours -- Git les conserve pendant des semaines, et le reflog enregistre chaque position vers laquelle votre branch a pointé, vous pouvez donc revenir à l'état d'avant le rebase avec un simple reset. Nous détaillons ce filet de sécurité dans récupérer des commits perdus avec git reflog. Et si votre erreur est plus petite -- seul le dernier commit doit être modifié -- vous n'avez pas du tout besoin d'un rebase : voyez comment annuler un commit Git.
Le rebase interactif dans GitSquid
La todo list est puissante, mais éditer des verbes dans un fichier texte est exactement la partie qui rend les gens nerveux. GitSquid la remplace par un rebase interactif visuel : vous réordonnez les commits par drag & drop, et vous choisissez l'action pour chaque commit -- pick, squash, fixup, reword ou drop -- via un sélecteur déroulant. Tout le plan est visible d'un coup d'œil avant que quoi que ce soit ne s'exécute, ce qui élimine l'essentiel du facteur peur.
GitSquid vous aide aussi avant même de commencer : le Conflict Predictor, inclus dans l'offre gratuite, montre quels fichiers entreront en conflit avant que vous ne lanciez un rebase, vous savez donc à quoi vous vous engagez au lieu de découvrir les conflits à mi-chemin. Le rebase est accessible directement depuis les menus contextuels des branches et des commits sur le graphe de commits.
Référence rapide
| Commande / verbe | Ce que ça fait |
|---|---|
git rebase -i HEAD~n |
Réécrit interactivement les n derniers commits |
pick |
Conserve le commit tel quel |
reword |
Conserve les changements, modifie le message |
edit |
S'arrête sur ce commit pour l'amender ou le découper |
squash |
Fusionne dans le commit précédent, combine les messages |
fixup |
Fusionne dans le commit précédent, jette ce message |
drop |
Supprime entièrement le commit |
git commit --fixup <hash> |
Enregistre un correctif destiné à un commit antérieur |
git rebase -i --autosquash |
Place automatiquement les commits fixup! dans la todo list |
git rebase --continue |
Reprend après la résolution d'un conflit |
git rebase --abort |
Annule et restaure l'état d'avant le rebase |
Le rebase interactif récompense un peu de pratique par beaucoup de puissance. Commencez sur une branch jetable, gardez --abort en tête, respectez la règle d'or, et transformer "wip, fix typo, actually fix it" en un historique propre devient une routine. Et si vous préférez voir le plan plutôt que d'éditer un fichier texte, téléchargez GitSquid gratuitement et essayez le rebase visuel sur votre prochaine branch de fonctionnalité.