Aller au contenu

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.

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)
Fenêtre de terminal
git switch fonctionnalite
git fetch origin
git rebase origin/main
Avant : A---B---C main Après : A---B---C main
\ \
D---E fonctionnalite D'---E' fonctionnalite

Le 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 :

Fenêtre de terminal
git status # fichiers en conflit
# corriger les fichiers, puis
git add fichier
git rebase --continue
git rebase --skip # ignorer ce commit (si devenu inutile)
git rebase --abort # tout annuler, retour à l'état d'avant

git rebase --abort ramène exactement à l’état initial : l’utiliser sans hésiter en cas de doute.

Fenêtre de terminal
git rebase --autostash origin/main # met de côté les modifications non validées, puis les restaure
git rebase --rebase-merges origin/main # conserve la structure des fusions
git rebase --onto nouvelle-base ancienne-base branche
git pull --rebase # fetch puis rebase au lieu d'un merge
git config --global pull.rebase true # comportement par défaut de git pull
git config --global rebase.autoStash true
main ── A ── B
\
C ── D (branche1)
\
E ── F (branche2, démarrée depuis branche1)
Fenêtre de terminal
git rebase --onto main branche1 branche2 # branche2 repart de main, sans les commits de branche1

Syntaxe : --onto <nouvelle base> <point de départ exclu> <branche>. Les commits de branche2 après branche1 sont rejoués sur main.

Fenêtre de terminal
git rebase -i HEAD~5 # les 5 derniers commits
git rebase -i origin/main # tous les commits de la branche depuis main

Git ouvre un éditeur avec la liste des commits (du plus ancien au plus récent) :

pick a1b2c3d ajouter la page d'accueil
pick d4e5f6a corriger une faute de frappe
pick 7a8b9c0 ajouter le formulaire de contact
pick 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 commande

Modifier 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'accueil
fixup d4e5f6a corriger une faute de frappe
pick 7a8b9c0 ajouter le formulaire de contact
fixup 1d2e3f4 corriger le formulaire

Résultat : deux commits propres au lieu de quatre.

Fenêtre de terminal
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).

Workflow rapide pour corriger un commit antérieur sans ouvrir l’éditeur :

Fenêtre de terminal
git add fichier-corrige
git commit --fixup a1b2c3d # crée « fixup! <message du commit visé> »
git rebase -i --autosquash origin/main # place et marque automatiquement le fixup
git 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>.

Fenêtre de terminal
git commit --amend # modifier message et contenu (après git add)
git commit --amend --no-edit # ajouter des modifications, garder le message
git commit --amend --reset-author # reprendre l'auteur courant
git commit --amend --date=now
Fenêtre de terminal
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 travail
git add -p # choisir les morceaux (« hunks ») du premier commit
git commit -m "premier morceau"
git add -p
git commit -m "second morceau"
git rebase --continue

git add -p permet de sélectionner des parties d’un fichier (touches y, n, s pour découper, e pour éditer).

Copier un commit (ou plusieurs) sur la branche courante :

Fenêtre de terminal
git cherry-pick a1b2c3d
git cherry-pick a1b2c3d..f4e5d6c # une plage (exclut a1b2c3d)
git cherry-pick -x a1b2c3d # ajoute « (cherry picked from commit …) » au message
git cherry-pick -n a1b2c3d # applique sans committer
git cherry-pick -m 1 <commit-de-fusion> # pour un commit de fusion : choisir le parent
git cherry-pick --continue | --abort | --skip

Usage 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.

Quand plusieurs branches dépendent les unes des autres (a → b → c), rebaser la dernière déplace aussi les branches intermédiaires avec :

Fenêtre de terminal
git rebase -i --update-refs origin/main
git config --global rebase.updateRefs true

Git 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).

Après un rebase d’une branche déjà poussée, git push refuse (l’historique a divergé). La commande à utiliser :

Fenêtre de terminal
git push --force-with-lease origin fonctionnalite
git 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 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).

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.