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 :

  1. Il calcule ce que chacun de vos commits a modifié — la différence entre le commit et son parent.
  2. Il place votre branche sur le nouveau point de départ, ici C.
  3. Il applique la première modification, et crée un commit neuf : D'.
  4. 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 — comme squash, 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 main pour 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 :

  1. Le rebase rejoue vos modifications sur une nouvelle base. Il ne déplace rien, il recrée.
  2. Comme il recrée, les identifiants changent — d'où la règle d'or sur les branches partagées.
  3. 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