Aller au contenu
Développement medium

Réécrire l'historique Git

16 min de lecture

git commit --amend modifie le dernier commit, git rebase -i réorganise les N derniers commits, et git filter-repo nettoie tout l'historique. Réécrire l'historique produit des commits propres et logiques avant de les partager. Ce guide couvre les trois niveaux de réécriture.

Prérequis : Rebase fondamental et Staging interactif.

  • Modifier le dernier commit avec --amend (message ou contenu)
  • Réécrire plusieurs commits avec rebase -i : squash, fixup, reword, edit, drop
  • Supprimer un fichier de tout l'historique avec git filter-repo
  • Appliquer la règle d'or : ne jamais réécrire un historique déjà partagé

La forme la plus simple de réécriture. Modifie le message et/ou le contenu du dernier commit :

Le cas le plus courant : vous venez de committer et vous voyez la faute de frappe, ou vous avez oublié le préfixe Conventional Commits. Avec -m, Git remplace le message sans ouvrir l'éditeur.

Fenêtre de terminal
git commit --amend -m "fix: corriger le calcul de TVA"

Vérifiez le résultat, le SHA affiché n'est plus le même qu'avant :

Fenêtre de terminal
git log -1 --oneline

--amend ne sert pas qu'au message : tout ce qui se trouve dans l'index au moment de l'appel rejoint le commit précédent. C'est la façon propre de rattraper un fichier oublié sans polluer l'historique d'un commit « oups, fichier manquant ».

Fenêtre de terminal
git add fichier-oublie.py
git commit --amend --no-edit

--no-edit conserve le message original. Le commit amendé remplace l'ancien (nouveau SHA).

Corriger l'auteur devient nécessaire quand un commit part avec la mauvaise adresse e-mail, typiquement une adresse personnelle sur un dépôt professionnel. Attention à deux effets vérifiables avec git log, la date d'auteur d'origine est conservée, et vous devenez le committer du commit sans que cela apparaisse dans l'affichage par défaut.

Fenêtre de terminal
git commit --amend --author="Alice <alice@example.com>"

Contrôlez les deux identités d'un coup :

Fenêtre de terminal
git log -1 --format='auteur=%an <%ae> committer=%cn <%ce>'

Le rebase interactif permet de réorganiser, fusionner, renommer, éditer ou supprimer une série de commits.

HEAD~5 désigne le point de départ du rebase, pas le nombre de commits à modifier : Git rejoue tous les commits situés après cette référence, soit les 5 derniers. Si votre branche compte moins de commits que le chiffre indiqué, la commande échoue avec fatal: invalid upstream. Sur une branche partant de main, git rebase -i main évite le comptage.

Fenêtre de terminal
# Réécrire les 5 derniers commits
git rebase -i HEAD~5

Git ouvre l'éditeur avec une liste de commits (du plus ancien au plus récent) :

pick a1b2c3d feat: ajouter le module pricing
pick e4f5a6b fix: corriger la remise
pick d7c8b9a wip: debug
pick f0e1d2c feat: ajouter les tests
pick b3a4c5d fix: typo dans le README

Chaque ligne de la liste porte une commande que vous remplacez à la main. Deux d'entre elles se ressemblent et sont la source d'erreur numéro un : squash conserve les deux messages et vous fait ouvrir l'éditeur pour les combiner, alors que fixup jette le message du commit fusionné sans rien vous demander. Retenez aussi que la fusion se fait toujours avec la ligne au-dessus, jamais celle du dessous.

CommandeEffet
pick (p)Garder le commit tel quel
reword (r)Garder le commit, modifier le message
edit (e)S'arrêter pour modifier le commit (contenu + message)
squash (s)Fusionner avec le commit précédent (combiner les messages)
fixup (f)Fusionner avec le précédent (supprimer le message du fixup)
drop (d)Supprimer le commit

Vous pouvez aussi réordonner les lignes pour changer l'ordre des commits.

Voici le scénario type d'une branche de travail : cinq commits, dont un correctif immédiat, un commit de debug et une correction de faute de frappe. Comparez les deux listes ligne à ligne : seul le mot de la première colonne change, vous ne touchez ni à l'ordre des lignes ni aux SHA affichés par Git dans l'éditeur.

Historique initial :

pick a1b2c3d feat: ajouter le module pricing
pick e4f5a6b fix: corriger la remise
pick d7c8b9a wip: debug
pick f0e1d2c feat: ajouter les tests
pick b3a4c5d fix: typo dans le README

