Réécrire l'historique en toute sécurité
Chaque réécriture – commit --amend, reset, rebase, filter-repo – produit des commits nouveaux et déplace le pointeur de branche des anciens. Cela est inoffensif tant que personne d'autre ne possède les anciens commits, et destructeur dès que c'est le cas : leur clone conserve encore l'ancien historique, et la prochaine fois qu'ils poussent ou tirent, il revient sous forme de doublons ou de conflits. La première question n'est donc jamais « comment », mais « qui d'autre possède ces commits ? »
1. Décider : réécrire ou révoquer
git fetch --prune
git status --short --branch # en avance/en retard, et état modifié
git branch -r --contains <commit> # quelles branches distantes le contiennent déjà
git log --oneline @{u}..HEAD # commits qui n'existent que localement
| Où vit le commit | Faites ceci |
|---|---|
| Local uniquement, jamais poussé | Réécrivez librement. |
| Poussé sur une branche que seul cet utilisateur utilise (sa branche PR) | Réécrivez, puis poussez avec un lease (étape 5). |
Sur une branche partagée ou protégée : main, master, develop, release/*, ou tout ce qui a d'autres commits au-dessus | Ne réécrivez pas. Utilisez git revert. |
| Déjà publié ou déployé | git revert. Une réécriture ne peut pas annuler le déploiement. |
Quand l'utilisateur dit « supprime ce commit de main », il veut généralement que son effet disparaisse, pas son SHA. git revert <sha> fait cela, maintient l'historique honnête, et ne nécessite qu'un push normal. Pour un commit de merge, git revert -m 1 <merge-sha>. Expliquez pourquoi vous avez choisi revert, et si l'utilisateur insiste pour l'effacer d'une branche partagée, nommez qui cela affectera et obtenez un accord explicite d'abord.
Un revert entre en conflit lorsque des commits ultérieurs ont modifié les mêmes lignes – c'est exactement le cas où des commits ont été construits au-dessus du mauvais. Résolvez comme tout conflit (voir git-conflict-resolution) : supprimez la mauvaise modification, conservez le travail ultérieur, puis git revert --continue.
Un piège du revert : après avoir révoqué un merge, fusionner à nouveau la même branche ne ramène rien, car git pense que ces commits ont déjà atterri. Pour rétablir le travail, révoquez d'abord le revert.
2. Prendre un point de récupération avant de toucher à quoi que ce soit
git rev-parse HEAD # notez-le
git branch backup/<branche>-pre-rewrite # une vraie référence, pas seulement le reflog
Le reflog enregistre aussi où se trouvait la branche, mais il est local à ce clone, expire (90 jours pour les entrées atteignables, 30 pour les inatteignables par défaut), et git gc après reflog expire y met fin. Une branche de sauvegarde nommée ne coûte rien. Laissez-la en place jusqu'à ce que l'utilisateur ait confirmé le résultat.
3. Choisir la commande la moins destructrice
| Objectif | Commande | Remarques |
|---|---|---|
| Corriger le message du dernier commit ou ajouter un fichier oublié | git commit --amend | -m "..." ou --no-edit évite d'ouvrir un éditeur. |
| Annuler le commit mais garder les modifications indexées | git reset --soft HEAD~1 | |
| Annuler le commit et désindexer, garder les fichiers | git reset --mixed HEAD~1 (par défaut) | |
| Reculer une branche, refuser de perdre les modifications locales | git reset --keep <commit> | Abandonne si cela écraserait des modifications non commitées. Préférez ceci à --hard. |
| Jeter aussi le travail non commité | git reset --hard | Le travail non commité n'est pas dans le reflog et ne peut pas être récupéré. Après avoir confirmé que rien n'est souhaité ; git stash -u d'abord si incertain. |
| Annuler les modifications d'un seul fichier | git restore <fichier> | |
| Squasher ou fusionner des correctifs dans des commits antérieurs | git commit --fixup=<sha> puis autosquash | Voir ci-dessous. |
| Supprimer ou réorganiser des commits | rebase -i avec un todo fourni | Voir ci-dessous. |
Un hook pré-commit échoué signifie qu'aucun commit n'a été fait. Exécuter git commit --amend pour « réessayer » modifie alors le commit précédent, fusionnant les nouvelles modifications dans le mauvais. Après un échec de hook, corrigez le problème, ré-indexez, et faites un nouveau commit normal.
Rebase interactif sans éditeur interactif
git rebase -i ouvre un éditeur, qui bloque dans un shell non interactif. Ne vous y fiez pas. Deux méthodes fonctionnent :
# Fusionner les commits de correction dans leurs cibles - accepter le todo que git génère :
git commit --fixup=<sha-cible>
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>
# Supprimer ou réorganiser : écrire le todo vous-même.
GIT_SEQUENCE_EDITOR="sed -i '/^pick <short-sha> /d'" git rebase -i <base>
<base> est le commit avant le premier que vous voulez modifier. Si un rebase s'arrête sur un conflit, résolvez-le, git add, puis GIT_EDITOR=true git rebase --continue. Si ça tourne mal, git rebase --abort revient au point de départ.
4. Vérifier que la réécriture a bien fait ce que vous vouliez
Une édition de message, une réorganisation ou un squash devraient laisser l'arbre final identique :
git diff backup/<branche>-pre-rewrite HEAD --stat # vide = même contenu
git range-diff backup/<branche>-pre-rewrite...HEAD # comparaison commit par commit
Si vous vouliez supprimer du contenu, le diff devrait montrer exactement ce contenu et rien d'autre. Tout ce qui est inattendu signifie restaurer depuis la sauvegarde et recommencer.
5. Publier – seulement avec l'accord de l'utilisateur
Pousser une réécriture est l'étape tournée vers l'extérieur, difficile à annuler. Confirmez d'abord, sauf si l'utilisateur a déjà demandé le push.
git push --force-with-lease=<branche>:<sha-distant-attendu> --force-if-includes origin <branche>
- Jamais de
--forcenu. Il écrase tout ce qui est sur le distant, y compris un push d'un collègue que vous n'avez pas vu. - Nommez le SHA attendu. Un simple
--force-with-leasecompare avec votre ref de suivi distant, et ungit fetchen arrière-plan de l'IDE met tranquillement à jour cette ref, donc le lease ne protège alors rien. Utilisez le SHA que vous avez vu à l'étape 1 (git rev-parse origin/<branche>). --force-if-includes(git 2.30+) refuse en plus si le distant s'est déplacé vers quelque chose que votre branche locale n'a jamais intégré.- Ne forcez jamais le push sur
main/master/develop/release/*. Si une règle d'hébergement le bloque, cette règle est correcte ; ne la contournez pas.
Après avoir poussé, dites à l'utilisateur que quiconque a l'ancienne branche extraite doit réinitialiser vers la nouvelle plutôt que de tirer, et que les commentaires de révision d'une PR ouverte peuvent perdre leurs ancres.
Garde-fous
- Les commits signés perdent leurs signatures lors de la réécriture ; re-signez avec
-Sou informez l'utilisateur. - Ne réécrivez pas les commits d'un autre auteur sans demander, même sur une branche de fonctionnalité.
- Les branches empilées au-dessus de celle réécrite sont maintenant obsolètes. Utilisez
git-stacked-pr-rebasepour les ré-empiler plutôt que de rebaser chacune à la main. - Un secret commité n'est pas corrigé par une réécriture. Ruptez d'abord l'identifiant ; supposez-le compromis.
- Rapportez clairement : quel était l'ancien sommet, quel est le nouveau, où se trouve la ref de sauvegarde, et si quelque chose n'est pas poussé. Si revert était la bonne décision, dites que c'est ce que vous avez fait.