Tutoriel

git rebase : fonctionnement, mode interactif et squash de commits

Mis à jour: · Skillaria.Top

git rebase déplace les commits de la branche courante sur une nouvelle base : Git retire temporairement vos commits, place la branche au bout de la branche cible et les réapplique sous forme de nouveaux commits. On obtient ainsi un historique linéaire sans commits de merge, et git rebase -i permet de fusionner (squash), renommer ou supprimer des commits ; on ne réécrit ainsi que des commits pas encore publiés pour les autres.

Ce que fait git rebase

git rebase « transplante » votre branche sur un autre point de l'historique. Imaginons que vous ayez créé feature à partir de main il y a une semaine et que main ait avancé depuis. Après un rebase, votre branche donne l'impression d'avoir été commencée à l'instant, à partir du commit le plus récent de main.

git switch feature
git fetch origin
git rebase origin/main

Sous le capot, il y a trois étapes. Git trouve l'ancêtre commun des branches, enregistre une à une les modifications de chacun de vos commits postérieurs, déplace la branche sur origin/main et réapplique les modifications enregistrées, l'une après l'autre. On obtient de nouveaux commits avec le même contenu et les mêmes messages, mais avec d'autres hashes.

Avant :  A---B---C---F---G   main
                  \
                   D---E     feature

Après :  A---B---C---F---G   main
                          \
                           D'---E'   feature

Les primes de D' et E' ne sont pas décoratives. Ce sont d'autres objets, avec d'autres hashes et un autre parent. D'où la règle principale du rebase, détaillée ci-dessous.

La règle d'or : ne rebasez pas ce qui est publié

Rebase réécrit l'historique. Tant que les commits n'existent que chez vous, c'est sans danger. Mais si vos collègues ont déjà récupéré la branche, ils garderont les anciens D et E, vous aurez les nouveaux D' et E', et au prochain pull l'historique sera dupliqué et les conflits seront à résoudre une seconde fois.

En pratique : on utilise rebase sur ses branches locales et sur les branches de fonctionnalité personnelles où une seule personne travaille. Les branches partagées — main, develop, les branches de release — ne se rebasent jamais.

Comment pousser une branche après un rebase

Si la branche était déjà sur le serveur, un git push classique sera refusé : l'historique local ne prolonge plus l'historique distant. Il faut un push forcé, dans sa version sûre :

git push --force-with-lease

Contrairement à --force, l'option --force-with-lease refuse d'écraser la branche si des commits que vous n'avez pas vus sont apparus sur le serveur. Vous n'effacerez donc pas le travail d'un autre par accident.

Les conflits pendant un rebase

Comme les commits sont appliqués un par un, un conflit peut survenir sur n'importe lequel — et même plusieurs fois d'affilée. Git s'arrête et indique quel commit n'a pas pu être appliqué.

$ git rebase origin/main
Auto-merging src/api.js
CONFLICT (content): Merge conflict in src/api.js
error: could not apply 4e1f9a2... Ajouter un cache des requêtes
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".

Marche à suivre :

  1. Ouvrez le fichier en conflit, choisissez la bonne version et supprimez les marqueurs <<<<<<<, =======, >>>>>>>.
  2. Marquez le fichier comme résolu : git add src/api.js. Inutile de committer.
  3. Continuez : git rebase --continue. Git applique le commit suivant.

Les autres commandes de pilotage :

CommandeEffet
git rebase --continueContinuer après la résolution du conflit
git rebase --skipSauter entièrement le commit courant (ses modifications seront perdues)
git rebase --abortInterrompre le rebase et remettre la branche dans son état initial

Attention à la confusion entre « notre » côté et « leur » côté. Pendant un rebase, HEAD est la base sur laquelle vous déplacez la branche, et les modifications « entrantes » sont vos propres commits. Ainsi, git checkout --ours pendant un rebase prend la version de main, et --theirs la vôtre. Avec merge, c'est l'inverse.

Conflits récurrents : rerere

