git reflog: come recuperare commit e branch persi
git reflog è un registro locale in cui Git annota ogni spostamento di HEAD e dei branch: commit, reset, checkout, rebase, merge. Trovate in git reflog la voce precedente all'errore, per esempio HEAD@{2}, e ripristinate lo stato con git reset --hard HEAD@{2}, oppure create un branch con git branch rescue HEAD@{2}.
git reflog è il registro dei riferimenti (reference log): Git vi annota ogni spostamento di HEAD e delle punte dei branch. Avete fatto un commit, cambiato branch, eseguito reset, rebase o merge: nel reflog compare una riga con l'hash a cui puntava il riferimento dopo l'operazione. Per questo anche un commit a cui non punta più nessun branch si può trovare e recuperare.
La cosa principale da sapere: il reflog è locale. Esiste solo nel vostro clone, non viene inviato al server e non arriva con git clone. In compenso, nel vostro repository vi salva da quasi ogni errore con la cronologia.
Come leggere l'output di git reflog
git reflog
3c4d5e6 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
9a8b7c6 HEAD@{1}: commit: Aggiungere validazione e-mail
1f2e3d4 HEAD@{2}: commit: Spostare la configurazione in un file separato
3c4d5e6 (HEAD -> main) HEAD@{3}: checkout: moving from feature to main
7b6a5c4 (feature) HEAD@{4}: commit: Bozza del form di accesso
Ogni riga è uno stato di HEAD, il più recente in alto:
- hash: il commit a cui puntava HEAD dopo l'operazione;
- HEAD@{n}: «dov'era HEAD n spostamenti fa»; è un riferimento a tutti gli effetti e si può passare a qualsiasi comando;
- descrizione: il tipo di operazione (commit, reset, checkout, rebase, merge, pull, cherry-pick, commit (amend)) e i dettagli.
Nell'esempio si vede che sono stati fatti due commit, «Spostare la configurazione» e «Aggiungere validazione», e poi reset HEAD~2 ha riportato indietro il branch. In git log non ci sono più, ma nel reflog sono al loro posto.
Il reflog di un branch specifico
Ogni branch ha il suo registro. È più corto e più utile quando avete cambiato spesso branch:
git reflog show feature
git reflog show origin/main # come è cambiato il branch remoto con i fetch
Riferimenti temporali
Invece del numero si può indicare un momento: Git troverà dove puntava il riferimento in quel momento:
git show main@{yesterday}
git diff main@{"2 hours ago"} main
git log -g --since="1 day ago" --oneline
Il flag -g (--walk-reflogs) di git log percorre il reflog invece dei legami con i genitori e permette di usare i formati e i filtri abituali. Per mostrare le date è comodo git reflog --date=iso.
Come recuperare i commit dopo git reset --hard
La situazione più frequente: avete eseguito git reset --hard sul commit sbagliato e il lavoro è «sparito». In realtà i commit non sono andati da nessuna parte: semplicemente non sono più visibili dai branch.
- Aprite il registro:
git reflog. - Trovate la riga subito prima di
reset: moving to ...: è lo stato precedente all'errore. Nell'esempio sopra èHEAD@{1}con hash9a8b7c6. - Verificate che sia quello giusto:
git log --oneline -5 HEAD@{1}. - Ripristinate il branch:
git reset --hard HEAD@{1}.
git reset --hard HEAD@{1}
HEAD is now at 9a8b7c6 Aggiungere validazione e-mail
Attenzione: dopo ogni operazione i numeri scorrono. Ciò che era HEAD@{1} dopo il reset diventerà HEAD@{2}. Per questo è più affidabile copiare l'hash invece del numero.
Un limite importante: il reflog conserva solo ciò che è stato committato. Le modifiche non committate cancellate da reset --hard o git checkout -- file non si recuperano con il reflog. Resta un po' di speranza se i file erano stati aggiunti all'index: ne parliamo più sotto.
Come recuperare un branch eliminato
Avete eliminato un branch con git branch -D feature e conteneva commit non integrati. Il registro del branch viene eliminato insieme a esso, ma il registro di HEAD resta, e HEAD è passato sui suoi commit mentre ci lavoravate.
git reflog | grep feature
7b6a5c4 HEAD@{4}: commit: Bozza del form di accesso
3c4d5e6 HEAD@{3}: checkout: moving from feature to main
# L'ultimo commit del branch è quello da cui ve ne siete andati
git branch feature 7b6a5c4
Cercate la riga checkout: moving from feature to ...: l'hash nella voce precedente (più in basso nell'elenco) è l'ultimo stato del branch. Tra l'altro, all'eliminazione Git stampa l'hash da solo: Deleted branch feature (was 7b6a5c4). Se il terminale è ancora aperto, basta quello.
Come annullare un rebase non riuscito
Il rebase riscrive i commit, e se durante il processo avete risolto male i conflitti, il risultato può essere peggiore dell'originale. Il reflog conserva lo stato precedente all'inizio del rebase.
git reflog
e1f2a3b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e1f2a3b HEAD@{1}: rebase (pick): Bozza del form di accesso
c4d5e6f HEAD@{2}: rebase (start): checkout main
7b6a5c4 HEAD@{3}: commit: Bozza del form di accesso
git reset --hard HEAD@{3}
Serve la voce precedente a rebase (start). C'è anche una scorciatoia: prima delle operazioni rischiose (rebase, merge, reset) Git salva la posizione precedente nel riferimento ORIG_HEAD, quindi subito dopo il rebase funziona git reset --hard ORIG_HEAD. Però ORIG_HEAD viene sovrascritto dalla successiva operazione di questo tipo, mentre il reflog conserva tutta la cronologia.
Come recuperare il commit prima di git commit --amend
--amend non modifica il commit, ne crea uno nuovo al suo posto. Quello vecchio resta nel reflog:
git reflog
5a6b7c8 HEAD@{0}: commit (amend): Aggiungere validazione e-mail
9a8b7c6 HEAD@{1}: commit: Aggiungere validazione e-mail
git diff HEAD@{1} HEAD # cosa ha cambiato l'amend
git reset --soft HEAD@{1} # ripristinare il vecchio commit, le modifiche dell'amend restano nell'index
Recupero sicuro: un branch invece del reset
Se non siete sicuri di quale commit vi serva, non fate reset --hard alla cieca. Create un branch di salvataggio: non rompe nulla e protegge il commit trovato dall'eliminazione:
git branch rescue HEAD@{5}
git log --oneline rescue
git switch rescue
Poi i commit di rescue si possono trasferire con cherry-pick o unire con un merge, e il branch si può eliminare quando tutto è tornato al suo posto.
Per quanto tempo si conserva il reflog
Le voci non sono eterne. Le ripulisce git gc, che Git avvia periodicamente da solo. I tempi sono definiti dalle impostazioni:
| Impostazione | Predefinito | Significato |
|---|---|---|
gc.reflogExpire | 90 giorni | Scadenza per le voci con commit raggiungibili dai branch attuali |
gc.reflogExpireUnreachable | 30 giorni | Scadenza per le voci con commit non raggiungibili da nessun branch, cioè proprio quelli «persi» |
gc.pruneExpire | 2 settimane | Dopo quanto tempo vengono eliminati gli oggetti irraggiungibili a cui non fa più riferimento nulla |
In pratica: un commit perso è garantito accessibile tramite reflog per circa un mese. Non rimandate il recupero e non lanciate a mano git gc --prune=now o git reflog expire --expire=now --all finché non avete recuperato ciò che serve.
Se il reflog non aiuta: git fsck
Ci sono casi in cui il commit non è nel reflog: per esempio, l'ha creato git stash e poi lo stash è stato eliminato con git stash drop, oppure le modifiche erano solo nell'index. In questi casi cercate gli oggetti «pendenti» (dangling):
git fsck --lost-found
dangling commit 2b3c4d5e6f...
dangling blob 8e9f0a1b2c...
git show 2b3c4d5e6f # vedere il contenuto
git branch recovered 2b3c4d5e6f
I commit pendenti di uno stash eliminato contengono il messaggio «WIP on ...». I blob pendenti sono il contenuto di file che a un certo punto sono stati aggiunti con git add ma non committati. Non hanno un nome di file, quindi andranno riconosciuti dal contenuto, ma a volte così si riesce a salvare il lavoro dopo un reset --hard. Con il flag --lost-found Git li copia anche in .git/lost-found/.
Cosa il reflog non sa fare
- Non funziona tra macchine diverse. Se un collega ha fatto force push e ha cancellato i vostri commit sul server, cercateli nel reflog di chi li aveva in locale: per esempio con
git reflog show origin/mainda chiunque abbia fatto fetch prima della sovrascrittura. - Non vede ciò che non è committato. Per questo la migliore assicurazione prima di un'operazione rischiosa è un commit o
git stash. - Non conserva la cronologia per sempre. Valgono i tempi sopra, e un clone nuovo parte con un registro vuoto.
In pratica: un'assicurazione prima di un'operazione rischiosa
Il reflog salva, ma il recupero richiede attenzione: bisogna trovare la voce giusta tra decine di voci simili. È molto più semplice mettere un segnaposto in anticipo. Prima di un rebase, di un reset importante o di esperimenti con la cronologia, create un branch o un tag temporaneo:
git branch backup/before-rebase
git rebase -i main
# qualcosa è andato storto
git reset --hard backup/before-rebase
# è andato tutto bene
git branch -D backup/before-rebase
Costa un solo comando e vi evita la ricerca dell'hash. Il reflog resta il piano B per quando vi ricordate dell'assicurazione troppo tardi.
Come trovare velocemente la voce giusta
In un registro lungo aiuta filtrare. Cercate per tipo di operazione o per testo del messaggio di commit, e nei casi dubbi guardate il contenuto:
git reflog | grep -E "reset|rebase \(start\)"
git log -g --grep="validazione" --oneline
git show --stat HEAD@{7}
Il comando git log -g con --grep cerca nei messaggi dei commit del reflog, mentre git show --stat mostra quali file sono stati toccati: da questi è facile riconoscere il commit giusto, anche se il messaggio era poco informativo come «wip».
Riepilogo
git reflog è la rete di sicurezza di Git: tutto ciò che è stato committato nel vostro repository nelle ultime settimane si può trovare e recuperare. La procedura è sempre la stessa: aprire il registro, trovare la voce precedente all'errore, verificarla con git log o git show, poi creare un branch o fare reset --hard. Il lavoro con restore, reset, revert e il recupero dopo gli errori è affrontato in pratica nel corso su Git.
Domande frequenti
Che differenza c'è tra git reflog e git log?
git log segue i legami tra commit e genitori e mostra la cronologia del branch. git reflog mostra dove si sono spostati HEAD e i branch nel vostro repository, compresi i commit che non sono più nella cronologia dei branch.
Il reflog finisce su GitHub o GitLab?
No, il reflog è conservato solo nella cartella locale .git/logs e non viene trasmesso con push e clone.
Si può svuotare il reflog?
Sì, con git reflog expire --expire=now --all seguito da git gc --prune=now, ma dopo non sarà più possibile recuperare i commit persi.
Dove si trova fisicamente il reflog?
Nei file di testo .git/logs/HEAD e .git/logs/refs/heads/nome-branch, una riga per ogni spostamento.
Il reflog funziona in un repository bare sul server?
Per impostazione predefinita nei repository bare il reflog è disattivato (core.logAllRefUpdates), quindi sul server non ci si può contare.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Git da zero a sviluppatore sicuro».
Vai al corso