Annuler les changements dans Git: reset, revert et restore
1. Aperçu
Tout utilisateur de Git a un jour besoin de revenir en arrière : modifier un fichier par erreur, faire un git add inutile, ou commiter quelque chose qu’il ne fallait pas. Git dispose de trois commandes principales pour cela : restore, reset et revert. Leurs noms se ressemblent, mais chaque commande agit sur une zone différente.
Cet article passe en revue chaque commande sur un dépôt exemple commun. Une fois que vous aurez lu cet article, vous saurez quelle commande utiliser dans chaque situation. Vous saurez aussi comment récupérer un commit après un reset --hard malencontreux.
2. Préparation du dépôt exemple
D’abord, n’oubliez pas que Git dispose de trois zones :
- Working tree : les fichiers que vous modifiez sur le disque.
- Staging area (index) : ce qui entrera dans le prochain commit.
- Commit history : les commits enregistrés, avec
HEADpointant vers le commit actuel.
Créons un petit dépôt avec quatre commits :
mkdir order-service && cd order-service
git init -b main
echo "class Order {}" > Order.java
git add Order.java && git commit -m "Ajouter la classe Order"
echo "class Customer {}" > Customer.java
git add Customer.java && git commit -m "Ajouter la classe Customer"
echo "class Order { int total; }" > Order.java
git commit -am "Ajouter total à Order"
echo "class PaymentService {}" > PaymentService.java
git add PaymentService.java && git commit -m "Ajouter PaymentService"
git log --oneline
La commande git commit -am stage tous les fichiers déjà suivi et les commit directement. Le résultat de git log ressemblera à ceci (le hash sur votre machine sera différent) :
d4e5f60 (HEAD -> main) Ajouter PaymentService
c3d4e5f Ajouter total à Order
b2c3d4e Ajouter la classe Customer
a1b2c3d Ajouter la classe Order
3. git restore : annuler les modifications non commises
git restore existe depuis Git 2.23. Cette commande ne fonctionne que sur les fichiers. Elle ne déplace jamais HEAD et ne modifie jamais l’historique des commits. Dans les anciennes versions de Git, c’était git checkout -- <file> qui gérait cela.
3.1. Annuler les modifications dans le working tree
Supposons que vous ayez modifié Order.java et que vous changiez d’avis :
echo "class Order { int total; String note; }" > Order.java
git status --short
git restore Order.java
cat Order.java
M Order.java
class Order { int total; }
git status --short affiche M ce qui signifie que le fichier a été modifié mais n’est pas staged. git restore Order.java copie à nouveau le contenu de la staging area vers le working tree. Comme rien n’a été staged, ce contenu correspond à HEAD.
Attention : Les modifications annulées avec
git restoresont définitivement perdues. Git ne les a jamais enregistrées, donc reflog ne peut pas les récupérer. Si vous hésitez, exécutezgit stashau lieu derestore.
3.2. Retirer un fichier de la staging area
Vous avez accidentellement fait un git add sur un fichier que vous ne vouliez pas commiter :
echo "class Order { int total; String note; }" > Order.java
git add Order.java
git restore --staged Order.java
git status --short
M Order.java
L’option --staged copie le contenu de HEAD vers la staging area. Le résultat est que le fichier est “unstaged”, tandis que le contenu que vous veniez de modifier reste dans le working tree. L’ancienne commande équivalente est git reset HEAD Order.java.
Pour annuler à la fois la staging area et le working tree en une fois, vous pouvez utiliser :
git restore --staged --worktree Order.java
3.3. Récupérer un fichier d’un ancien commit
L’option --source vous permet de récupérer le contenu du fichier à partir de n’importe quel commit :
git restore --source=HEAD~2 Order.java
cat Order.java
git status --short
class Order {}
M Order.java
HEAD~2 est le commit “Ajouter la classe Customer”. À ce commit, Order.java n’avait pas d’attribut total. Par défaut, la commande n’écrit que dans le working tree, donc le fichier est M (non staged). Vous vérifiez, puis vous faites vous-même un git add et commit si vous voulez conserver. Sinon, exécutez git restore Order.java pour revenir à l’état précédent.
4. git reset : déplacer le pointeur de branche
git reset <commit> déplace la branche actuelle (et HEAD) vers le commit spécifié. Les trois modes --soft, --mixed, --hard diffèrent selon que la staging area et le working tree soient écrasés ou non.
| Mode | Branche/HEAD | Staging area | Working tree |
|---|---|---|---|
--soft |
déplacée vers commit cible | inchangée | inchangée |
--mixed (par défaut) |
déplacée vers commit cible | réinitialisée selon commit cible | inchangée |
--hard |
déplacée vers commit cible | réinitialisée selon commit cible | réinitialisée selon commit cible |
Tous les exemples ci-dessous partent d’un état propre au commit d4e5f60.
4.1. –soft : retirer le commit, conserver les modifications staged
git reset --soft HEAD~1
git log --oneline -1
git status --short
c3d4e5f (HEAD -> main) Ajouter total à Order
A PaymentService.java
Le commit “Ajouter PaymentService” a quitté la branche. Le fichier PaymentService.java est toujours là et est en état A (staged). Cette approche convient quand vous voulez modifier le message ou fusionner plusieurs commits en un seul :
git commit -m "Ajouter PaymentService pour gérer les paiements"
4.2. –mixed : retirer le commit et unstage
git reset HEAD~1
git status --short
?? PaymentService.java
C’est le mode par défaut. La branche recule d’un commit, la staging area est réinitialisée. PaymentService.java est un nouveau fichier, il est donc maintenant untracked (??). Vous utilisez --mixed quand vous voulez séparer à nouveau un commit : stagez une partie à la fois, puis committez plusieurs petits commits.
4.3. –hard : tout supprimer
git add PaymentService.java && git commit -m "Ajouter PaymentService"
git reset --hard HEAD~1
ls
HEAD is now at c3d4e5f Ajouter total à Order
Customer.java Order.java
Le commit, le contenu de la staging area et du working tree de tous les fichiers tracked sont ramenés à c3d4e5f. PaymentService.java a disparu.
Attention :
reset --hardsupprime aussi les modifications non commises sur les fichiers tracked. Ces modifications ne peuvent pas être récupérées. En revanche, il ne supprime pas les fichiers untracked. Pour supprimer les fichiers untracked, vous devez utilisergit clean -fd, et cette commande est tout aussi dangereuse.
4.4. Récupérer un commit avec reflog
Le commit reséti n’est pas immédiatement perdu. Git enregistre chaque mouvement de HEAD dans reflog :
git reflog -3
c3d4e5f HEAD@{0}: reset: moving to HEAD~1
9f8e7d6 HEAD@{1}: commit: Ajouter PaymentService
c3d4e5f HEAD@{2}: commit: Ajouter PaymentService pour gérer les paiements
Vous n’avez qu’à réinitialiser vers le commit que vous voulez récupérer :
git reset --hard 9f8e7d6
git log --oneline -1
9f8e7d6 (HEAD -> main) Ajouter PaymentService
Reflog n’existe que sur votre machine. Par défaut, les entrées non référencées expireront après 30 jours et seront supprimées par le garbage collector. Par conséquent, récupérez dès que possible.
4.5. Quand NE PAS utiliser reset
Vous ne devriez pas réinitialiser les commits qui ont été pushés vers une branche partagée. Après un reset, l’historique local et distant divergent, vous êtes obligé de faire un git push --force. Les collègues qui ont pull ces commits auront un historique confus et pourraient accidentellement les repousser.
Si vous devez absolument écraser, par exemple sur une branche personnelle, utilisez une commande plus sûre :
git push --force-with-lease
--force-with-lease refuse de pusher si le remote a des commits nouveaux que vous n’avez pas fetched. Cela vous évite d’écraser accidentellement le travail d’autrui.
5. git revert : annuler avec un nouveau commit
git revert ne supprime pas l’historique. Cette commande crée un nouveau commit ayant un contenu inverse de celui du commit à annuler. L’historique ne fait que s’allonger, il n’est pas réécrit, c’est donc le moyen sûr pour une branche qui a été pushée.
5.1. Annuler un commit
git revert --no-edit HEAD
git log --oneline -3
ls
1a2b3c4 (HEAD -> main) Revert "Ajouter PaymentService"
9f8e7d6 Ajouter PaymentService
c3d4e5f Ajouter total à Order
Customer.java Order.java
--no-edit utilise le message par défaut sans ouvrir d’éditeur. Le commit original 9f8e7d6 reste dans l’historique. Le nouveau commit 1a2b3c4 supprime PaymentService.java. Le corps du nouveau commit contient la ligne This reverts commit 9f8e7d6..., ce qui aide les autres à retrouver l’origine.
Vous pouvez annuler n’importe quel ancien commit au milieu de l’historique, pas nécessairement HEAD :
git revert --no-edit c3d4e5f
cat Order.java
class Order {}
Si les commits ultérieurs ont modifié les mêmes lignes de code, Git signalera un conflit. Vous résolvez le conflit, exécutez git add, puis git revert --continue. Pour annuler à mi-chemin, utilisez git revert --abort.
5.2. Annuler plusieurs commits en un seul
Par défaut, chaque commit annulé génère un commit distinct. Pour les regrouper, utilisez --no-commit :
git revert --no-commit HEAD~3..HEAD~1
git commit -m "Annuler les changements concernant Customer et Order"
La syntaxe A..B sélectionne les commits après A jusqu’à B inclus. Git applique chaque commit à l’envers dans la staging area, puis vous committez une seule fois.
5.3. Annuler un merge commit
Un merge commit a deux parents. Par conséquent, Git a besoin de savoir quelle branche vous voulez conserver comme “ligne principale” :
git revert -m 1 <hash-merge-commit>
-m 1 choisit le premier parent, généralement la branche dans laquelle vous avez fusionné (par exemple main). Le résultat est que toutes les modifications apportées par l’autre branche seront annulées.
Attention : Après avoir annulé une fusion, si vous fusionnez à nouveau la même branche, Git considère les anciens commits comme “déjà fusionnés”. Ces modifications ne reviendront pas. Pour les ramener, vous devez annuler le commit de revert lui-même.
6. Quelle commande choisir ?
| Situation | Commande à utiliser |
|---|---|
| Annuler les modifications non staged d’un fichier | git restore <file> |
| Unstage un fichier | git restore --staged <file> |
| Récupérer une ancienne version d’un fichier | git restore --source=<commit> <file> |
| Modifier/fusionner le dernier commit, non pushé | git reset --soft HEAD~1 |
| Diviser un commit, non pushé | git reset HEAD~1 |
| Supprimer complètement un commit local | git reset --hard <commit> |
| Annuler un commit déjà pushé | git revert <commit> |
Règle simple à retenir :
restorefonctionne sur les fichiers.resetfonctionne sur la branche, à utiliser en local.revertfonctionne sur l’historique partagé.
7. Erreurs courantes
- Confondre reset avec revert sur une branche partagée. C’est l’erreur la plus courante. Si c’est pushé, utilisez
revert. - Exécuter
reset --hardquand il y a des modifications non commises. Avant d’exécuter, vérifiezgit status. S’il y a des modifications, faites ungit stashd’abord. - Croire que
reset --hardnettoie le répertoire. Les fichiers untracked restent intact. Vous devez exécutergit clean -npour prévisualiser, puisgit clean -fdpour supprimer. - Oublier que
HEAD~1etHEAD^sont différents avec les merge commits.HEAD^2est le deuxième parent, tandis queHEAD~2est le grand-parent, c’est-à-dire le parent du parent. Avec un historique linéaire,HEAD~1etHEAD^sont identiques. - Utiliser
git checkout <file>par habitude. Cette commande fonctionne toujours, maischeckoutassume trop de rôles, il est facile de se tromper et de changer de branche par accident. Dans le Git moderne,restoreetswitchsont plus clairs.
8. Conclusion
Dans cet article, vous avez découvert trois façons d’annuler des modifications dans Git :
git restorerestaure le contenu d’un fichier dans le working tree ou la staging area sans toucher à l’historique.git resetdéplace la branche vers un autre commit, avec trois niveaux--soft,--mixed,--hard. Cette commande ne devrait être utilisée que pour les commits non pushés.git revertcrée un commit inverse, c’est le choix sûr pour une branche partagée.
En cas de faux pas, n’oubliez pas git reflog. La plupart des commits “perdus” sont toujours là en attente de récupération.