Git : annuler le dernier commit sans perdre ses modifications
Le commit est parti trop vite. Il manque un fichier, le message est bâclé, ou on s'aperçoit qu'on a validé sur la mauvaise branche. Git sait presque toujours revenir en arrière, mais la bonne commande dépend de deux questions :
- Le commit a-t-il déjà été poussé sur le dépôt distant ?
- Que veut-on garder : les modifications, l'index, ou rien du tout ?
Le commit n'est pas encore poussé
Juste corriger le message ou ajouter un fichier oublié
Pas besoin d'annuler : on modifie le dernier commit.
$ git add fichier_oublie.py
$ git commit --amend --no-edit
--no-edit garde le message existant. Pour le changer en même temps :
git commit --amend -m "Message corrigé"
Techniquement, --amend ne modifie pas le commit : il le remplace par un nouveau, avec un autre identifiant. C'est sans conséquence tant qu'il n'a pas été poussé.
Défaire le commit en gardant tout
Les trois variantes de git reset reculent la branche d'un commit. Elles diffèrent par ce qu'elles font des modifications :
git reset --soft HEAD~1 # modifications conservées ET toujours indexées (prêtes à recommiter)
git reset HEAD~1 # modifications conservées, mais retirées de l'index (--mixed, le défaut)
git reset --hard HEAD~1 # modifications PERDUES : retour exact à l'avant-dernier commit
| Commande | Fichiers modifiés | Index |
|---|---|---|
reset --soft |
conservés | conservé |
reset (--mixed) |
conservés | vidé |
reset --hard |
perdus | vidé |
Dans la grande majorité des cas, c'est --soft qu'on veut : on refait ensuite le commit proprement, éventuellement découpé en plusieurs.
HEAD~1 désigne le parent du commit courant. Pour remonter de trois commits : HEAD~3.
Cas particulier : le tout premier commit du dépôt
HEAD~1 n'existe pas encore, donc git reset échoue. On supprime alors la référence de la branche, et les fichiers restent dans l'index :
git update-ref -d HEAD
Le commit est déjà poussé
Réécrire un historique que d'autres ont peut-être récupéré, c'est leur préparer des conflits. La méthode sûre est d'ajouter un commit qui annule le précédent :
$ git revert HEAD
$ git push
L'historique garde la trace de l'erreur et de sa correction, ce qui est exactement ce qu'on attend d'un historique.
Si vous êtes seul sur la branche (une branche de travail personnelle, par exemple), vous pouvez réécrire puis forcer l'envoi :
git reset --soft HEAD~1
# … corrections, nouveau commit …
git push --force-with-lease
Préférez toujours --force-with-lease à --force : il refuse d'écraser le dépôt distant si quelqu'un y a poussé entre-temps.
Le filet de sécurité : git reflog
Un reset --hard de trop ? Tant que le ménage automatique de Git n'est pas passé (au moins 30 jours par défaut pour un commit abandonné), rien n'est vraiment perdu : le reflog garde la liste de toutes les positions successives de HEAD.
$ git reflog
3f2a9c1 HEAD@{0}: reset: moving to HEAD~1
8b7e4d2 HEAD@{1}: commit: Ajoute la page de contact
…
$ git reset --hard HEAD@{1}
Et le commit « perdu » est revenu.
Seule exception : des modifications qui n'avaient jamais été commitées ni indexées. Celles-là, reset --hard les efface pour de bon. D'où la règle : en cas de doute, --soft.