Le rebase interactif est l'outil que j'ouvre quand une branche feature ressemble à un carnet de labo: cinq commits "wip", un correctif de typo sur un correctif de typo, et un message que je ne mergerais jamais tel quel. Bien utilisé, ça transforme ce bruit en une courte histoire qu'un reviewer peut suivre. Sur la mauvaise branche, ça réécrit un historique sur lequel d'autres ont déjà construit, et le chat d'équipe s'enflamme.
Voici le playbook que j'utilise en équipe: les commandes, les verbes de todo qui comptent, quand s'arrêter, et comment s'en sortir quand ça tourne mal.
Ce que fait vraiment le rebase interactif
Un rebase classique rejoue vos commits sur une autre base (souvent main). L'interactif fait la même chose, mais ouvre d'abord une liste todo pour modifier chaque commit avant le rejeu.
# Réécrire les 4 derniers commits de la branche courante
git rebase -i HEAD~4
# Rejouer votre branche sur un main à jour, en éditant en route
git fetch origin
git rebase -i origin/main
Git ouvre l'éditeur avec des lignes de ce genre (le plus ancien en haut):
pick a1b2c3d add login form
pick d4e5f6a fix typo in label
pick 7890abc wip validation
pick bcdef01 tests for login
Vous changez le verbe en début de ligne (et éventuellement l'ordre ou vous supprimez des lignes). Enregistrez, quittez. Git rejoue les commits selon ce nouveau plan.
Deux règles qui évitent la plupart des catastrophes:
- Ne réécrivez que des commits qui ne sont pas sur une branche distante partagée que d'autres pullent (ou qui ne sont qu'à vous).
- Avant un rebase risqué, notez un point de reprise:
git rev-parse HEAD, ou comptez surORIG_HEADet lereflog(plus bas).
Les verbes de todo que vous utilisez vraiment
Pas besoin de tous le premier jour. Ces six couvrent presque tout le travail d'équipe.
| Verbe | Effet | Quand je l'utilise |
|---|---|---|
pick |
Garde le commit tel quel | Par défaut; ne touchez pas |
reword |
Garde le diff, édite le message | Typos, "wip", id de ticket manquant |
edit |
Pause pour amend ou découpe | Fichier oublié; vouloir deux commits au lieu d'un |
squash |
Fusionne dans le précédent; édite le message combiné | Plusieurs WIP liés qui doivent devenir une seule histoire |
fixup |
Fusionne dans le précédent; jette ce message | Micro-correctifs: lint, import, test |
drop (ou supprimer la ligne) |
Supprime le commit | Expérience qui ne doit jamais ship |
Il y a aussi exec pour lancer une commande après un commit (pratique pour npm test à chaque étape quand vous nettoyez une série sale). Je m'en sers rarement au quotidien sur un PR.
Squash vs fixup
Les deux plient un commit dans celui du dessus. La différence, c'est le message.
squash: un éditeur s'ouvre pour rédiger le message combiné. À utiliser quand le commit enfant avait une vraie intention à garder dans le message final.fixup: le message de l'enfant est jeté. À utiliser pour du pur bruit de réparation ("fix tests", "oops").
Exemple de liste todo:
pick a1b2c3d add rate limiter middleware
fixup d4e5f6a fix off-by-one in window
fixup 7890abc missing unit test
reword bcdef01 document env vars
Résultat: deux commits. Le premier porte les trois diffs et le message middleware d'origine (sauf reword plus tard). Le second garde son arbre avec un message nettoyé.
Raccourci quand vous savez déjà vouloir des fixup en cours de route:
# Stage d'un petit fix qui appartient au commit précédent
git commit --fixup HEAD
# Plus tard, autosquash de ces fixup pendant le rebase
git rebase -i --autosquash origin/main
--autosquash réordonne les commits fixup! ... et squash! ... sous leurs cibles et met le bon verbe. Vous confirmez encore dans l'éditeur.
Reword sans toucher au code
Les mauvais messages sont le nettoyage le plus courant. Pas besoin de toucher au diff.
git rebase -i HEAD~3
# remplacez "pick" par "reword" sur les lignes concernées
Git s'arrête à chaque reword, ouvre l'éditeur de message, puis continue. Préférez ça à git commit --amend quand le mauvais message n'est pas le commit de tête.
Si c'est la tête et que rien n'a été push (ou que la branche est à vous seul):
git commit --amend
# ou seulement le message:
git commit --amend -m "feat(auth): validate session before refresh"
Edit: amend en milieu d'historique ou découper un commit
edit met le rebase en pause sur ce commit. Le working tree est à ce snapshot. Cas fréquents:
Fichier oublié dans un vieux commit
git rebase -i HEAD~5
# marquez le commit cible en "edit"
# quand Git s'arrête:
git add path/to/missing-file
git commit --amend --no-edit
git rebase --continue
Découper un commit en deux
# en pause sur "edit":
git reset HEAD^
git add -p # première moitié logique
git commit -m "first half"
git add -p
git commit -m "second half"
git rebase --continue
git reset HEAD^ laisse les changements dans l'arbre pour les re-stager. Vous ne supprimez pas le travail; vous recoupez les commits.
Réordonner les commits
Parfois l'histoire est dans le désordre: tests avant le code qu'ils testent, ou un refactor au milieu d'une feature.
Dans la liste todo, déplacez les lignes. Le plus ancien reste en haut; l'ordre des lignes est le nouvel historique.
pick aaa1111 add API handler
pick bbb2222 add unit tests for handler
pick ccc3333 wire route in main
Si un commit plus récent dépend d'un plus ancien, réordonner peut créer des conflits. Ce n'est pas grave. Résolvez, git add, git rebase --continue. Si la dépendance est réelle, mettez d'abord le commit fondation.
Une passe de nettoyage complète (le rituel habituel du PR)
Avant d'ouvrir ou de mettre à jour un PR, je veux souvent:
- Branche basée sur le
maincourant. - Plus de bruit WIP.
- Messages au style de l'équipe (ticket, scope, pourquoi).
git fetch origin
git rebase -i origin/main
Esquisse de liste todo:
pick 111aaaa feat: add checkout retry
fixup 222bbbb wip
fixup 333cccc fix tests
reword 444dddd more logging
pick 555eeee docs: note retry env flag
Ensuite:
# Si la branche était déjà pushée, l'historique a changé:
git push --force-with-lease
Préférez toujours --force-with-lease à --force. Lease refuse le push si le remote a bougé depuis votre dernier fetch (quelqu'un d'autre a push). Ceinture pas chère.
Quand NE PAS rebaser
Le rebase interactif réécrit les SHA. Tout ce qui dépend déjà des anciens SHA va diverger.
Ne rebaser pas:
main/master/ branches de release partagées que beaucoup de gens pullent. Point final.- Toute branche sur laquelle plusieurs personnes pushent, sauf accord explicite et fenêtre de freeze.
- Des commits déjà entrés dans d'autres branches longues via merge. Vous créez des doublons ou un double historique sale.
- Tags publics et SHA de release que le CI, les deploys ou l'audit épinglent.
Préférez un merge (ou laissez l'historique) quand:
- La branche est d'intégration partagée.
- L'audit tient à la chaîne exacte des commits d'origine.
- Vous n'êtes pas sûr de coordonner un force-push avec chaque collaborateur.
Défaut sûr pour les features: rebasez votre branche feature sur main, nettoyez en local, force-with-lease sur votre remote feature, ouvrez/mettez à jour le PR. Ne réécrivez jamais main.
Certaines équipes interdisent le rebase sur les branches distantes et n'autorisent que le squash-on-merge côté GitHub/GitLab. C'est une politique valide. Le rebase interactif reste utile avant le premier push, ou sur des branches qui ne sont qu'à vous.
Conflits pendant le rebase
Un conflit n'est pas un échec. Git s'est arrêté pour que vous décidiez.
# Voir les chemins en conflit
git status
# Corrigez les fichiers, puis:
git add path/that/you/fixed
git rebase --continue
# Ou abandonnez tout le rebase et revenez:
git rebase --abort
Si vous êtes en plein milieu et voulez un arbre propre pour réfléchir:
git rebase --abort # revient à l'état pré-rebase (quand c'est possible)
--abort est la sortie de secours. Mieux qu'un arbre à moitié résolu et un "je corrigerai plus tard."
Se rattraper après une erreur
ORIG_HEAD
Beaucoup d'opérations de rebase posent ORIG_HEAD sur la tête d'avant l'opération.
# Voir où vous étiez
git log --oneline ORIG_HEAD -5
# Hard reset de la tête de branche (destructif pour la tête actuelle seulement)
git reset --hard ORIG_HEAD
Ne faites un hard reset que si vous comprenez que vous jetez la tête réécrite au profit de la pré-rebase. Le travail non commité est un autre sujet; committez ou stash d'abord.
Le reflog est le vrai filet
Chaque mouvement de tête est enregistré en local un moment (souvent ~90 jours par défaut, selon la config).
git reflog
# lignes d'exemple:
# abc1234 HEAD@{0}: rebase (finish): returning to refs/heads/feature/x
# def5678 HEAD@{1}: rebase (start): checkout origin/main
# 999aaaa HEAD@{2}: commit: wip
Trouvez le SHA d'avant le mauvais rebase (HEAD@{n}), puis:
git reset --hard HEAD@{2}
# ou
git reset --hard 999aaaa
Le reflog est local. Il ne vous sauve pas sur un autre clone. Il vous sauve sur la machine où vous avez réécrit l'historique, ce qui suffit souvent pour "j'ai squash le mauvais truc il y a cinq minutes."
Récupérer un commit droppé encore dans la base d'objets
Si vous avez droppé un commit mais vous souvenez d'un fragment de message:
git fsck --lost-found
# ou chercher des commits dangling
git log --all --oneline --grep='partial message'
# une fois le SHA en main:
git show deadbeef
git branch rescue/deadbeef deadbeef
Puis cherry-pick ou reset sur cette branche de secours.
Annuler un mauvais force push (coordination requise)
Si vous avez force-push une mauvaise tête et que quelqu'un a déjà fetch:
- Récupérez le bon SHA dans votre reflog (ou le leur).
git push --force-with-leasedu bon SHA.- Prévenez l'équipe:
git fetchetgit reset --hard origin/votre-brancheuniquement sur cette feature branch.
C'est pour ça qu'un force-push sur une branche partagée est un problème de process, pas seulement de Git.
Conventions d'équipe qui gardent le rebase utile
Quelques normes qui tiennent la route:
- Les branches feature sont un historique jetable. Nettoyez-les. Le squash-on-merge est optionnel si le PR a déjà des commits propres.
- Ne réécrivez jamais
main. Protégez-le côté hébergeur. - Force-with-lease seulement sur les branches que vous possédez. Préfixez le handle dans le nom de branche si l'org est grande.
- Le CI doit relancer après force-push. La plupart des hôtes le font; vérifiez.
- Préférez de petits commits fixup en codant, autosquash avant la review. Flux local rapide, PR lisible.
- Si deux personnes co-possèdent une longue branche, mergez depuis
mainplutôt que tout le monde rebase, ou nommez un propriétaire de l'historique.
Aide-mémoire
# Rebase interactif des N derniers commits
git rebase -i HEAD~N
# Sur un main à jour
git fetch origin && git rebase -i origin/main
# Commits fixup en cours de route
git commit --fixup <sha-or-HEAD>
# Appliquer les fixups dans la liste todo
git rebase -i --autosquash origin/main
# Flux de conflit
git add <files> && git rebase --continue
git rebase --abort
# Après réécriture de votre feature branch
git push --force-with-lease
# Récupération
git reflog
git reset --hard ORIG_HEAD
git reset --hard HEAD@{n}
Modèle mental de fin
Voyez le rebase interactif comme l'édition d'une série de patches, pas comme "effacer le passé." Les vieux commits restent souvent dans le reflog jusqu'au garbage collection. L'histoire partagée, c'est ce que vous push.
Utilisez-le pour rendre la review moins chère: moins de commits, messages honnêtes, histoire linéaire sur le main actuel. Arrêtez-vous dès que les commits sont un contrat partagé. Cette frontière, plus que n'importe quel verbe de la todo, sépare un historique propre d'un incident d'équipe.
