Guida

git reflog: come recuperare commit e branch persi

Aggiornato: · Skillaria.Top

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.

  1. Aprite il registro: git reflog.
  2. Trovate la riga subito prima di reset: moving to ...: è lo stato precedente all'errore. Nell'esempio sopra è HEAD@{1} con hash 9a8b7c6.
  3. Verificate che sia quello giusto: git log --oneline -5 HEAD@{1}.
  4. 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:

ImpostazionePredefinitoSignificato
gc.reflogExpire90 giorniScadenza per le voci con commit raggiungibili dai branch attuali
gc.reflogExpireUnreachable30 giorniScadenza per le voci con commit non raggiungibili da nessun branch, cioè proprio quelli «persi»
gc.pruneExpire2 settimaneDopo 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/main da 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».

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.

Impara l’argomento in video

Questo argomento è trattato in modo approfondito, con pratica, nel corso «Git da zero a sviluppatore sicuro».

Vai al corso