Corriger d'anciens commits avec git history
git history dans Git 2.55 réécrit de vieux commits avec fixup, reword ou split, met à jour les branches empilées et propose un aperçu dry-run sûr.
La commande expérimentale git history réécrit un unique commit ancien — en y intégrant des modifications indexées, en remplaçant son message ou en le scindant en deux — et déplace chaque branche locale construite par-dessus, sans jamais lancer de rebase interactif.
Si vous maintenez plusieurs branches empilées les unes sur les autres, vous reconnaîtrez peut-être le scénario d’échec que cette commande vient remplacer : vous repérez une coquille trois commits plus bas, vous lancez un rebase -i, vous tombez sur un conflit dans un commit que vous n’aviez aucune intention de toucher, et vous voilà en plein rebase avec un HEAD détaché et une décision à prendre. git history est conçue pour vous éviter d’en arriver là. Ses sous-commandes partagent une même garantie : l’opération aboutit entièrement, ou bien s’interrompt avec une erreur sans rien modifier.
Cet article passe en revue les trois sous-commandes disponibles dans Git 2.55, avec les sorties correspondantes, présente l’option --dry-run pour prévisualiser une réécriture avant tout déplacement, et détaille les limites : pas d’historiques avec fusions, pas d’opérations conflictuelles, pas de hooks.
Points clés à retenir
- Dans Git 2.55,
git historypropose trois sous-commandes :fixupintègre des modifications indexées dans un commit ancien,rewordremplace son message, etsplitle divise en deux. - Chaque opération est atomique : si la réécriture devait produire un conflit à un endroit quelconque de l’historique rejoué, la commande s’interrompt sans rien modifier.
- Par défaut, toutes les branches locales descendant du commit réécrit sont mises à jour ;
--update-refs=headne déplace que le HEAD courant. --dry-runne déplace rien et affiche à la place les mises à jour de références destinées à être appliquées ultérieurement pargit update-ref, ce qui constitue la manière sûre de prévisualiser une réécriture.- La commande est expérimentale, n’exécute aucun hook et refuse les historiques contenant des fusions ; utilisez
git rebase --rebase-mergesdans ce cas.
Pourquoi la commande git history existe-t-elle ?
git history existe pour corriger un unique commit ancien sans le cérémonial ni les risques d’un rebase interactif. Le manuel officiel la positionne face à git rebase : moins de choix, et une commande plus simple à dégainer lorsque la modification est ciblée. Elle porte également l’étiquette « expérimental », ce qui signifie que ses options et son comportement peuvent encore évoluer d’une version à l’autre.
Deux propriétés justifient de s’y intéresser. D’abord l’atomicité : toute opération susceptible de se terminer par un conflit de fusion n’est tout simplement pas proposée. Il s’agit d’un choix délibéré, car la commande traite une réécriture comme un coup unique plutôt que comme une session que l’on déroule étape par étape. Il n’y a ni --continue, ni --abort, ni état à moitié terminé dont il faudrait se sortir. Ensuite la portée : par défaut, la commande met à jour toutes les branches locales pointant vers des descendants du commit réécrit, ce qui correspond exactement aux besoins d’un workflow à branches empilées.
La commande est arrivée en deux temps. reword et split sont apparues dans Git 2.54 ; Git 2.55 a ajouté fixup. Cela donne trois sous-commandes à partir de la version 2.55, celle que couvre cet article :
| Sous-commande (Git 2.55) | Rôle | Fonctionne dans un dépôt bare |
|---|---|---|
git history fixup | Intègre les modifications indexées dans un commit ancien | Non, elle lit l’index |
git history reword | Remplace le message d’un commit ancien | Oui |
git history split | Divise un commit en deux | Oui |
Attendez-vous à voir cette liste s’allonger. Le manuel de git history dans la branche de développement de Git documente déjà une quatrième sous-commande, drop, qui supprime un commit et rejoue ses descendants sur son parent. Vérifiez donc git history -h avec votre propre version avant de supposer que trois est le compte définitif.
fixup : intégrer des modifications indexées dans un commit ancien
git history fixup <commit> prend tout ce qui se trouve dans l’index et l’incorpore au commit cible. En interne, il s’agit d’une fusion à trois branches, avec HEAD, le commit cible et un arbre construit à partir de vos modifications indexées comme trois entrées. Le commit cible conserve son message et son auteur d’origine, sauf si vous demandez l’ouverture d’un éditeur avec --reedit-message. C’est l’équivalent moral d’un git commit --fixup suivi d’un git rebase --autosquash, condensé en une seule étape qui ne peut pas vous laisser en plan.
Supposons qu’un chargeur de configuration ait été validé deux commits plus tôt sans valeur par défaut, et qu’une branche de fonctionnalité soit empilée par-dessus :
$ git log --oneline --branches
c41f9e2 (feature/retries) add retry logic
7b2d8a0 (HEAD -> main) add http client
3e59c11 add config loader
$ git add src/config.js
$ git history fixup 3e59c11
$ git log --oneline --branches
f8a01d3 (feature/retries) add retry logic
92c6b7e (HEAD -> main) add http client
5d40e19 add config loader
Les trois empreintes ont changé, et main comme feature/retries pointent désormais vers l’historique réécrit. C’est le comportement par défaut --update-refs=branches à l’œuvre : chaque branche locale descendant de la cible est déplacée, et pas uniquement celle sur laquelle vous êtes positionné. Passez --update-refs=head pour ne déplacer que HEAD. Cela va plus loin que git rebase --update-refs, qui ne met à jour que les références pointant vers des commits situés à l’intérieur de la plage rebasée.
Avant de lancer cela sur une pile à laquelle vous tenez, ajoutez --dry-run. Aucune référence n’est déplacée. Vous obtenez à la place la liste imprimée des déplacements qui auraient été effectués, présentée de manière à pouvoir être transmise ultérieurement à git update-ref. Git écrit tout de même les nouveaux objets dont il a eu besoin, raison pour laquelle rejouer cette liste après coup ne pose normalement pas de problème. C’est la façon honnête de voir exactement quelles branches une réécriture va toucher avant de s’y engager.
Notez la différence de portée par rapport à l’amend : git commit --amend ne touche que HEAD, tandis que fixup atteint n’importe quel commit de votre historique linéaire. C’est aussi la seule sous-commande qui a besoin de votre index, d’où son incapacité à s’exécuter dans un dépôt bare, contrairement aux deux autres.
reword : remplacer le message d’un commit ancien
git history reword <commit> ne modifie qu’une seule chose dans le commit cible : son message. Tout le reste du commit est conservé, chaque descendant est rejoué par-dessus, et les références de branches suivent.
$ git history reword 7b2d8a0
Votre éditeur s’ouvre pré-rempli avec le message actuel, add http client. Enregistrez-en un meilleur et la pile est reconstruite :
$ git log --oneline --branches
3ba90cf (feature/retries) add retry logic
a1d27e8 (HEAD -> main) add http client with timeout handling
5d40e19 add config loader
Comme reword laisse l’index et l’arbre de travail intacts, la commande fonctionne parfaitement dans un dépôt bare. Vous pouvez également corriger le message d’une branche que vous n’avez pas extraite, sans perturber le travail en cours.
split : transformer un commit en deux
git history split <commit> vous fait parcourir, section par section (hunk par hunk), le diff introduit par ce commit. Tout ce que vous sélectionnez part dans un nouveau commit, inséré sous l’original en tant que nouveau parent. L’original conserve les sections que vous avez laissées de côté. Sélectionner toutes les sections, ou aucune, est rejeté : dans les deux cas, l’un des deux commits se retrouverait vide.
Prenons un commit qui mélangeait un limiteur de débit et du code de métriques sans rapport :
$ git history split 91b04c7
Répondez y aux sections relatives aux métriques et n à celles du limiteur. L’éditeur demande les deux messages de commit, la paternité reste celle de l’original, et le résultat est une paire propre :
$ git log --oneline
e7d3f21 (HEAD -> main) add rate limiter
b19c8a4 add request metrics
Un pathspec en fin de commande (git history split 91b04c7 -- src/metrics.js) restreint la scission aux fichiers que vous nommez. Tout ce qui ne figure pas dans cette liste reste en place, dans le commit d’origine. Comme reword, split opère uniquement sur le graphe de commits et fonctionne dans un dépôt bare.
Ce que git history ne fera pas
La section LIMITATIONS du manuel est brève et mérite d’être prise au pied de la lettre. Les commits de fusion sont hors du périmètre : si l’historique que vous souhaitez réécrire en contient, le manuel vous renvoie vers git rebase --rebase-merges. Tout ce qui pourrait aboutir à un conflit est refusé, une contrainte qui pourrait être assouplie si Git venait un jour à intégrer les conflits comme citoyens de première classe. Les hooks ne sont pas exécutés aujourd’hui non plus, et le manuel laisse la porte ouverte à une évolution sur ce point.
fixup gère un cas limite supplémentaire via --empty=(drop|keep|abort). Deux chemins mènent ici à un commit vide. Votre correctif indexé peut annuler entièrement le commit cible, ou bien un commit ultérieur peut déjà porter la modification que vous venez de repousser dans un ancêtre. drop est la valeur par défaut et supprime ces commits lors de la reconstruction de l’historique ; keep les conserve tels quels ; abort interrompt la commande avec une erreur plutôt que de décider à votre place. La suppression du commit racine n’est pas encore prise en charge.
Quand faut-il plutôt recourir à rebase ?
git history modifie un seul commit ; git rebase reste l’outil pour tout ce qui a une portée plus large. Rebase est la réponse lorsqu’une plage entière de commits doit reposer sur une nouvelle base, et le rebase interactif lorsque vous souhaitez traiter plusieurs commits en une seule session. Et si ce que vous cherchez réellement est un outil capable de transporter des états conflictuels à travers un rebase pour les résoudre plus tard, il s’agit du modèle de conflits de première classe de jj, que git history ne tente délibérément pas de reproduire.
Conclusion
Pour la modification d’historique la plus courante — corriger un commit sur lequel des branches sont empilées — git history remplace un rebase interactif risqué par une opération qui réussit intégralement ou refuse de s’exécuter. Choisissez une vraie pile de branches, lancez git history fixup <commit> --dry-run dessus et lisez les mises à jour de références qu’elle aurait effectuées. Cette expérience de cinq minutes vous dira si cette commande a sa place dans votre workflow quotidien. Gardez simplement l’étiquette « expérimental » à l’esprit pendant votre réflexion, car les options et le comportement peuvent encore changer d’une version à l’autre.
FAQ
Quelle est la différence entre git history fixup et git commit --fixup avec autosquash ?
git history fixup intègre les modifications indexées dans le commit cible en une seule étape atomique. git commit --fixup se contente d'enregistrer un commit fixup! qu'un git rebase --autosquash ultérieur devra écraser, et ce rebase interactif peut s'arrêter en cours de route sur des conflits. git history fixup déplace en outre, par défaut, toutes les branches locales descendantes, et s'interrompt sans rien modifier si la réécriture devait entraîner un conflit.
git history met-elle à jour les branches distantes ou les commits que j'ai déjà poussés ?
Non. git history ne met à jour que les branches locales ; les références de suivi distant ne sont pas touchées. Comme les commits réécrits reçoivent de nouvelles empreintes, les commits déjà poussés nécessitent un push forcé (git push --force-with-lease) et une coordination avec toute personne ayant récupéré l'ancien historique, comme pour n'importe quelle réécriture. La commande est plus sûre sur des commits qui n'ont pas encore été poussés, ou sur des branches sur lesquelles vous êtes seul à travailler.
Comment annuler une réécriture effectuée par git history ?
Utilisez le reflog. git history déplace les références de branches vers de nouveaux commits, mais les commits d'origine restent présents dans le dépôt, et dans un dépôt normal (non bare) chaque branche déplacée consigne la mise à jour dans son reflog. Lancez git reflog show avec le nom de la branche pour retrouver l'empreinte antérieure à la réécriture, puis restaurez-la avec git reset --hard sur une branche extraite, ou avec git update-ref dans les autres cas.
git history peut-elle supprimer entièrement un commit ?
Pas dans Git 2.55. Ses trois sous-commandes (fixup, reword, split) modifient ou divisent un commit mais ne peuvent pas en supprimer un. Pour supprimer un commit aujourd'hui, utilisez git rebase -i et marquez le commit avec 'drop', ou git rebase --onto pour le contourner. Une sous-commande drop figure dans la documentation en cours de développement de git history, une option native pourrait donc arriver dans une prochaine version.