Réécriture souhaitée :

pick a1b2c3d feat: ajouter le module pricing
fixup e4f5a6b fix: corriger la remise
drop d7c8b9a wip: debug
pick f0e1d2c feat: ajouter les tests
fixup b3a4c5d fix: typo dans le README

Résultat : 2 commits propres au lieu de 5.

La première étape n'est pas décorative : elle vérifie que vous réécrivez bien des commits locaux. Si git log origin/main..HEAD ne renvoie rien, c'est que tout est déjà poussé et que la règle d'or s'applique. Gardez également en tête que git rebase --abort ramène la branche à son état de départ tant que le rebase n'est pas terminé, ce qui rend l'opération sans risque à l'essai.

  1. Vérifiez que vos commits ne sont pas encore poussés :

    Fenêtre de terminal
    git log origin/main..HEAD --oneline
  2. Lancez le rebase interactif :

    Fenêtre de terminal
    git rebase -i HEAD~5
  3. Modifiez les commandes (picksquash, fixup, drop...) dans l'éditeur, sauvegardez, fermez.

  4. Si squash : Git ouvre l'éditeur pour combiner les messages. Rédigez un message propre.

  5. Si conflit : résolvez puis continuez :

    Fenêtre de terminal
    git add fichier-resolu.py
    git rebase --continue
  6. Pour annuler à tout moment :

    Fenêtre de terminal
    git rebase --abort

edit stoppe le rebase pour vous laisser modifier un commit intermédiaire :

Fenêtre de terminal
# Le rebase s'arrête au commit marqué "edit"
# Modifiez les fichiers...
git add .
git commit --amend
git rebase --continue

Vous pouvez même découper un commit en deux :

Fenêtre de terminal
# Au point d'arrêt "edit"
git reset HEAD~1 # Annuler le commit (garder les fichiers)
git add partie-1.py
git commit -m "feat: partie 1"
git add partie-2.py
git commit -m "feat: partie 2"
git rebase --continue

Git peut placer automatiquement les commits de fixup au bon endroit :

Fenêtre de terminal
# Créer un commit de fixup lié au commit a1b2c3d
git commit --fixup=a1b2c3d
# Lancer le rebase avec autosquash
git rebase -i --autosquash HEAD~10

Le commit fixup! feat: ajouter le module pricing est automatiquement placé juste après et marqué fixup.

Depuis Git 2.32, --fixup accepte deux variantes utiles : --fixup=amend:<sha> pour modifier à la fois le contenu et le message du commit visé, et --fixup=reword:<sha> pour ne toucher que le message. Vérifiez la syntaxe disponible sur votre poste avec git commit -h.

Pour activer --autosquash par défaut :

Fenêtre de terminal
git config --global rebase.autoSquash true

Pour supprimer un fichier de tout l'historique (par exemple un secret commité par erreur), git filter-repo remplace l'ancien filter-branch (déprécié).

