Lancez git reset --hard sur un commit que votre équipe a déjà récupéré par pull, et votre historique local se met à diverger du leur. Lancez git revert sur une simple faute de frappe que vous n’avez pas encore poussée, et vous ajoutez un commit inutile au log. Git propose quatre commandes pour annuler un changement, restore, reset, commit --amend et revert, et elles ne sont pas interchangeables. Choisissez la mauvaise et elle ne fait rien, ou elle fait bien plus que ce que vous vouliez.
Le choix dépend d’un seul critère : où se trouve le changement en ce moment, dans le working tree, dans la staging area, dans un commit que vous seul possédez, ou dans un commit déjà poussé.
Où vit un changement dans Git : working tree, index, historique
Chaque commande d’annulation agit sur une ou plusieurs de ces trois couches :
- Working tree : les fichiers sur disque, tels que vous les modifiez.
- Index (la staging area) : l’instantané construit avec
git add, en attente de devenir le prochain commit. - Historique : les commits déjà enregistrés, accessibles depuis
HEAD.
Un changement commence dans le working tree, passe dans l’index avec git add, puis dans l’historique avec git commit. L’annuler revient à le faire reculer à travers ces mêmes couches. Plus il est allé loin, plus la commande doit être délibérée.
| |
Comment annuler des changements non indexés avec git restore
Vous avez modifié config.py, cassé le fichier, et voulez récupérer la version de Git :
| |
Cela remplace le fichier dans votre working tree par la version présente dans l’index, identique à HEAD sauf si vous avez déjà indexé quelque chose. La modification disparaît. Elle n’est pas mise de côté dans un stash, et aucune commande Git ne la fera revenir. Pour annuler toutes les modifications non indexées d’un coup :
| |
git restore remplace l’ancien git checkout -- <fichier>. Les deux fonctionnent encore, mais restore existe pour que « récupère ce fichier » ne partage plus la même commande que « change de branche ».
Comment désindexer un fichier sans perdre vos modifications
Vous avez fait git add sur trois fichiers, puis remarqué que l’un d’eux n’a rien à faire dans ce commit. La modification elle-même est correcte :
| |
migration.sql quitte l’index et redevient « modifié, non indexé ». Le fichier sur disque reste intact. C’est équivalent à l’ancien git reset HEAD -- migration.sql, et c’est le seul endroit où reset et restore se recoupent : reset avec un pathspec ne touche que l’index, exactement ce que fait restore --staged.
Comment annuler un commit avec git reset : soft, mixed et hard
git restore ne peut pas toucher à un commit. C’est le rôle de git reset, et le flag choisi détermine l’ampleur de l’annulation :
| Flag | Working tree | Index | Historique (HEAD) |
|---|---|---|---|
--soft | inchangé | inchangé | déplacé |
--mixed (par défaut) | inchangé | réinitialisé sur la cible | déplacé |
--hard | réinitialisé sur la cible | réinitialisé sur la cible | déplacé |
Les trois déplacent le pointeur de branche. La différence porte sur la quantité de travail qui recule avec lui.
--soft rembobine les commits mais laisse leurs changements indexés. C’est la façon la plus rapide de regrouper les trois derniers commits en un seul :
| |
--mixed est ce que vous obtenez avec un simple git reset HEAD~1. Il rembobine le commit et renvoie ses changements dans le working tree comme modifications non indexées, pour que vous puissiez les répartir en commits plus petits ou en jeter une partie.
--hard ne garde rien. Le diff disparaît, et le working tree se retrouve comme si le commit n’avait jamais existé :
| |
Aucune confirmation n’est demandée. Les commits dépassés restent accessibles via git reflog (voir plus bas), mais les modifications non commitées présentes dans votre working tree à cet instant disparaissent pour de bon : vérifiez git status avant. Et ne l’exécutez jamais sur des commits que quelqu’un d’autre a déjà récupérés par pull : faire reculer une branche partagée fait diverger votre historique et le leur sur ce qui s’est passé, le même problème que décrit la règle d’or du rebase.
Comment corriger le dernier commit avec git commit –amend
Si l’erreur se trouve dans le commit le plus récent, un reset est excessif. --amend remplace le commit en tête au lieu de reculer au-delà :
| |
Indexez d’autres changements au préalable et ils fusionnent dans le même commit, sur le même parent :
| |
Le commit amendé est un nouvel objet avec un nouveau hash, comme un commit rebasé. La règle est donc la même que pour le rebase : n’amendez que ce que vous n’avez pas encore poussé. Amendez un commit déjà présent sur une branche partagée, et chaque personne qui l’a récupéré doit réconcilier deux commits qui prétendent être le même changement.
Comment annuler un commit déjà poussé avec git revert
Une fois un commit sur une branche partagée, reset et --amend ne sont plus des options. Réécrire un historique que d’autres ont déjà récupéré, c’est exactement ce qui produit des branches divergentes et des commits dupliqués. git revert prend l’autre chemin : il laisse le commit fautif en place et ajoute un nouveau commit qui fait l’inverse.
| |
Git ouvre un éditeur pour le message du commit de revert, puis l’enregistre :
| |
L’historique contient désormais le commit original et son inverse. Rien n’a été réécrit, donc un simple git pull sur chaque autre clone récupère le correctif. Pour annuler une plage de commits en un seul commit :
| |
--no-commit (-n) applique l’inverse de chaque commit au working tree et à l’index sans créer de commit, pour que trois reverts aboutissent à un seul commit plutôt qu’à trois. Si un revert entre en conflit avec des changements effectués depuis, Git s’arrête comme en plein merge : résolvez les marqueurs de conflit, faites git add sur le fichier, puis lancez git revert --continue.
« Annuler un push » signifie donc : faites un revert du commit, puis poussez ce revert. Ne faites jamais de force-push d’un historique réécrit sur une branche que d’autres récupèrent par pull.
Comment récupérer après un mauvais reset avec git reflog
Après un reset --hard, les commits disparaissent de git log. Ils existent toujours. git reflog liste chaque position occupée par HEAD sur votre machine, y compris celle juste avant le reset :
| |
| |
git reset --hard HEAD@{1} vous replace sur 7d4e1b8. La durée de vie des entrées du reflog, et pourquoi le reflog reste local à votre machine, sont expliquées dans la section récupération de Git Rebase vs Merge : différences et quand les utiliser. Cela fonctionne de la même façon quelle que soit la commande qui a déplacé HEAD.
Le reflog vous ramène là où vous étiez. Il ne vous dira pas quel ancien commit était le dernier bon. Pour ça, git bisect réduit des centaines de commits à celui qui a introduit le bug.
Git reset vs revert vs restore : quelle commande utiliser
| Le changement est… | Commande |
|---|---|
| Modifié, non indexé, et vous voulez qu’il disparaisse | git restore <fichier> |
| Indexé, et vous voulez le désindexer (modification conservée) | git restore --staged <fichier> |
| Commité, non poussé, et c’est le dernier commit | git commit --amend |
| Commité, non poussé, et vous voulez reculer plus loin | git reset --soft / --mixed / --hard HEAD~n |
| Commité et déjà poussé ou partagé | git revert <commit> |
Perdu après un reset --hard que vous regrettez | git reflog + git reset --hard HEAD@{n} |
Pour tout ce qui est déjà commité, une seule question détermine la ligne à suivre : quelqu’un d’autre a-t-il déjà vu ce commit ? Si non, reset ou amend, et on passe à la suite. Si oui, revert. Cela coûte un commit de plus dans le log, et personne dans l’équipe n’a besoin de démêler un historique qui ne correspond plus au sien.