Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 restore et corriger le dernier commit avec --amend ;
  • faire reculer une branche avec git reset, ou annuler un commit poussé avec git revert ;
  • retrouver un commit perdu avec git reflog et mettre ton travail de côté avec git 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 :

L'erreur est-ellecommitée ?nonouiModification en courspas encore préparée :git restore fichierdéjà préparée :git restore --staged fichierLe commit est-ilpoussé ?nonouiCommit localgit commit --amendcorriger le derniergit reset HEAD~1défaire, garder les fichiersCommit partagégit revert<commit>nouveau commitqui annulePerdu un commit ?git reflog le retrouve
Annuler en Git : la commande dépend de l'état de l'erreur, et surtout de savoir si le commit est poussé
SituationCommandeCe qui se passe
Modification non préparée, à jetergit restore fichierLe fichier revient à l'état de la zone de préparation, ou du dernier commit
Modification préparée par erreurgit restore --staged fichierLe fichier sort de la zone de préparation, la modification reste sur le disque
Dernier commit à corriger (message, fichier oublié), non pousségit commit --amendLe dernier commit est remplacé
Derniers commits à défaire, non poussésgit resetLa branche recule ; selon le mode, les modifications sont gardées ou jetées
Commit déjà poussé à annulergit revert <commit>Un nouveau commit annule l'ancien ; l'historique partagé n'est pas réécrit
Commit perdu après une fausse manœuvregit reflog puis git reset ou git branchGit 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 :

bash
git restore app.js

Les variantes :

CommandeEffet
git restore app.jsRemet 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.jsRetire le fichier de la zone de préparation ; le contenu sur le disque ne change pas
git restore --staged --worktree app.jsLes deux : le fichier redevient identique au dernier commit
git restore --source=HEAD~3 app.jsRamè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 commandeNouvelle commandeEffet
git checkout feature/exportgit switch feature/exportChanger de branche
git checkout -b feature/exportgit switch -c feature/exportCréer une branche et y aller
git checkout -git switch -Revenir à la branche précédente
git checkout a1b2c3dgit switch --detach a1b2c3dAller sur un ancien commit, hors de toute branche
git checkout -- app.jsgit restore app.jsJeter les modifications d'un fichier
git reset HEAD app.jsgit restore --staged app.jsRetirer un fichier de la zone de préparation
git checkout a1b2c3d -- app.jsgit restore --source=a1b2c3d app.jsRé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é.

bash
# 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 :

ModeCommandeLa branche reculeZone de préparationRépertoire de travail
--softgit reset --soft HEAD~1OuiGarde les modifications des commits défaits, préparéesInchangé
--mixed (par défaut)git reset HEAD~1OuiVidée : les modifications ne sont plus préparéesInchangé : les modifications sont toujours là
--hardgit reset --hard HEAD~1OuiVidéeRemis à l'état du commit visé : les modifications sont perdues

Chaque mode a son usage :

  • --soft sert à refaire un commit. Tu as fait trois petits commits « wip » sur ta branche, non poussés : git reset --soft HEAD~3 les défait en gardant tout préparé, puis un seul git commit les remplace par un commit propre.
  • --mixed sert à 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 de git add et git commit.
  • --hard sert à 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 avec git reset --hard origin/main.

Le scénario « j'ai commité sur main au lieu de ma branche », sans avoir poussé :

bash
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 :

bash
git revert 8d3e1a2
text
[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.

CommandeEffet
git revert HEADAnnule le dernier commit
git revert a1b2c3d e4f5a6bAnnule deux commits, avec un commit d'annulation chacun
git revert --no-commit HEAD~2..HEADAnnule 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 resetgit revert
Ce qu'il faitFait reculer la branche, les commits disparaissentAjoute un commit qui annule
HistoriqueRéécritConservé, l'annulation est visible
Après un pushPush refusé, il faudrait forcerPush normal
UsageCommits locaux, non partagésCommits 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).

bash
git reflog
text
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 :

bash
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 :

text
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 :

bash
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
CommandeEffet
git stash ou git stash push -m "message"Range les modifications des fichiers suivis (préparées ou non)
git stash -uRange aussi les fichiers non suivis
git stash listListe la pile : stash@{0} est le plus récent
git stash show -p stash@{1}Affiche le contenu d'une entrée
git stash popRé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-brancheCré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.

  1. Tu as modifié style.css depuis le dernier commit, le résultat est affreux, tu veux revenir à la version commitée.
  2. Tu as lancé git add . et préparé notes-perso.txt par erreur. Tu veux le garder sur ton disque mais pas dans le commit.
  3. Ton dernier commit, non poussé, a pour message ajout route. Tu veux Ajoute la route DELETE /notes/:id.
  4. Le commit 3fa91c0, poussé hier sur main, casse l'export PDF. Toute l'équipe a récupéré main depuis.
  5. Tes trois derniers commits, non poussés, s'appellent wip, wip et ça marche. Tu veux un seul commit propre.
Voir la réponseMasquer la réponse de l'exercice 1
  1. git restore style.css (après un git diff style.css pour vérifier ce que tu jettes).
  2. git restore --staged notes-perso.txt, puis l'ajouter au .gitignore ou à .git/info/exclude pour ne plus le préparer.
  3. git commit --amend -m "Ajoute la route DELETE /notes/:id"
  4. git revert 3fa91c0, puis git push. Le commit est partagé : on ne réécrit pas l'historique.
  5. git reset --soft HEAD~3 puis git 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.

  1. git reset --soft HEAD~1
  2. git reset HEAD~1
  3. git reset --hard HEAD~1
Voir la réponseMasquer la réponse de l'exercice 2

Dans les trois cas, main pointe sur B.

  1. --soft : les modifications de C sont préparées. git status affiche, sous Changes to be committed, new file: export.js et modified: app.js. Un git commit refait un commit équivalent à C.
  2. --mixed : la zone de préparation est vidée. git status affiche modified: app.js sous Changes not staged for commit et export.js sous Untracked files : le fichier est sur ton disque, mais Git ne le suit plus puisqu'il n'existe pas dans B.
  3. --hard : le répertoire de travail est remis à l'état de B. export.js est supprimé du disque, app.js revient à sa version de B, et git status affiche nothing to commit, working tree clean. Le commit C reste récupérable avec git 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 :

text
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
  1. Combien de commits a-t-il perdus de vue, et lesquels ?
  2. Il voulait seulement défaire le dernier commit. Quelle commande donne exactement l'état qu'il visait ?
  3. 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
  1. Trois : « Ajoute le champ motif » (d7f15b0), « Ajoute le champ date » (2ad93e7) et « Valide le montant côté client » (61c0f8a).
  2. git reset --hard 2ad93e7 (ou git reset --hard HEAD@{2}) : la branche pointe sur « Ajoute le champ date », ce qu'aurait donné git reset --hard HEAD~1 depuis 61c0f8a.
  3. Le reflog n'enregistre que des déplacements entre commits. Des modifications jamais commitées n'ont pas d'empreinte : --hard les 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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Les branches Git : créer, changer, fusionnerRevoir le sommaire du cours