Si vous rebasez régulièrement une branche longue et résolvez encore et encore le même conflit, activez git config --global rerere.enabled true. Git mémorisera votre résolution et l'appliquera tout seul la fois suivante.

Le rebase interactif : retoucher l'historique avant publication

Le plus précieux dans rebase, c'est le mode interactif. Il permet de remettre de l'ordre dans vos commits avant de les soumettre à la revue : regrouper « fix », « fix typo », « encore un fix » en un seul commit cohérent, réécrire les messages, changer l'ordre ou supprimer le superflu.

git rebase -i HEAD~4        # les 4 derniers commits
git rebase -i origin/main   # tous les commits de la branche absents de main

Git ouvre un éditeur avec la liste des commits, du plus ancien au plus récent (à l'inverse de git log) :

pick 1a2b3c4 Ajouter le formulaire de connexion
pick 5d6e7f8 fix
pick 9a0b1c2 Validation de l'e-mail
pick 3d4e5f6 coquille dans la validation

# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash" but keep only the previous commit's log message
# d, drop = remove commit

Vous changez le mot en début de ligne, enregistrez et fermez l'éditeur — Git exécute le plan de haut en bas.

CommandeEffet
pickGarder le commit tel quel
rewordGarder les modifications mais réécrire le message
editS'arrêter sur le commit pour modifier son contenu (git commit --amend) ou le découper en plusieurs
squashFusionner dans le commit précédent, en combinant et éditant les messages
fixupFusionner dans le commit précédent en jetant le message de ce commit
dropSupprimer le commit et ses modifications

Déplacer des lignes change l'ordre des commits, supprimer une ligne équivaut à drop.

Git squash : comment fusionner plusieurs commits en un

Il n'existe pas de commande git squash dans Git. On peut « squasher » des commits de trois manières, et le rebase interactif est la plus souple.

Méthode 1. Le rebase interactif

Pour l'exemple ci-dessus, transformons quatre commits en deux commits propres :

pick 1a2b3c4 Ajouter le formulaire de connexion
fixup 5d6e7f8 fix
pick 9a0b1c2 Validation de l'e-mail
fixup 3d4e5f6 coquille dans la validation

Le premier commit du groupe doit rester en pick : squash et fixup fusionnent un commit dans le précédent, et le tout premier n'a rien dans quoi se fondre.

Méthode 2. Commits fixup et autosquash

Si vous savez d'avance quel commit vous corrigez, créez la correction déjà marquée :

git commit --fixup=1a2b3c4
git rebase -i --autosquash origin/main

Git place lui-même le commit fixup sous le bon commit et le marque fixup — il ne reste qu'à enregistrer le plan. Pour en faire le comportement par défaut : git config --global rebase.autoSquash true.

Méthode 3. Compresser toute la branche en un commit

git reset --soft $(git merge-base HEAD origin/main)
git commit -m "Connexion par e-mail avec validation"

reset --soft ramène le pointeur de branche au point de divergence mais laisse toutes les modifications dans l'index — il ne reste qu'à faire un seul commit. Autres options : git merge --squash lors de l'intégration dans la branche principale, ou le bouton « Squash and merge » d'une pull request.

Variantes utiles de rebase

git pull --rebase

Un pull classique fusionne la branche distante dans la vôtre et crée un commit de merge si vous avez committé en parallèle de vos collègues. git pull --rebase rejoue plutôt vos commits locaux au-dessus des commits distants récents. Pour en faire le comportement par défaut : git config --global pull.rebase true. C'est précisément la zone sûre du rebase — personne n'a encore vu vos commits locaux.

--onto : déplacer une partie de branche

Vous avez créé feature-b à partir de feature-a et voulez maintenant que feature-b parte directement de main, sans les commits de feature-a :

git rebase --onto main feature-a feature-b

Ça se lit ainsi : prendre les commits de feature-b absents de feature-a et les transplanter sur main.

--exec : vérifier chaque commit

git rebase -i --exec "npm test" origin/main

Après chaque commit appliqué, les tests sont lancés ; s'ils échouent, le rebase s'arrête sur le commit problématique. Pratique quand il est important que chaque commit de l'historique compile.

--update-refs : une pile de branches dépendantes

Si vous maintenez une chaîne de branches qui dépendent les unes des autres, l'option --update-refs (Git 2.38+) déplace aussi les branches intermédiaires lors du rebase de la branche du haut.

Comment annuler un rebase

Tant que le processus est en cours : git rebase --abort. Si le rebase est terminé et que le résultat ne vous plaît pas, les anciens commits n'ont pas disparu : le journal des références s'en souvient.

$ git reflog
e5f6a7b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e5f6a7b HEAD@{1}: rebase (pick): Validation de l'e-mail
...
c3d4e5f HEAD@{5}: commit: coquille dans la validation

$ git reset --hard HEAD@{5}

Trouvez dans git reflog l'entrée précédant le début du rebase et réinitialisez la branche dessus. Juste après un rebase, le raccourci fonctionne aussi : git reset --hard ORIG_HEAD. Le reflog est local et conserve par défaut les entrées environ 90 jours, inutile donc de paniquer après un rebase raté.

Erreurs typiques

  • Rebaser une branche partagée puis forcer le push — l'historique des collègues part dans tous les sens. Correction : se coordonner, restaurer la branche depuis le reflog de quelqu'un qui a la bonne version, et interdire le force push sur les branches protégées côté serveur.
  • Utiliser --force au lieu de --force-with-lease et écraser un commit qu'un collègue venait de pousser sur la même branche.
  • Un squash en première ligne du plan interactif — Git répond par l'erreur « cannot 'squash' without a previous commit ».
  • Lancer un rebase avec un répertoire de travail sale. Git refuse de démarrer. Committez vos modifications, mettez-les de côté avec git stash ou utilisez git rebase --autostash.
  • Résoudre le conflit puis faire git commit au lieu de git rebase --continue. Vous obtenez un commit en trop ; après --continue, vous pouvez le supprimer avec un nouveau rebase interactif.

En résumé

git rebase déplace vos commits sur une nouvelle base et les recrée, d'où un historique linéaire et propre. Le mode interactif transforme des commits brouillons en historique soigné : squash et fixup regroupent les corrections, reword corrige les messages, drop retire le superflu. Une seule règle essentielle : ne rebasez que ce que vous n'avez pas encore publié pour les autres, et après le rebase de votre branche distante, poussez-la avec --force-with-lease. Le rebase et le nettoyage de l'historique des commits sont abordés en pratique dans le cours Git.

Questions fréquentes

git rebase modifie-t-il le contenu du code ?

Sans conflit, le contenu final des fichiers est le même qu'avec un merge, mais les commits eux-mêmes sont recréés avec de nouveaux hashes et de nouveaux parents. Si vous avez choisi une autre version lors d'un conflit, les commits intermédiaires différeront des originaux.

Comment rebaser jusqu'au tout premier commit du dépôt ?

Utilisez git rebase -i --root : tous les commits, y compris le commit racine, figurent dans le plan. C'est rarement utile, par exemple pour réécrire le message du tout premier commit.

Le rebase conserve-t-il l'auteur et la date des commits ?

L'auteur et la date d'auteur sont conservés, tandis que le committer et la date de commit prennent les valeurs actuelles. L'option --committer-date-is-author-date garde la date de commit égale à la date d'auteur.

Peut-on rebaser des commits de merge ?

Par défaut, rebase aplatit l'historique et laisse tomber les commits de merge. Pour conserver la structure des fusions, utilisez git rebase --rebase-merges.

Comment savoir qu'un rebase est en cours ?

git status affiche interactive rebase in progress ou You are currently rebasing et indique les commandes disponibles. Beaucoup de shells et d'IDE l'affichent aussi dans l'invite.

Apprenez ce sujet en vidéo

Ce sujet est traité en détail avec de la pratique dans le cours « Git de zéro à développeur confirmé ».

Voir le cours