07 - Reflog et recovery : rien n'est jamais perdu dans git
Ce que tu vas apprendre
- Ce qu'est le reflog et pourquoi c'est ton filet de sécurité
- Récupérer une branche supprimee
- Annuler un rebase foireux
- Retrouver un commit amende par erreur
Prerequisites
Le reflog : la boîte noire de git
Chaque fois que HEAD bouge dans ton repo (commit, checkout, rebase, merge, reset, pull...), git enregistre le mouvement dans le reflog. C'est un journal chronologique de tout ce qui s'est passe. Pendant 90 jours.
bashgit reflog
a1b2c3d HEAD@{0}: commit: feat(search): add autocomplete
e4f5g6h HEAD@{1}: checkout: moving from main to feat/add-search
i7j8k9l HEAD@{2}: commit: chore(deps): update vite
m0n1o2p HEAD@{3}: rebase (finish): returning to refs/heads/main
q3r4s5t HEAD@{4}: rebase (pick): fix(auth): handle timeout
u6v7w8x HEAD@{5}: rebase (start): checkout origin/main
Chaque ligne montre : le hash du commit, la référencé (HEAD@{N}), l'opération, et une description. C'est l'historique complet de tes deplacements dans le graphe git.
La différence avec git log : le log montre l'historique des commits de la branche courante. Le reflog montre l'historique de tes actions. Un commit supprime disparaît du log mais reste dans le reflog.
Scénario 1 : branche supprimee par erreur
Tu supprimes une branche par accident. Ca arrive. Surtout avec git branch -D qui force la suppression sans vérifier si les changements ont ete merges.
bashgit branch -D feat/search-v2
# Deleted branch feat/search-v2 (was a1b2c3d).
Pas de panique. Le commit est toujours la. Git ne supprime pas les objets immédiatement. Il faut juste retrouver le hash.
bashgit reflog
# ... cherche l'entree ou tu etais sur cette branche ...
# a1b2c3d HEAD@{7}: commit: feat(search): add fuzzy matching
bashgit checkout -b feat/search-v2 a1b2c3d
Ta branche est de retour, avec tous ses commits. Si tu ne trouves pas le hash dans le reflog (par exemple, tu n'as jamais checkout cette branche toi-meme), git te l'a affiche quand tu l'as supprimee : (was a1b2c3d). Regarde ton terminal.
Scénario 2 : rebase qui tourne mal
Tu fais un rebase et le résultat est catastrophique. Les conflits etaient trop complexes, tu as fait des erreurs de résolution, le code ne compile plus. Tu as deja fait git rebase --continue sur les 8 commits. Trop tard pour --abort.
bashgit reflog
# a1b2c3d HEAD@{0}: rebase (finish): returning to refs/heads/feat/search
# ... plein d'etapes de rebase ...
# m0n1o2p HEAD@{9}: rebase (start): checkout origin/main
# q3r4s5t HEAD@{10}: commit: feat(search): add results page
HEAD@{10} est l'état juste avant le rebase. Tu y retournes :
bashgit reset --hard HEAD@{10}
C'est comme si le rebase n'avait jamais eu lieu. Ta branche est revenue a son état d'avant. Tu peux recommencer plus calmement, ou choisir un merge a la place.
J'utilise ca au moins une fois par mois. Pas parce que je fais beaucoup d'erreurs (enfin, si, un peu), mais parce que parfois un rebase avec 15 conflits, c'est juste pas le bon moment. Tu reset, tu fais un merge classique, tu avances.
Scénario 3 : commit amende par erreur
Tu fais git commit --amend sur le mauvais commit. Ou tu amendes en ecrasant un message que tu voulais garder. L'ancien commit semble perdu puisque git log montre la version amendee.
Mais le reflog garde l'ancien :
bashgit reflog
# a1b2c3d HEAD@{0}: commit (amend): feat(search): add autocomplete (v2)
# e4f5g6h HEAD@{1}: commit: feat(search): add autocomplete
HEAD@{1} est le commit original, avant l'amend. Tu peux le restaurer :
bashgit reset --soft HEAD@{1}
--soft garde les modifications en staging. Tu peux re-commiter proprement.
Scénario 4 : le force push sur main
L'anecdote de l'introduction. Un dev force push sur main et ecrase des commits. Sur le serveur GitLab, ces commits sont partis (le reflog serveur est généralement inaccessible ou désactivé). Mais sur ta machine locale, ton reflog garde les références.
bashgit reflog main
# a1b2c3d main@{0}: reset: moving to HEAD~5 <-- le force push a fait ca
# e4f5g6h main@{1}: pull: Fast-forward <-- l'etat d'avant
bashgit checkout main
git reset --hard main@{1}
git push --force
Ca restaure main a son état d'avant le desastre. C'est exactement ce que j'ai fait pour sauver les trois jours de travail de l'équipe.
Depuis cet episode, j'ai ajoute deux protections :
- Branche main protégée sur GitLab (Settings > Repository > Protected branches) : push interdit, seul le merge via MR est autorise
- Regle
receive.denyNonFastForwardssur le remote si tu geres ton propre serveur git
git fsck : les objets orphelins
Si le reflog ne suffit pas (par exemple, tu cherches un commit qui date de plus de 90 jours ou que le reflog a ete nettoye), git fsck peut trouver des objets orphelins :
bashgit fsck --lost-found
# dangling commit a1b2c3d4e5f6...
# dangling commit e4f5g6h7i8j9...
bashgit show a1b2c3d4e5f6
Ca te montre le contenu du commit orphelin. Si c'est celui que tu cherches, tu peux créer une branche dessus.
En pratique, j'ai utilise fsck une seule fois en cinq ans. Le reflog resout 99% des cas.
git gc et la fenêtre de 90 jours
Git fait du garbage collection periodiquement (git gc). Les objets non références depuis plus de 90 jours sont supprimes définitivement. Avant 90 jours, tout est recuperable.
bash# Voir la config d'expiration
git config gc.reflogExpire
# default: 90 days
Ne lance pas git gc --prune=now sauf si tu sais exactement ce que tu fais. Ca supprime immédiatement les objets non références. Sur paltemps.fr, j'ai un cron qui fait git gc hebdomadaire sur le serveur, mais avec les paramètres par défaut (90 jours).
La leçon
Git ne perd presque rien. Le reflog est ton filet de sécurité. La prochaine fois que tu paniques apres un reset, un rebase, ou un force push, respire et tape git reflog. La réponse est probablement dedans.
Mais le meilleur recovery, c'est celui dont tu n'as pas besoin. Protege tes branches, utilise --force-with-lease au lieu de --force, et impose des hooks qui attrapent les erreurs avant qu'elles arrivent.
Article précédent : 06 - Git hooks Article suivant : 08 - Glossaire