Le rebase fait peur, et on lui prête volontiers des pouvoirs mystérieux. En réalité il ne fait qu'une chose : rejouer vos commits ailleurs. Reste à comprendre ce que « rejouer » implique.
La situation de départ
Vous travaillez sur une fonctionnalité. Lundi matin, vous créez votre branche :
git checkout -b feature/remise-fidelite
Vous faites deux commits. Pendant ce temps, un collègue livre quelque chose sur main. Mercredi, vous voulez récupérer son travail — parce qu'il touche un fichier que vous modifiez aussi, ou simplement parce que votre branche prend du retard.
Voici l'état du dépôt. Chaque lettre est un commit, les flèches vont du plus ancien au plus récent :
D───E feature
/
A───B───C main
Vous avez créé votre branche à partir de B. Depuis, main a avancé jusqu'à C.
Deux commandes permettent de vous remettre à jour. Elles produisent le même code, mais pas du tout la même histoire.
Option 1 : merger main dans feature
git checkout feature/remise-fidelite git merge main
Git crée un nouveau commit — un commit de fusion — dont le rôle est de dire : « ici, deux histoires se rejoignent ». Il a deux parents, E et C :
D───E───M feature
/ /
A───B───C──────' main
Vos commits D et E n'ont pas bougé d'un pouce. Ils ont toujours le même identifiant, le même parent, le même contenu. On a simplement ajouté M par-dessus.
Le journal ressemble à ça :
* 9f2a1c4 Merge branch 'main' into feature/remise-fidelite |\ | * 3c1d8e9 Renomme calculerTotal en calculerMontantTotal * | 7b4e2a1 Ajoute les frais de port * | a1b2c3d Ajoute la remise fidélité |/ * 5e8f0b2 Crée le panier
C'est fidèle à la réalité : deux personnes ont travaillé en parallèle, puis les travaux ont été réunis. C'est aussi un peu plus difficile à lire, et ça le devient franchement quand vous répétez l'opération tous les deux jours pendant trois semaines.
Option 2 : rebaser feature sur main
git checkout feature/remise-fidelite git rebase main
Cette fois, aucun commit de fusion. Git déplace le point de départ de votre branche :
D'──E' feature
/
A───B───C main
Le journal devient une ligne droite :
* 4d9c7f1 Ajoute les frais de port * 8a3b6e2 Ajoute la remise fidélité * 3c1d8e9 Renomme calculerTotal en calculerMontantTotal * 5e8f0b2 Crée le panier
On lit l'histoire de haut en bas comme un récit. Plus de tresse, plus d'entrelacs.
Mais regardez bien les identifiants : a1b2c3d est devenu 8a3b6e2. Ce n'est pas un détail de présentation.
Pourquoi les commits changent d'identifiant
Un commit git n'est pas un « diff » stocké quelque part. C'est un objet qui contient un instantané complet du projet, un message, un auteur, une date… et l'identifiant de son parent. Son propre identifiant — le fameux SHA — est le résultat d'un calcul portant sur tout ça.
Changez le parent, et l'identifiant change forcément.
Or c'est exactement ce que fait un rebase. Concrètement, git procède ainsi :
- Il calcule ce que chacun de vos commits a modifié — la différence entre le commit et son parent.
- Il place votre branche sur le nouveau point de départ, ici
C. - Il applique la première modification, et crée un commit neuf :
D'. - Il applique la deuxième par-dessus, et crée
E'.
D' contient les mêmes modifications que D, mais ce n'est pas le même commit. D et E existent encore quelque part dans le dépôt, orphelins, jusqu'à ce que git fasse le ménage.
D'où la formule qu'on entend partout : le rebase réécrit l'histoire. Elle est exacte, et elle explique à peu près tous les problèmes qu'on peut rencontrer avec cette commande.
Les conflits : c'est là que la différence se sent
C'est le point qui déroute le plus quand on débute, alors prenons un exemple précis.
Votre collègue a renommé une méthode dans panier.js (commit C) :
// avant
function calculerTotal(lignes) { ... }
// après
function calculerMontantTotal(lignes) { ... }
De votre côté, vous avez deux commits qui touchent cette même méthode : D y ajoute une remise, E y ajoute des frais de port.
Avec un merge, git compare trois choses : l'état commun de départ (B), l'état final de main (C) et l'état final de votre branche (E). Vous obtenez un seul conflit, mais il contient tout en vrac : la remise et les frais de port, mêlés au renommage. Vous résolvez une fois, et c'est fini.
Avec un rebase, git rejoue vos commits un par un. Il applique donc D sur C :
Auto-merging panier.js CONFLICT (content): Merge conflict in panier.js error: could not apply a1b2c3d... Ajoute la remise fidélité
Il s'arrête au milieu du rejeu. Un git status vous dit précisément où vous en êtes :
interactive rebase in progress; onto 3c1d8e9 Last commands done (1 command done): pick a1b2c3d Ajoute la remise fidélité
Vous ouvrez le fichier, vous ne voyez que le conflit entre le renommage et la remise. C'est petit, c'est ciblé. Vous résolvez, puis :
git add panier.js git rebase --continue
Git crée D', puis passe au suivant : il applique E sur D'. Et là, éventuellement, un second conflit — celui des frais de port. Vous le résolvez à son tour.
Deux conflits au lieu d'un. C'est plus de manipulations, mais chacune est plus simple : vous savez exactement quel changement vous êtes en train de réconcilier avec quoi. Là où le merge vous demande de trancher un gros nœud d'un coup, le rebase vous fait démêler les fils un par un.
Et si ça part en vrille, une seule commande vous ramène à l'état initial, comme si vous n'aviez rien fait :
git rebase --abort
C'est ce filet de sécurité qui rend le rebase beaucoup moins impressionnant qu'il n'en a l'air. Tant que vous n'avez rien poussé, vous ne pouvez rien casser d'irréversible.
Le rebase interactif : nettoyer avant de montrer
Il existe une seconde utilisation du rebase, sans rapport avec la mise à jour d'une branche : ranger ses propres commits.
Soyons honnêtes sur ce à quoi ressemble une branche en fin de journée :
* 3f8a2b1 fix typo * 9c4d7e2 wip * 6b1a5f3 Ajoute les frais de port * a1b2c3d Ajoute la remise fidélité
Personne n'a envie de relire ça en revue de code. Le rebase interactif permet de le réécrire :
git rebase -i HEAD~4
HEAD~4 signifie « les quatre derniers commits ». Git ouvre alors votre éditeur avec cette liste :
pick a1b2c3d Ajoute la remise fidélité pick 6b1a5f3 Ajoute les frais de port pick 9c4d7e2 wip pick 3f8a2b1 fix typo
Attention à un piège classique : ici les commits sont dans l'ordre chronologique, du plus ancien en haut au plus récent en bas. C'est l'inverse de ce qu'affiche git log. Beaucoup de premières tentatives échouent uniquement à cause de ça.
Vous ne modifiez pas le code dans ce fichier : vous remplacez le mot en début de ligne par une instruction. Les principales :
pick— garder le commit tel quel (le défaut) ;reword— garder le commit, mais changer son message ;squash— fusionner ce commit avec celui du dessus, en combinant les deux messages ;fixup— commesquash, mais en jetant le message de ce commit (parfait pour les « fix typo ») ;drop— supprimer complètement le commit ;edit— s'arrêter sur ce commit pour le modifier.
Vous pouvez aussi réordonner les lignes : déplacer une ligne, c'est déplacer le commit dans l'histoire.
Reprenons notre exemple. On veut absorber les deux commits de bricolage dans celui des frais de port, et corriger un message :
pick a1b2c3d Ajoute la remise fidélité reword 6b1a5f3 Ajoute les frais de port fixup 9c4d7e2 wip fixup 3f8a2b1 fix typo
Vous enregistrez, vous fermez l'éditeur. Git rejoue tout : il garde le premier commit, s'arrête pour vous laisser réécrire le message du deuxième, puis fond les deux derniers dedans. Résultat :
* 7e2f9a4 Ajoute les frais de port et la TVA associée * 8a3b6e2 Ajoute la remise fidélité
Deux commits propres, chacun racontant une intention. C'est autrement plus agréable à relire — pour vos collègues aujourd'hui, et pour vous dans six mois quand un git blame vous ramènera sur cette ligne.
Là encore, git rebase --abort à n'importe quel moment annule tout.
La règle d'or
Ne rebasez jamais des commits déjà poussés et récupérés par quelqu'un d'autre.
La raison découle directement de ce qu'on a vu : le rebase crée de nouveaux commits. Si votre collègue a déjà récupéré D et E, et que vous poussez D' et E', vous vous retrouvez avec deux versions du même travail portant des identifiants différents. Quand il fera un git pull, git ne verra pas « la même chose renommée » : il verra quatre commits distincts, et tentera de les fusionner. Vous obtiendrez des doublons, des conflits absurdes, et une soirée gâchée.
En pratique, retenez :
- votre branche personnelle, sur laquelle personne d'autre ne travaille : rebasez autant que vous voulez ;
main, ou toute branche partagée : jamais.
Si vous avez déjà poussé votre branche personnelle avant de la rebaser, il faudra forcer la mise à jour. Utilisez systématiquement :
git push --force-with-lease
plutôt que --force. La différence est réelle : --force-with-lease refuse de pousser si quelqu'un a touché la branche distante depuis votre dernier fetch. C'est la ceinture de sécurité qui vous évite d'écraser le travail d'un autre par mégarde.
Alors, merge ou rebase ?
La question est mal posée : les deux ont leur usage, et la plupart des équipes utilisent les deux.
Ce qui fonctionne bien, en général :
- rebaser sa branche sur
mainpour rester à jour pendant le développement — l'histoire reste lisible, et on découvre tôt les frictions avec le travail des autres ; - merger — ou faire un squash merge — pour intégrer la branche terminée dans
main, ce qui laisse une trace explicite de l'arrivée de la fonctionnalité ; - rebaser en interactif juste avant d'ouvrir la pull request, pour nettoyer.
Et si votre équipe a décidé de bannir le rebase, ce n'est pas une hérésie : l'historique reflète alors exactement ce qui s'est passé, avec ses parallélismes et ses hésitations. C'est un choix défendable. L'important est qu'il soit conscient et partagé, pas subi.
Conclusion
Le rebase souffre de sa réputation. On le présente comme une commande dangereuse qui « réécrit l'histoire », ce qui est vrai mais n'aide personne à comprendre.
Retenez plutôt ces trois idées :
- Le rebase rejoue vos modifications sur une nouvelle base. Il ne déplace rien, il recrée.
- Comme il recrée, les identifiants changent — d'où la règle d'or sur les branches partagées.
- Comme il rejoue commit par commit, les conflits arrivent par petites doses plutôt qu'en un bloc.
Le reste n'est que pratique. Créez un dépôt de test, faites-vous quelques branches bidon, provoquez volontairement des conflits, et abusez de git rebase --abort. Une demi-heure à jouer avec vaut mieux que dix articles — celui-ci compris.
Références
- Pro Git — Rebasing (chapitre 3.6, en français)
- Documentation officielle de
git rebase - Learn Git Branching — un bac à sable visuel pour s'entraîner sans risque