Comment annuler en toute sécurité des commits locaux et distants dans Git
Annulez en toute sécurité des commits Git avec reset, revert, reflog et force-with-lease sans perdre de travail ni casser les branches partagées.
Comment annuler en toute sécurité des commits locaux et distants dans Git
Annuler un commit Git est facile. Annuler la bonne chose, sans perdre de travail ni surprendre tous les autres sur la branche, c'est la partie qui demande du jugement.
La première question n'est pas "quelle commande dois-je exécuter ?" C'est "qui a vu ce commit ?" Si le commit n'existe que sur votre ordinateur portable, vous pouvez généralement le réécrire avec git reset ou git commit --amend. Si le commit a déjà été poussé vers une branche que d'autres personnes utilisent, préférez git revert. Cela maintient l'historique intact et crée un nouveau commit qui annule la mauvaise modification.
Avant de toucher à l'historique, prenez un instantané rapide de votre position :
git status
git branch backup-before-undo
git log --oneline --decorate -5
Cette branche temporaire est une assurance bon marché. Si vous réinitialisez trop loin ou changez d'avis, l'ancien commit a toujours un nom.
Si le commit n'a pas été poussé
Pour un commit local que vous n'avez pas poussé, git reset est généralement l'outil le plus propre. Il déplace votre pointeur de branche en arrière. Le mode que vous choisissez décide ce qui arrive aux fichiers.
Utilisez --soft lorsque le message de commit était erroné ou que vous avez oublié un petit fichier :
git reset --soft HEAD~1
Votre dernier commit disparaît, mais les modifications restent en zone de staging. Vous pouvez ajouter le fichier manquant et commiter à nouveau :
git add missing-file.yml
git commit -m "Mise à jour de la configuration de déploiement"
Utilisez la réinitialisation mixte par défaut lorsque vous voulez que les modifications reviennent dans votre arbre de travail, non indexées :
git reset HEAD~1
C'est la commande quotidienne "défaire ce commit, mais garder mes modifications". Elle est utile lorsqu'un commit devrait en réalité devenir deux commits plus petits, ou lorsque vous avez commité une instruction d'impression de débogage avec du code réel.
Utilisez --hard uniquement lorsque vous voulez vraiment jeter les modifications locales :
git reset --hard HEAD~1
Cela réinitialise les fichiers suivis au commit précédent. Cela ne demande pas poliment si vous le vouliez vraiment. Si vous avez du travail non commité dans des fichiers suivis, il peut disparaître de l'arbre de travail. Vérifiez d'abord git status, et stash ou créez une branche pour tout ce que vous pourriez vouloir récupérer.
Pour une petite correction du dernier commit local, git commit --amend est souvent meilleur que reset :
git add corrected-file.js
git commit --amend
Cela remplace le dernier commit par un nouveau. La même règle s'applique : amendez librement avant de pousser ; soyez prudent après avoir poussé.
Si le commit a déjà été poussé
Sur une branche partagée, utilisez git revert sauf si vous avez une raison solide de réécrire l'historique. Revert crée un nouveau commit qui applique le patch opposé.
git revert a1b2c3d
Cette commande ouvre votre éditeur avec un message généré. Sauvegardez-le, et Git crée un nouveau commit. Le commit original reste dans l'historique, ce qui est exactement ce que vous voulez sur main, master, develop, les branches de release, et toute branche que vos coéquipiers ont pu tirer.
Si vous devez annuler plusieurs commits consécutifs, vous pouvez le faire en un seul lot préparé :
git revert --no-commit HEAD~3..HEAD
git status
git commit -m "Annuler les modifications récentes de déploiement"
Lisez attentivement la plage. HEAD~3..HEAD signifie les trois derniers commits, sans inclure HEAD~3 lui-même. En cas de doute, listez d'abord les commits :
git log --oneline HEAD~3..HEAD
Les commits de fusion nécessitent une décision supplémentaire. Une fusion a plus d'un parent, donc Git a besoin de savoir quel côté doit être traité comme la ligne principale :
git revert -m 1 <sha-du-commit-de-fusion>
La plupart des équipes utilisent -m 1 lors de l'annulation d'une fusion de branche de fonctionnalité à partir de la branche cible, mais ne l'exécutez pas aveuglément. Regardez le commit de fusion avec git show --summary <sha> et confirmez l'ordre des parents.
Si vous avez poussé la mauvaise chose et devez la supprimer
Parfois, revert ne suffit pas. Si vous avez poussé un secret, un gros binaire ou des données clients privées, un revert ne fait que le supprimer de l'arbre le plus récent. Le contenu sensible existe toujours dans l'historique. Cela devient un problème de réponse aux incidents, pas seulement un problème de nettoyage Git.
Pour les secrets, faites d'abord tourner le credential. Ensuite, supprimez les données de l'historique avec un outil de réécriture d'historique approprié tel que git filter-repo, BFG Repo-Cleaner, ou un processus de suppression de secret spécifique à l'hôte. Coordonnez-vous avec le propriétaire du dépôt et supposez que toute personne ayant accès a pu récupérer le mauvais commit avant que vous ne le supprimiez.
Pour un commit erroné normal sur votre propre branche de fonctionnalité, réécrire la branche distante peut être acceptable. Réinitialisez localement, puis force push avec un lease :
git reset --hard HEAD~1
git push --force-with-lease origin my-feature-branch
--force-with-lease est la forme plus sûre car elle refuse de mettre à jour le distant si quelqu'un d'autre a poussé du nouveau travail depuis votre dernier fetch. C'est toujours un force push. Cela réécrit toujours la branche. Utilisez-le sur des branches de fonctionnalités personnelles ou coordonnées, pas à la légère sur des branches partagées.
Une bonne habitude avant de forcer un push est :
git fetch origin
git log --oneline --left-right --graph origin/my-feature-branch...my-feature-branch
Cela montre ce qui existe uniquement sur le distant et ce qui existe uniquement localement. Si le côté gauche contient les commits de quelqu'un d'autre, arrêtez-vous et parlez-lui.
Récupération avec reflog
git reflog est la commande qui sauve de nombreux mauvais après-midis. Elle enregistre où votre HEAD local et les références de branche ont pointé récemment. Si vous réinitialisez au mauvais endroit, supprimez une branche ou amendez le mauvais commit, reflog connaît souvent l'ancien SHA du commit.
git reflog
Vous pourriez voir quelque chose comme :
7cc8a91 HEAD@{0}: reset: moving to HEAD~1
2b41f0d HEAD@{1}: commit: add retry around deploy step
Pour récupérer l'ancien commit, créez une branche à cet endroit :
git branch recovered-deploy-work 2b41f0d
Je préfère créer une branche plutôt que d'exécuter immédiatement une autre réinitialisation dure. Cela vous donne une poignée stable, et vous pouvez inspecter le travail récupéré calmement :
git show recovered-deploy-work
git switch recovered-deploy-work
Reflog est local. Le reflog de votre coéquipier ne contiendra pas vos commits locaux perdus, et les hôtes distants peuvent ne pas exposer le même chemin de récupération. Il est également élagué au fil du temps selon les paramètres de garbage collection de Git, donc traitez-le comme un filet de sécurité récent, pas comme un stockage d'archives.
Un guide de décision pratique
Si vous avez commité trop tôt et n'avez pas poussé, utilisez git reset --soft HEAD~1 ou git reset HEAD~1.
Si vous avez commité le mauvais fichier et voulez que les modifications disparaissent localement, utilisez git reset --hard HEAD~1 seulement après avoir vérifié git status.
Si le mauvais commit est déjà sur une branche partagée, utilisez git revert <sha>.
Si votre branche de fonctionnalité est distante mais que vous seul l'utilisez, git reset plus git push --force-with-lease est généralement acceptable.
Si le commit contient un secret ou des données sensibles, ne comptez pas sur revert. Faites tourner le secret, réécrivez l'historique avec le bon outil, et coordonnez le nettoyage.
Le workflow d'annulation Git le plus sûr est ennuyeux : inspectez d'abord, faites une branche de sauvegarde, choisissez la commande en fonction du partage du commit, et utilisez reflog lorsque vous devez récupérer de votre propre tentative de récupération.