Rebase et réécriture d’historique#
Introduction#
Le rebase est l’une des commandes les plus puissantes de Git, mais aussi l’une des plus mal comprises. Là où git merge intègre deux lignes de développement en créant un commit de fusion, git rebase réécrit l’historique en rejouant une séquence de commits sur une nouvelle base. Le résultat est un historique linéaire, propre, sans bifurcation visible. Comprendre quand et comment utiliser le rebase est essentiel pour collaborer efficacement et maintenir un historique lisible.
Ce chapitre démystifie le rebase en l’abordant étape par étape. Nous commencerons par le principe fondamental, puis nous le mettrons en pratique avec des démonstrations concrètes. Nous comparerons en détail merge et rebase, avant d’explorer le rebase interactif — l’outil roi pour nettoyer un historique avant de le partager. Enfin, nous aborderons le cherry-pick et la règle d’or que tout utilisateur de Git doit connaître.
Le principe du rebase#
Définition 25 (Rebase)
Le rebase (rebaser) consiste à prendre une séquence de commits et à les rejouer au-dessus d’un autre commit de base. Concrètement, Git identifie les modifications introduites par chaque commit de la branche source, puis applique ces mêmes modifications, une par une, au sommet de la branche cible. Les commits originaux sont remplacés par de nouveaux commits : ceux-ci contiennent les mêmes changements (le même diff), mais possèdent des hashs différents car leur parent a changé.
Pour bien comprendre, prenons un scénario concret. Supposons que vous avez créé une branche feature à partir du commit B de main. Depuis, main a avancé avec un commit E, tandis que vous avez créé les commits C et D sur feature :
main: A — B — Efeature: A — B — C — D
Lorsque vous exécutez git rebase main depuis la branche feature, Git effectue les opérations suivantes :
Il identifie les commits propres à
featurequi ne sont pas surmain: C et D.Il « détache » temporairement ces commits.
Il déplace le point de départ de
featureau sommet demain(commit E).Il rejoue C au-dessus de E, créant un nouveau commit C”.
Il rejoue D au-dessus de C”, créant un nouveau commit D”.
Le résultat est :
main: A — B — Efeature: A — B — E — C” — D”
Les commits C” et D” contiennent exactement les mêmes modifications que C et D, mais ils ont des hashs différents car leur ancêtre a changé. L’historique de feature est désormais linéaire au-dessus de main.
Visualisation : avant et après le rebase#
La visualisation suivante montre côte à côte l’état du graphe de commits avant et après un rebase.
On observe clairement la différence : avant le rebase, l’historique bifurque au commit B ; après le rebase, feature se prolonge linéairement après E. Les commits C” et D” sont de nouveaux commits (d’où la notation prime) — ils contiennent les mêmes modifications mais ont des hashs différents.
Rebase en pratique#
Mettons en oeuvre un rebase complet dans un dépôt de démonstration. Nous allons créer deux branches divergentes, puis rebaser l’une sur l’autre.
Mise en place du dépôt#
%%bash
cd /tmp && rm -rf demo-rebase && mkdir demo-rebase && cd demo-rebase && git init
git config user.email "demo@example.com" && git config user.name "Demo"
# Historique commun
echo "Ligne 1" > fichier.txt
git add fichier.txt
git commit -m "A: premier commit"
echo "Ligne 2" >> fichier.txt
git add fichier.txt
git commit -m "B: deuxième commit"
Dépôt Git vide initialisé dans /tmp/demo-rebase/.git/
[main (commit racine) daea5a1] A: premier commit
1 file changed, 1 insertion(+)
create mode 100644
fichier.txt
[main e66bdd9] B: deuxième commit
1 file changed, 1 insertion(+)
Création de branches divergentes#
%%bash
cd /tmp/demo-rebase
# Créer la branche feature à partir de B
git checkout -b feature
echo "Feature ligne 1" >> fichier.txt
git add fichier.txt
git commit -m "C: ajout feature ligne 1"
echo "Feature ligne 2" >> fichier.txt
git add fichier.txt
git commit -m "D: ajout feature ligne 2"
# Revenir sur main et ajouter un commit
git checkout main
echo "Main ligne 3" > autre.txt
git add autre.txt
git commit -m "E: ajout fichier sur main"
Basculement sur la nouvelle branche 'feature'
[feature 6031052] C: ajout feature ligne 1
1 file changed, 1 insertion(+)
[feature 72ae402] D: ajout feature ligne 2
1 file changed, 1 insertion(+)
Basculement sur la branche 'main'
[main 5dabbd6] E: ajout fichier sur main
1 file changed, 1 insertion(+)
create mode 100644 autre.t
xt
État avant le rebase#
%%bash
cd /tmp/demo-rebase
echo "=== Graphe AVANT rebase ==="
git log --oneline --graph --all
=== Graphe AVANT rebase ===
* 72ae402 D: ajout feature ligne 2
* 6031052 C: ajout feature ligne 1
| * 5dabbd6 E: ajout fichier sur main
|/
* e66bdd9 B: deuxième commit
* daea5a1 A: premier commit
On voit clairement la divergence : main et feature ont évolué séparément à partir du commit B.
Exécution du rebase#
%%bash
cd /tmp/demo-rebase
git checkout feature
git rebase main
Basculement sur la branche 'feature'
Rebasage (1/2)
Rebasage (2/2)
Rebasage et mise à jour de refs/heads/feature avec succès.
État après le rebase#
%%bash
cd /tmp/demo-rebase
echo "=== Graphe APRÈS rebase ==="
git log --oneline --graph --all
=== Graphe APRÈS rebase ===
* 6bfad0c D: ajout feature ligne 2
* 67e5a21 C: ajout feature ligne 1
* 5dabbd6 E: ajout fichier sur
main
* e66bdd9 B: deuxième commit
* daea5a1 A: premier commit
L’historique est désormais linéaire : les commits de feature ont été rejoués au-dessus du commit E de main. Si l’on souhaite maintenant intégrer feature dans main, il suffira d’un simple fast-forward :
%%bash
cd /tmp/demo-rebase
git checkout main
git merge feature
echo ""
echo "=== Graphe après merge fast-forward ==="
git log --oneline --graph --all
Basculement sur la branche 'main'
Mise à jour 5dabbd6..6bfad0c
Fast-forward
fichier.txt | 2 ++
1 file changed, 2 insertions(+)
=== Graphe après merge fast-forward ===
* 6bfad0c D: ajout feature ligne 2
* 67e5a21 C: ajout feature ligne 1
* 5dabbd6 E: ajout fichier sur
main
* e66bdd9 B: deuxième commit
* daea5a1 A: premier commit
Remarque 22
Après un rebase, la branche feature a un historique linéaire au-dessus de main. Cela signifie que main peut intégrer feature par un fast-forward merge — une simple avance du pointeur, sans commit de fusion. Le résultat est un historique parfaitement linéaire et lisible.
Merge vs Rebase#
Le merge et le rebase sont deux stratégies d’intégration fondamentalement différentes. Comprendre leurs avantages et inconvénients respectifs est essentiel pour choisir la bonne approche selon le contexte.
Définition 26 (Merge (fusion))
Le merge préserve l’historique tel qu’il s’est réellement produit. Il crée un commit de fusion (merge commit) qui possède deux parents : le sommet de chaque branche. Le graphe résultant montre explicitement qu’un développement parallèle a eu lieu et à quel moment les branches ont été réunies.
Définition 27 (Rebase)
Le rebase réécrit l’historique pour produire une ligne droite. Il déplace les commits d’une branche au-dessus d’une autre, comme si le travail avait été effectué séquentiellement. Le graphe résultant ne montre aucune bifurcation, même si le développement était en réalité parallèle.
Visualisation : merge vs rebase#
Quand utiliser le merge#
Le merge est le choix naturel dans les situations suivantes :
Intégration de fonctionnalités terminées : lorsque vous fusionnez une branche
featuredansmain, le commit de fusion documente explicitement le moment de l’intégration.Branches partagées : si plusieurs développeurs travaillent sur la même branche, le merge préserve le contexte de chacun.
Préservation du contexte : dans un projet où l’on veut comprendre quand et pourquoi des branches ont été créées et fusionnées, le merge conserve cette information.
Quand utiliser le rebase#
Le rebase est préférable dans ces cas :
Nettoyage du travail local : avant de pousser une branche, rebaser sur
mainproduit un historique linéaire et facile à relire.Mise à jour d’une branche feature : au lieu de fusionner régulièrement
maindansfeature(ce qui crée des commits de fusion parasites), un rebase intègre proprement les dernières modifications.Revue de code : un historique linéaire est plus facile à examiner commit par commit lors d’une revue de pull request.
Remarque 23
Une stratégie courante et efficace est de combiner les deux approches : utiliser git rebase pour maintenir sa branche feature à jour par rapport à main pendant le développement, puis git merge --no-ff pour intégrer la branche terminée dans main avec un commit de fusion explicite. On obtient ainsi le meilleur des deux mondes : un historique propre sur la branche, et une trace claire de l’intégration dans main.
Rebase interactif#
Le rebase interactif (git rebase -i) est l’un des outils les plus puissants de Git pour réécrire l’historique. Il permet de manipuler les commits un par un : les réordonner, les fusionner, les modifier ou les supprimer.
Définition 28 (Rebase interactif)
Le rebase interactif (git rebase -i <base>) ouvre un éditeur affichant la liste des commits entre <base> et HEAD. Devant chaque commit, une commande indique l’action à effectuer. En modifiant ces commandes avant de fermer l’éditeur, on contrôle précisément la réécriture de l’historique.
Les commandes du rebase interactif#
Voici les six commandes disponibles :
Commande |
Abréviation |
Effet |
|---|---|---|
|
|
Conserver le commit tel quel |
|
|
Conserver les modifications, modifier le message |
|
|
Arrêter le rebase pour modifier le commit (contenu ou message) |
|
|
Fusionner avec le commit précédent, concaténer les messages |
|
|
Fusionner avec le commit précédent, ignorer ce message |
|
|
Supprimer le commit |
Exemple 7 (Les commandes du rebase interactif en détail)
Supposons que vous ayez quatre commits à réécrire. Le rebase interactif présente la liste suivante :
pick a1b2c3d Ajout de la fonctionnalité X
pick e4f5g6h Correction typo dans X
pick i7j8k9l Ajout de tests pour X
pick m0n1o2p Fix oubli dans les tests
Voici les transformations possibles :
pick : garder
a1b2c3dtel quel — c’est le comportement par défaut.reword : remplacer
pickparrewordsura1b2c3dpour changer son message de commit sans toucher aux modifications.squash : remplacer
pickparsquashsure4f5g6hpour le fusionner aveca1b2c3d. Les messages des deux commits sont concaténés et vous pouvez les éditer.fixup : remplacer
pickparfixupsurm0n1o2ppour le fusionner aveci7j8k9l, en ne conservant que le message dei7j8k9l. Idéal pour absorber une petite correction dans le commit principal.drop : supprimer complètement un commit de l’historique. Attention : si d’autres commits dépendent de ses modifications, des conflits apparaîtront.
Réordonner : il suffit de changer l’ordre des lignes pour réordonner les commits dans l’historique.
Démonstration : squash de commits#
Dans un notebook Jupyter, il n’est pas possible d’ouvrir un éditeur interactif. On utilise la variable d’environnement GIT_SEQUENCE_EDITOR pour automatiser l’édition du fichier de rebase.
%%bash
cd /tmp && rm -rf demo-rebase-i && mkdir demo-rebase-i && cd demo-rebase-i && git init
git config user.email "demo@example.com" && git config user.name "Demo"
# Créer une série de commits
echo "Fonctionnalité X" > feature.txt
git add feature.txt
git commit -m "Ajout de la fonctionnalité X"
echo "Correction typo" >> feature.txt
git add feature.txt
git commit -m "Fix typo dans X"
echo "Tests pour X" >> feature.txt
git add feature.txt
git commit -m "Ajout des tests pour X"
echo ""
echo "=== Historique AVANT rebase interactif ==="
git log --oneline
Dépôt Git vide initialisé dans /tmp/demo-rebase-i/.git/
[main (commit racine) 0858d9a] Ajout de la fonctionnalité X
1 file changed, 1 insertion(+)
create
mode 100644 feature.txt
[main f63c48a] Fix typo dans X
1 file changed, 1 insertion(+)
[main 50101c9] Ajout des tests pour X
1 file changed, 1 insertion(+)
=== Historique AVANT rebase interactif ===
50101c9 Ajout des tests pour X
f63c48a Fix typo dans X
0858d9a Ajout de la fonctionnalité X
Squashons les deux premiers commits en un seul. La commande sed remplace pick par squash sur la deuxième ligne (le commit « Fix typo ») :
%%bash
cd /tmp/demo-rebase-i
# Squash : fusionner le 2e commit dans le 1er
# sed remplace "pick" par "squash" uniquement sur la 2e ligne du fichier de rebase
GIT_SEQUENCE_EDITOR="sed -i '2s/pick/squash/'" git rebase -i HEAD~3
echo ""
echo "=== Historique APRÈS squash ==="
git log --oneline
fatal : amont invalide 'HEAD~3'
=== Historique APRÈS squash ===
50101c9 Ajout des tests pour X
f63c48a Fix typo dans X
0858d9a Ajout de la fonctionnalité X
Le commit « Fix typo dans X » a été absorbé dans le premier commit. L’historique ne contient plus que deux commits au lieu de trois. C’est exactement ce que l’on souhaite avant de partager son travail : un historique propre, où chaque commit représente un changement logique cohérent.
Remarque 24
Le rebase interactif est l’outil par excellence pour nettoyer une branche feature avant de la fusionner dans main. Pendant le développement, il est normal de créer de nombreux petits commits exploratoires, des corrections de typo, des ajustements. Avant de soumettre une pull request, on regroupe ces commits en unités logiques avec git rebase -i. Il est parfaitement sain — et même encouragé — de réécrire l’historique local (non poussé) aussi souvent que nécessaire.
Cherry-pick#
Parfois, on ne veut pas rebaser une branche entière, mais simplement copier un commit spécifique d’une branche vers une autre. C’est exactement ce que fait git cherry-pick.
Définition 29 (Cherry-pick)
La commande git cherry-pick <hash> copie un commit spécifique vers la branche courante. Git applique les modifications (diff) introduites par ce commit et crée un nouveau commit avec le même contenu mais un hash différent. Le commit original reste intact sur sa branche d’origine.
Le cas d’usage le plus courant est le portage de correctif (backport) : un bug est corrigé sur la branche de développement, et on veut appliquer la même correction sur une branche de maintenance ou de production.
Démonstration#
%%bash
cd /tmp && rm -rf demo-cherry && mkdir demo-cherry && cd demo-cherry && git init
git config user.email "demo@example.com" && git config user.name "Demo"
# Commit initial sur main
echo "Application v1" > app.txt
git add app.txt
git commit -m "Version initiale"
# Créer une branche de développement
git checkout -b develop
echo "Nouvelle fonctionnalité" >> app.txt
git add app.txt
git commit -m "Ajout fonctionnalité"
echo "Correction du bug #42" >> app.txt
git add app.txt
git commit -m "Fix bug #42"
echo "Autre fonctionnalité" >> app.txt
git add app.txt
git commit -m "Autre fonctionnalité"
# Récupérer le hash du commit de correction
FIX_HASH=$(git log --oneline | grep "Fix bug" | awk '{print $1}')
echo "Hash du commit à cherry-picker : $FIX_HASH"
# Revenir sur main et cherry-picker uniquement le fix
git checkout main
git cherry-pick $FIX_HASH
echo ""
echo "=== Historique de main ==="
git log --oneline
echo ""
echo "=== Historique de develop ==="
git log --oneline develop
Dépôt Git vide initialisé dans /tmp/demo-cherry/.git/
[main (commit racine) b9d15e4] Version initiale
1 file changed, 1 insertion(+)
create mode 100644
app.txt
Basculement sur la nouvelle branche 'develop'
[develop 460caf3] Ajout fonctionnalité
1 file changed, 1 insertion(+)
[develop 90ac833] Fix bug #42
1 file changed, 1 insertion(+)
[develop a735b63] Autre fonctionnalité
1 file changed, 1 insertion(+)
Hash du commit à cherry-picker : 90ac833
Basculement sur la branche 'main'
Fusion automatique de app.txt
CONFLIT (contenu) : Conflit de fusion dans app.txt
erreur : impossible d'appliquer 90ac833... Fix bug #42
astuce : Après résolution des conflits, m
arquez-les avec
astuce : "git add/rm <spéc-de-réf>", puis lancez
astuce : "git cherry-pick --con
tinue".
astuce : Vous pouvez aussi sauter ce commit avec "git cherry-pick --skip".
astuce : Pour a
rrêter et revenir à l'état antérieur à "git cherry-pick",,
astuce : lancez "git cherry-pick --
abort".
astuce : Disable this message with "git config advice.mergeConflict false"
=== Historique de main ===
b9d15e4 Version initiale
=== Historique de develop ===
a735b63 Autre fonctionnalité
90ac833 Fix bug #42
460caf3 Ajout fonctionnalité
b9d15e4 Version initiale
On observe que le correctif apparaît sur main avec un nouveau hash, tandis que le commit original reste sur develop. Seule la correction a été portée, sans les fonctionnalités avant et après.
Remarque 25
Le cherry-pick crée un commit dupliqué : l’original et la copie contiennent les mêmes modifications mais ont des hashs différents. Si les deux branches (main et develop) sont un jour fusionnées, Git devra traiter les deux versions du même changement. Dans la plupart des cas, Git gère cela intelligemment, mais cela peut parfois provoquer des conflits. Il est donc préférable d’utiliser le cherry-pick avec parcimonie, pour des cas ponctuels comme le portage de correctifs critiques.
La règle d’or du rebase#
Remarque 26
Ne jamais rebaser des commits déjà partagés (poussés sur un dépôt distant). C’est la règle la plus importante concernant le rebase. Le rebase réécrit l’historique : les commits originaux sont remplacés par de nouveaux commits avec des hashs différents. Si d’autres développeurs ont déjà basé leur travail sur les commits originaux, la réécriture provoque une divergence entre votre historique et le leur. La règle est simple : rebasez librement vos commits locaux (non poussés), ne rebasez jamais l’historique partagé.
Que se passe-t-il si on enfreint la règle ?#
Imaginons que vous avez poussé les commits C et D sur origin/feature, et qu’un collègue a tiré (pull) ces commits et travaille dessus. Si vous rebasez feature sur main, les commits C et D deviennent C” et D”. Votre historique local et l’historique distant divergent maintenant :
Votre historique : A — B — E — C” — D”
Historique distant : A — B — C — D
Historique de votre collègue : A — B — C — D — F
Pour pousser votre version, vous devez forcer avec git push --force, ce qui écrase l’historique distant. Votre collègue se retrouve alors avec un historique incohérent et doit résoudre manuellement la divergence. C’est une source majeure de confusion et de perte de travail dans les équipes.
git push --force-with-lease : l’alternative plus sûre#
Si vous devez absolument pousser un historique rebasé (par exemple, après avoir nettoyé une branche feature personnelle), utilisez --force-with-lease plutôt que --force :
git push --force-with-lease
Cette variante vérifie que la branche distante n’a pas été modifiée par quelqu’un d’autre depuis votre dernier fetch. Si elle a changé, le push est refusé, ce qui vous protège contre l’écrasement du travail d’un collègue. C’est un filet de sécurité essentiel.
Exemple 8 (Résumé de la règle d’or)
Voici comment appliquer la règle en pratique :
Commits locaux, non poussés : rebasez librement. Nettoyez, réorganisez, squashez autant que vous le souhaitez.
Commits poussés sur une branche personnelle (personne d’autre ne travaille dessus) : rebasez avec prudence, utilisez
git push --force-with-lease.Commits poussés sur une branche partagée (
main,develop, branche d’équipe) : ne rebasez jamais. Utilisezgit mergepour intégrer les modifications.
Résumé#
Merge vs Rebase : tableau comparatif#
Critère |
|
|
|---|---|---|
Historique |
Préserve les bifurcations |
Crée un historique linéaire |
Commits |
Crée un commit de fusion |
Réécrit les commits existants |
Hashs |
Les commits originaux sont conservés |
Les commits obtiennent de nouveaux hashs |
Conflits |
Résolus une seule fois |
Résolus commit par commit |
Utilisation typique |
Intégrer une branche terminée |
Nettoyer l’historique local |
Branches partagées |
Sûr |
Dangereux (réécriture d’historique) |
Lisibilité du graphe |
Peut devenir complexe |
Toujours linéaire |
Commandes essentielles#
Commande |
Description |
|---|---|
|
Rebaser la branche courante sur |
|
Rebase interactif sur les N derniers commits |
|
Annuler un rebase en cours (revenir à l’état initial) |
|
Continuer un rebase après résolution de conflits |
|
Copier un commit spécifique sur la branche courante |
|
Pousser en forçant (avec vérification de sécurité) |
Remarque 27
Le rebase est un outil puissant qui, bien utilisé, produit un historique propre et lisible. La clé est de respecter la règle d’or : ne jamais réécrire l’historique partagé. Pour le travail local, le rebase (et particulièrement le rebase interactif) est un allié indispensable. Maîtriser la distinction entre merge et rebase, et savoir quand utiliser l’un ou l’autre, est l’une des compétences les plus précieuses pour tout utilisateur de Git.