Chapitre 417 minDébutant
Annuler en Git : restore, reset, revert et reflog
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- choisir la bonne annulation selon que le travail est commité ou poussé ;
- jeter des modifications avec
git restoreet corriger le dernier commit avec--amend; - faire reculer une branche avec
git reset, ou annuler un commit poussé avecgit revert; - retrouver un commit perdu avec
git refloget mettre ton travail de côté avecgit stash.
4 exercices corrigés1 schéma
Annuler en Git dépend de l'endroit où se trouve l'erreur : git restore annule une modification pas encore commitée, git commit --amend corrige le dernier commit, git reset supprime des commits qui n'ont pas été poussés, et git revert annule un commit déjà partagé en créant un commit inverse. Ce chapitre donne la bonne commande pour chaque situation, explique les trois modes de git reset et montre comment git reflog récupère presque tout, même après une fausse manœuvre. Il se termine par git stash, qui met ton travail de côté sans commit.
La question à se poser d'abord
Avant de taper une commande d'annulation, situe ton erreur. Elle est forcément dans l'un de ces quatre états :
| Situation | Commande | Ce qui se passe |
|---|---|---|
| Modification non préparée, à jeter | git restore fichier | Le fichier revient à l'état de la zone de préparation, ou du dernier commit |
| Modification préparée par erreur | git restore --staged fichier | Le fichier sort de la zone de préparation, la modification reste sur le disque |
| Dernier commit à corriger (message, fichier oublié), non poussé | git commit --amend | Le dernier commit est remplacé |
| Derniers commits à défaire, non poussés | git reset | La branche recule ; selon le mode, les modifications sont gardées ou jetées |
| Commit déjà poussé à annuler | git revert <commit> | Un nouveau commit annule l'ancien ; l'historique partagé n'est pas réécrit |
| Commit perdu après une fausse manœuvre | git reflog puis git reset ou git branch | Git retrouve l'ancien état |
Jeter des modifications non commitées avec git restore
Tu as modifié app.js, rien ne marche, et tu veux revenir à la version du dernier commit :
git restore app.js
Les variantes :
| Commande | Effet |
|---|---|
git restore app.js | Remet le fichier dans l'état de la zone de préparation (ou du dernier commit s'il n'est pas préparé) |
git restore . | Fait de même pour tous les fichiers modifiés du dossier courant |
git restore --staged app.js | Retire le fichier de la zone de préparation ; le contenu sur le disque ne change pas |
git restore --staged --worktree app.js | Les deux : le fichier redevient identique au dernier commit |
git restore --source=HEAD~3 app.js | Ramène le fichier tel qu'il était trois commits plus tôt |
git restore ne touche pas aux fichiers non suivis. Pour supprimer les fichiers non suivis d'un dossier (des fichiers générés, par exemple), git clean -n affiche ce qui serait supprimé, puis git clean -f supprime. Avec -d, les dossiers non suivis aussi.
git switch et git restore remplacent git checkout
Dans des tutoriels plus anciens, tu croiseras git checkout partout. Cette commande faisait deux métiers très différents : changer de branche et écraser des fichiers. Une faute de frappe dans un nom de branche pouvait transformer un changement de branche en perte de modifications. Depuis la version 2.23 de Git (2019), deux commandes séparent ces métiers : git switch pour les branches, git restore pour les fichiers. git checkout fonctionne toujours, mais utilise les nouvelles commandes.
| Ancienne commande | Nouvelle commande | Effet |
|---|---|---|
git checkout feature/export | git switch feature/export | Changer de branche |
git checkout -b feature/export | git switch -c feature/export | Créer une branche et y aller |
git checkout - | git switch - | Revenir à la branche précédente |
git checkout a1b2c3d | git switch --detach a1b2c3d | Aller sur un ancien commit, hors de toute branche |
git checkout -- app.js | git restore app.js | Jeter les modifications d'un fichier |
git reset HEAD app.js | git restore --staged app.js | Retirer un fichier de la zone de préparation |
git checkout a1b2c3d -- app.js | git restore --source=a1b2c3d app.js | Récupérer un fichier d'un ancien commit (dans le répertoire de travail seulement ; checkout le préparait aussi) |
Modifier le dernier commit avec amend
Tu viens de commiter et tu remarques une faute dans le message, ou un fichier oublié.
# Corriger le message du dernier commit
git commit --amend -m "Ajoute la route GET /notes avec pagination"
# Ajouter un fichier oublié sans changer le message
git add routes/notes.test.js
git commit --amend --no-edit
--amend ne modifie pas le commit : il en crée un nouveau, avec une nouvelle empreinte, qui remplace l'ancien au bout de la branche. Si l'ancien avait été poussé, ton historique local et celui de GitHub divergent, et le prochain git push est refusé. Réserve donc --amend aux commits non poussés.
Faire reculer la branche avec git reset
git reset <commit> déplace la branche courante vers un commit plus ancien. Les commits qui suivaient disparaissent de la branche. Ce qui arrive à leurs modifications dépend du mode :
| Mode | Commande | La branche recule | Zone de préparation | Répertoire de travail |
|---|---|---|---|---|
--soft | git reset --soft HEAD~1 | Oui | Garde les modifications des commits défaits, préparées | Inchangé |
--mixed (par défaut) | git reset HEAD~1 | Oui | Vidée : les modifications ne sont plus préparées | Inchangé : les modifications sont toujours là |
--hard | git reset --hard HEAD~1 | Oui | Vidée | Remis à l'état du commit visé : les modifications sont perdues |
Chaque mode a son usage :
--softsert à refaire un commit. Tu as fait trois petits commits « wip » sur ta branche, non poussés :git reset --soft HEAD~3les défait en gardant tout préparé, puis un seulgit commitles remplace par un commit propre.--mixedsert à revenir avant le commit pour le recomposer : tu as tout commité d'un bloc, tu veux finalement deux commits.git reset HEAD~1, puis deux séries degit addetgit commit.--hardsert à tout jeter pour revenir à un état propre : ton essai est raté, tu veux retrouver exactement le commit d'avant, ou aligner ta branche sur celle du serveur avecgit reset --hard origin/main.
Le scénario « j'ai commité sur main au lieu de ma branche », sans avoir poussé :
git switch -c feature/export # la nouvelle branche emporte le commit
git switch main
git reset --hard HEAD~1 # main revient avant le commit
Le commit n'est pas perdu : il vit sur feature/export. Pense toujours à faire la sauvegarde (une branche, un stash) avant un --hard.
Annuler un commit déjà poussé avec git revert
Le commit 8d3e1a2 a introduit un bug et il est déjà sur GitHub. Hugo l'a récupéré. Réécrire l'historique casserait son dépôt. git revert crée un nouveau commit qui fait exactement l'inverse :
git revert 8d3e1a2
[main 5c71f40] Revert "Arrondit les indemnités au centime supérieur"
1 file changed, 1 insertion(+), 1 deletion(-)
Git ouvre l'éditeur pour le message, prérempli avec Revert "…" et une ligne This reverts commit 8d3e1a2…. Complète-le avec la raison de l'annulation. Le commit d'origine reste dans l'historique, suivi de son annulation : on voit ce qui s'est passé, et le prochain git push passe normalement.
| Commande | Effet |
|---|---|
git revert HEAD | Annule le dernier commit |
git revert a1b2c3d e4f5a6b | Annule deux commits, avec un commit d'annulation chacun |
git revert --no-commit HEAD~2..HEAD | Annule les deux derniers commits et laisse le résultat préparé, pour un seul commit d'annulation |
git revert -m 1 <commit-de-fusion> | Annule une fusion en gardant le côté de la branche principale |
Si le code a évolué depuis le commit annulé, git revert peut rencontrer un conflit : tu le résous comme un conflit de fusion (chapitre 6), puis git revert --continue.
Reset ou revert ?
git reset | git revert | |
|---|---|---|
| Ce qu'il fait | Fait reculer la branche, les commits disparaissent | Ajoute un commit qui annule |
| Historique | Réécrit | Conservé, l'annulation est visible |
| Après un push | Push refusé, il faudrait forcer | Push normal |
| Usage | Commits locaux, non partagés | Commits partagés |
Si tu as fait un reset sur des commits déjà poussés, GitHub refuse ton push (non-fast-forward). La tentation est git push --force, qui écrase l'historique distant : sur une branche partagée, tu supprimes les commits que tes coéquipiers ont poussés entre-temps. Si tu dois vraiment forcer sur ta propre branche, utilise git push --force-with-lease, qui refuse d'écraser des commits que tu n'as pas encore vus (chapitre 7).
Retrouver un commit perdu avec git reflog
Tu as fait un git reset --hard HEAD~2 de trop, et deux heures de travail commité semblent avoir disparu. Pas de panique : Git note dans le reflog chaque déplacement de HEAD, pendant 90 jours par défaut (30 jours pour les commits qui ne sont plus sur aucune branche).
git reflog
3759676 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
12607ec HEAD@{1}: commit: Ajoute le filtre par date
40f61f6 HEAD@{2}: commit: Documente l'installation
3759676 (HEAD -> main) HEAD@{3}: commit: Ajoute l'export CSV
On lit de haut en bas, du plus récent au plus ancien. Avant le reset, HEAD était sur 12607ec. Pour y revenir :
git reset --hard 12607ec
Ou, plus prudent, crée une branche sur ce commit pour l'examiner : git branch sauvetage 12607ec. Le reflog retrouve aussi une branche supprimée par erreur ou l'état d'avant un rebase raté.
Mettre son travail de côté avec git stash
Tu es au milieu d'une modification, pas assez avancée pour un commit, et tu dois changer de branche pour corriger un bug urgent. Git refuse si tes modifications entrent en conflit avec l'autre branche :
error: Your local changes to the following files would be overwritten by checkout:
src/frais.js
Please commit your changes or stash them before you switch branches.
Aborting
git stash range tes modifications non commitées dans une pile et remet ton répertoire de travail dans l'état du dernier commit :
git stash push -m "formulaire à moitié fait"
git switch main
# ... tu corriges le bug, tu commites, tu pousses ...
git switch feature/formulaire
git stash pop
| Commande | Effet |
|---|---|
git stash ou git stash push -m "message" | Range les modifications des fichiers suivis (préparées ou non) |
git stash -u | Range aussi les fichiers non suivis |
git stash list | Liste la pile : stash@{0} est le plus récent |
git stash show -p stash@{1} | Affiche le contenu d'une entrée |
git stash pop | Réapplique la dernière entrée et la retire de la pile |
git stash apply stash@{1} | Réapplique une entrée sans la retirer |
git stash drop stash@{1} | Supprime une entrée |
git stash branch nouvelle-branche | Crée une branche à partir du commit d'origine et y réapplique la dernière entrée |
Si git stash pop rencontre un conflit, l'entrée reste dans la pile : tu résous le conflit, puis tu la supprimes avec git stash drop. Ne laisse pas la pile grossir : un stash oublié depuis trois semaines, sans message, est presque impossible à reprendre. Au-delà de quelques heures, un commit sur une branche de travail vaut mieux.
Exercices
Exercice 1 sur 4 La bonne commande Réussi À revoir
Pour chaque situation, donne la commande à utiliser.
- Tu as modifié
style.cssdepuis le dernier commit, le résultat est affreux, tu veux revenir à la version commitée. - Tu as lancé
git add .et préparénotes-perso.txtpar erreur. Tu veux le garder sur ton disque mais pas dans le commit. - Ton dernier commit, non poussé, a pour message
ajout route. Tu veuxAjoute la route DELETE /notes/:id. - Le commit
3fa91c0, poussé hier surmain, casse l'export PDF. Toute l'équipe a récupérémaindepuis. - Tes trois derniers commits, non poussés, s'appellent
wip,wipetça marche. Tu veux un seul commit propre.
Voir la réponseMasquer la réponse de l'exercice 1
git restore style.css(après ungit diff style.csspour vérifier ce que tu jettes).git restore --staged notes-perso.txt, puis l'ajouter au.gitignoreou à.git/info/excludepour ne plus le préparer.git commit --amend -m "Ajoute la route DELETE /notes/:id"git revert 3fa91c0, puisgit push. Le commit est partagé : on ne réécrit pas l'historique.git reset --soft HEAD~3puisgit commit -m "Ajoute l'export des notes en CSV"(avec le vrai sujet de ces commits). Un rebase interactif donne le même résultat (chapitre 7).
Tu avais trouvé ?
Exercice 2 sur 4 Les trois modes de reset Réussi À revoir
L'historique de main est A ← B ← C et HEAD est sur C. Le commit C a ajouté le fichier export.js et modifié app.js. Ton répertoire de travail est propre. Pour chaque commande, lancée depuis cet état, indique où pointe main, ce que contient la zone de préparation et ce que montre git status.
git reset --soft HEAD~1git reset HEAD~1git reset --hard HEAD~1
Voir la réponseMasquer la réponse de l'exercice 2
Dans les trois cas, main pointe sur B.
--soft: les modifications deCsont préparées.git statusaffiche, sousChanges to be committed,new file: export.jsetmodified: app.js. Ungit commitrefait un commit équivalent àC.--mixed: la zone de préparation est vidée.git statusaffichemodified: app.jssousChanges not staged for commitetexport.jssousUntracked files: le fichier est sur ton disque, mais Git ne le suit plus puisqu'il n'existe pas dansB.--hard: le répertoire de travail est remis à l'état deB.export.jsest supprimé du disque,app.jsrevient à sa version deB, etgit statusaffichenothing to commit, working tree clean. Le commitCreste récupérable avecgit reflog.
Tu avais trouvé ?
Exercice 3 sur 4 Le reset de trop Réussi À revoir
Hugo tape git reset --hard HEAD~3 au lieu de HEAD~1. Voici le début de son git reflog :
9b4e2d1 (HEAD -> feature/formulaire) HEAD@{0}: reset: moving to HEAD~3
61c0f8a HEAD@{1}: commit: Valide le montant côté client
2ad93e7 HEAD@{2}: commit: Ajoute le champ date
d7f15b0 HEAD@{3}: commit: Ajoute le champ motif
9b4e2d1 (HEAD -> feature/formulaire) HEAD@{4}: commit: Crée le composant Formulaire
- Combien de commits a-t-il perdus de vue, et lesquels ?
- Il voulait seulement défaire le dernier commit. Quelle commande donne exactement l'état qu'il visait ?
- Pourquoi le reflog ne l'aurait-il pas sauvé s'il avait eu, en plus, des modifications non commitées ?
Voir la réponseMasquer la réponse de l'exercice 3
- Trois : « Ajoute le champ motif » (
d7f15b0), « Ajoute le champ date » (2ad93e7) et « Valide le montant côté client » (61c0f8a). git reset --hard 2ad93e7(ougit reset --hard HEAD@{2}) : la branche pointe sur « Ajoute le champ date », ce qu'aurait donnégit reset --hard HEAD~1depuis61c0f8a.- Le reflog n'enregistre que des déplacements entre commits. Des modifications jamais commitées n'ont pas d'empreinte :
--hardles a écrasées sur le disque, et Git n'en garde aucune copie.
Tu avais trouvé ?
Exercice 4 sur 4 Annuler une annulation Réussi À revoir
Lundi, Lina a annulé le commit 8d3e1a2 avec git revert, ce qui a créé le commit 5c71f40, poussé sur main. Mercredi, le bug est compris : le code de 8d3e1a2 était bon, c'était la base de test qui était fausse. Comment remettre les modifications de 8d3e1a2 sans réécrire l'historique ?
Voir la réponseMasquer la réponse de l'exercice 4
git revert 5c71f40 : annuler l'annulation réapplique les modifications de 8d3e1a2, dans un nouveau commit. L'historique montre les trois étapes, ce qui documente l'épisode. Écris dans le message pourquoi le code revient : « Rétablit l'arrondi des indemnités : le bug venait de la base de test ».
Tu avais trouvé ?