Git avancé : rebase et réécriture de l'historique
Cette rubrique prolonge les bases (Git : branches et Annuler et corriger) : elle suppose acquises les notions de commit, de branche et de dépôt distant. Les exemples utilisent des noms génériques.
Réécrire l’historique : le principe
Section intitulée « Réécrire l’historique : le principe »Un commit est immuable : « modifier » un commit en crée en réalité un nouveau, avec un nouvel identifiant, et déplace la branche. Les anciens commits restent quelque temps accessibles par le reflog.
| Outil | Effet |
|---|---|
git commit --amend |
Remplace le dernier commit |
git rebase |
Rejoue des commits sur une autre base |
git rebase -i |
Rejoue en les modifiant (réordonner, fusionner, éditer, supprimer) |
git cherry-pick |
Copie un ou plusieurs commits ailleurs |
git filter-repo |
Réécrit tout l’historique (voir Sécurité et configuration) |
Rebase : rejouer sa branche sur une nouvelle base
Section intitulée « Rebase : rejouer sa branche sur une nouvelle base »git switch fonctionnalitegit fetch origingit rebase origin/mainAvant : A---B---C main Après : A---B---C main \ \ D---E fonctionnalite D'---E' fonctionnaliteLe rebase reprend D et E, et les rejoue (D’, E’) au-dessus de C. Les identifiants changent, le contenu des modifications reste le même. Résultat : un historique linéaire, sans commit de fusion.
En cas de conflit :
git status # fichiers en conflit# corriger les fichiers, puisgit add fichiergit rebase --continuegit rebase --skip # ignorer ce commit (si devenu inutile)git rebase --abort # tout annuler, retour à l'état d'avantgit rebase --abort ramène exactement à l’état initial : l’utiliser sans hésiter en cas de doute.
Options utiles
Section intitulée « Options utiles »git rebase --autostash origin/main # met de côté les modifications non validées, puis les restauregit rebase --rebase-merges origin/main # conserve la structure des fusionsgit rebase --onto nouvelle-base ancienne-base branchegit pull --rebase # fetch puis rebase au lieu d'un mergegit config --global pull.rebase true # comportement par défaut de git pullgit config --global rebase.autoStash true--onto : déplacer une branche
Section intitulée « --onto : déplacer une branche »main ── A ── B \ C ── D (branche1) \ E ── F (branche2, démarrée depuis branche1)git rebase --onto main branche1 branche2 # branche2 repart de main, sans les commits de branche1Syntaxe : --onto <nouvelle base> <point de départ exclu> <branche>. Les commits de branche2 après branche1 sont rejoués sur main.
Rebase interactif
Section intitulée « Rebase interactif »git rebase -i HEAD~5 # les 5 derniers commitsgit rebase -i origin/main # tous les commits de la branche depuis mainGit ouvre un éditeur avec la liste des commits (du plus ancien au plus récent) :
pick a1b2c3d ajouter la page d'accueilpick d4e5f6a corriger une faute de frappepick 7a8b9c0 ajouter le formulaire de contactpick 1d2e3f4 corriger le formulaire
# Commandes :# p, pick = utiliser le commit# r, reword = utiliser le commit, mais modifier le message# e, edit = utiliser le commit, mais s'arrêter pour le modifier# s, squash = fusionner avec le commit précédent (en gardant les messages)# f, fixup = fusionner avec le commit précédent (en jetant ce message)# d, drop = supprimer le commit# x, exec = exécuter une commandeModifier les mots-clés (ou l’ordre des lignes), enregistrer et fermer : Git applique le plan.
| Commande | Usage |
|---|---|
pick |
Garder tel quel |
reword |
Corriger un message |
edit |
S’arrêter pour modifier le contenu (git commit --amend, puis git rebase --continue) |
squash |
Fusionner avec le précédent et combiner les messages |
fixup |
Fusionner avec le précédent et jeter le message |
drop |
Supprimer |
exec |
Lancer une commande après un commit (par exemple les tests) |
| Réordonner les lignes | Réordonner les commits |
Exemple pour nettoyer avant une demande de fusion :
pick a1b2c3d ajouter la page d'accueilfixup d4e5f6a corriger une faute de frappepick 7a8b9c0 ajouter le formulaire de contactfixup 1d2e3f4 corriger le formulaireRésultat : deux commits propres au lieu de quatre.
Vérifier chaque commit avec exec
Section intitulée « Vérifier chaque commit avec exec »git rebase -i origin/main --exec "npm test"Insère exec npm test après chaque commit : le rebase s’arrête au premier commit qui casse les tests. Excellent pour garder des commits atomiques qui compilent tous (voir Husky : commitlint).
fixup et autosquash : corriger un ancien commit
Section intitulée « fixup et autosquash : corriger un ancien commit »Workflow rapide pour corriger un commit antérieur sans ouvrir l’éditeur :
git add fichier-corrigegit commit --fixup a1b2c3d # crée « fixup! <message du commit visé> »git rebase -i --autosquash origin/main # place et marque automatiquement le fixupgit config --global rebase.autoSquash true # active autosquash par défaut--autosquash réordonne les commits fixup!/squash! à côté de leur cible. Il suffit de fermer l’éditeur. Variantes : git commit --squash <commit> (garde le message), git commit --fixup=amend:<commit> (modifier aussi le message), --fixup=reword:<commit>.
--amend : le dernier commit
Section intitulée « --amend : le dernier commit »git commit --amend # modifier message et contenu (après git add)git commit --amend --no-edit # ajouter des modifications, garder le messagegit commit --amend --reset-author # reprendre l'auteur courantgit commit --amend --date=nowCouper un commit en plusieurs
Section intitulée « Couper un commit en plusieurs »git rebase -i HEAD~3 # marquer le commit avec « edit »git reset HEAD^ # défait le commit mais garde les modifications dans le dossier de travailgit add -p # choisir les morceaux (« hunks ») du premier commitgit commit -m "premier morceau"git add -pgit commit -m "second morceau"git rebase --continuegit add -p permet de sélectionner des parties d’un fichier (touches y, n, s pour découper, e pour éditer).
Cherry-pick
Section intitulée « Cherry-pick »Copier un commit (ou plusieurs) sur la branche courante :
git cherry-pick a1b2c3dgit cherry-pick a1b2c3d..f4e5d6c # une plage (exclut a1b2c3d)git cherry-pick -x a1b2c3d # ajoute « (cherry picked from commit …) » au messagegit cherry-pick -n a1b2c3d # applique sans committergit cherry-pick -m 1 <commit-de-fusion> # pour un commit de fusion : choisir le parentgit cherry-pick --continue | --abort | --skipUsage typique : reporter une correction sur une branche de maintenance. Le commit copié a un autre identifiant : si on fusionne ensuite les deux branches, Git détecte en général que les changements sont déjà appliqués, mais cela peut produire des conflits ; éviter de cherry-picker puis fusionner la même chose.
Branches empilées : --update-refs
Section intitulée « Branches empilées : --update-refs »Quand plusieurs branches dépendent les unes des autres (a → b → c), rebaser la dernière déplace aussi les branches intermédiaires avec :
git rebase -i --update-refs origin/maingit config --global rebase.updateRefs trueGit ajoute des lignes update-ref refs/heads/a dans le plan : les branches a et b suivent le rebase de c. Pratique pour des demandes de fusion successives (une par étape).
Pousser après une réécriture
Section intitulée « Pousser après une réécriture »Après un rebase d’une branche déjà poussée, git push refuse (l’historique a divergé). La commande à utiliser :
git push --force-with-lease origin fonctionnalitegit push --force-with-lease --force-if-includes origin fonctionnalite--force-with-lease refuse d’écraser si quelqu’un a poussé entre-temps (il vérifie que la branche distante est celle qu’on connaît) : c’est la version sûre de --force. Ne jamais forcer sur main ou toute branche partagée ; configurer les branches protégées sur le serveur (Gitea : dépôts, accès et API).
Rebase ou merge ?
Section intitulée « Rebase ou merge ? »| Rebase | Merge | |
|---|---|---|
| Historique | Linéaire | Reflète les branches (commits de fusion) |
| Commits modifiés | Oui (nouveaux identifiants) | Non |
| Risque | Réécriture : à éviter sur du partagé | Aucun |
Lisibilité de git log |
Excellente | Plus bruyante |
| Conflits | À résoudre commit par commit | Une seule fois |
| Retour en arrière | Plus délicat | git revert -m 1 d’un commit de fusion |
Convention répandue : rebaser sa branche sur main avant de la proposer, puis fusionner dans main (par une demande de fusion, en --no-ff ou en squash : voir Fusion et conflits).
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
CONFLICT pendant le rebase |
Résoudre, git add, git rebase --continue ; --abort pour repartir |
error: cannot rebase: You have unstaged changes |
git stash, ou --autostash |
| Rebase interrompu, on ne sait plus où on en est | git status indique l’étape ; git rebase --abort |
! [rejected] … (non-fast-forward) après rebase |
Historique réécrit : git push --force-with-lease (branche personnelle seulement) |
| Des commits en double après un rebase | Rebase d’une branche déjà fusionnée en partie ; vérifier avec git log --oneline --graph |
| Un commit disparu après un rebase interactif | git reflog pour le retrouver (Reflog et récupération) |
git rebase -i ouvre un éditeur inconnu |
git config --global core.editor "nano" (ou vim, code --wait) |
| Un collègue a des conflits étranges après un push forcé | Historique réécrit sous ses pieds : git fetch puis git rebase --onto origin/branche <ancien-point> branche |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Les versions de Git diffèrent entre Ubuntu 24.04 et 26.04 (git --version) ; les options de cette page (--update-refs, --fixup=amend, --force-if-includes) existent dans les versions récentes. Si une option est inconnue, la version installée est trop ancienne : voir Git : installation.