Cette opération n'est pas réversible côté serveur une fois le force push effectué. Le point le plus souvent oublié est le premier de la liste : tant que le secret n'est pas révoqué, réécrire l'historique ne protège rien, puisque la valeur circule déjà dans les clones, les caches de la forge et les journaux de CI. Traitez la révocation d'abord, le nettoyage ensuite.

  • Le secret exposé est-il déjà changé/révoqué ? (sinon, faites-le d'abord)
  • Tout le monde est-il prévenu et ses branches locales sauvegardées ?
  • Avez-vous une sauvegarde du dépôt original (git bundle create backup.bundle --all) ?
  • Le dépôt est-il cloné à neuf (« fresh clone ») (ne pas l'exécuter sur un dépôt avec des modifications locales) ?

git-filter-repo est un unique script Python, ce qui explique la pluralité des modes d'installation. Le paquet système reste préférable quand il existe : il évite d'installer dans l'environnement Python global, ce que pip refuse d'ailleurs par défaut sur les distributions récentes (erreur externally-managed-environment).

Fenêtre de terminal
# Debian/Ubuntu
sudo apt install git-filter-repo
# macOS
brew install git-filter-repo
# Sinon, dans un environnement Python isolé
pipx install git-filter-repo

Contrôlez que Git voit bien la sous-commande :

Fenêtre de terminal
git filter-repo --version

--path sélectionne ce sur quoi filter-repo travaille, et --invert-paths inverse la sélection : sans cette seconde option, la commande conserverait uniquement le fichier indiqué et supprimerait tout le reste. C'est l'erreur la plus coûteuse de cette page, et elle passe inaperçue jusqu'au premier git log.

Fenêtre de terminal
git filter-repo --invert-paths --path secrets/api-key.txt

Cette commande réécrit chaque commit pour supprimer le fichier. Tous les SHA changent.

Supprimer le fichier ne suffit pas quand le secret a été copié dans plusieurs fichiers, ou collé dans un message de commit. --replace-text parcourt le contenu de tous les blobs et substitue les motifs que vous décrivez, ligne par ligne, dans un fichier de règles. Chaque ligne accepte un préfixe literal: (valeur par défaut), glob: ou regex:, et la partie après ==> fixe le texte de remplacement. Sans ==>, filter-repo écrit ***REMOVED***.

Contenu de replacements.txt :

regex:AKIA[A-Z0-9]{16}==>REDACTED_AWS_KEY
literal:mon-mot-de-passe==>REDACTED
Fenêtre de terminal
git filter-repo --replace-text replacements.txt

BFG couvre un périmètre plus étroit que filter-repo, mais avec une ligne de commande plus directe pour les deux besoins les plus fréquents : supprimer des fichiers par nom et purger les gros blobs. Deux différences de comportement comptent. BFG ne modifie pas le contenu du dernier commit de la branche HEAD, qu'il considère comme l'état propre que vous venez de corriger à la main : si le secret y est encore, il survivra au nettoyage, sauf à passer --no-blob-protection. Il exige par ailleurs un Java Runtime en version 11 ou supérieure, ce qui le rend moins commode sur un runner de CI minimal. En contrepartie, il traite les très gros dépôts nettement plus vite.

Fenêtre de terminal
# Installer (nécessite Java)
# Télécharger depuis https://rtyley.github.io/bfg-repo-cleaner/
# Supprimer un fichier de tout l'historique
bfg --delete-files api-key.txt
# Supprimer les fichiers > 100 Mo
bfg --strip-blobs-bigger-than 100M
# Remplacer du texte
bfg --replace-text passwords.txt
# Nettoyer après BFG
git reflog expire --expire=now --all
git gc --prune=now --aggressive

Le bon réflexe consiste à choisir l'outil dont la portée est la plus étroite pour le besoin. Passer par filter-repo là où un --amend suffirait force toute l'équipe à recloner pour une faute de frappe. La colonne « Portée » est donc celle à lire en premier ; la colonne « Complexité » ne sert qu'à départager filter-repo et BFG une fois que vous savez qu'il faut réécrire tout l'historique.

OutilPortéeCas d'usageComplexité
--amendDernier commitMessage ou contenu oubliéFaible
rebase -iN derniers commitsSquash, reword, réordonnerMoyenne
filter-repoTout l'historiqueSupprimer secrets/gros fichiersÉlevée
BFGTout l'historiqueSupprimer fichiers/texteMoyenne

Les incidents de réécriture se ressemblent : un conflit qui revient, un dépôt qui refuse le push, ou un filter-repo qui s'arrête avant de commencer. Le point commun est qu'aucun n'est destructif tant que vous n'avez pas forcé le push. Retenez surtout la dernière ligne : --force-with-lease vérifie que la référence distante est bien celle que vous aviez récupérée, et refuse le push si un collègue a poussé entre-temps, là où --force écraserait son travail sans prévenir.

SymptômeCause probableSolution
CONFLICT pendant rebase -iCommits interdépendants réordonnésRésolvez chaque conflit, git rebase --continue
Rebase boucle sur le même conflitCommit conflictuel non résolugit rebase --abort et repensez l'ordre
Cannot rebase: You have unstaged changesWorking dir salegit stash avant le rebase
filter-repo refuse de s'exécuterN'est pas un clone « fresh »Ajoutez --force ou clonez le repo d'abord
push rejected après réécritureSHA ont changégit push --force-with-lease (jamais --force seul)
  • --amend : rapide pour le dernier commit
  • rebase -i : squash, fixup, reword, edit, drop, réordonner
  • --fixup + --autosquash automatisent le nettoyage
  • filter-repo : pour les nettoyages profonds (secrets, gros fichiers)
  • Règle d'or : ne réécrivez jamais des commits déjà partagés
  • Après réécriture : --force-with-lease (pas --force)

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn