"Ho perso tutto" (spoiler: probabilmente no)
Respira. Se il lavoro che ti manca è stato committato almeno una volta -- anche una sola, anche sul branch sbagliato, anche in un commit che poi hai "cancellato" -- quasi sicuramente è ancora lì, nel tuo repository, in questo preciso momento. Git è molto più bravo a conservare le cose che a perderle, e i prossimi dieci minuti di lettura molto probabilmente finiranno con il tuo codice di nuovo sullo schermo.
Ecco la verità rassicurante su come funziona Git internamente: Git non cancella quasi mai nulla subito. Ogni commit che fai viene salvato come oggetto nella directory .git, identificato dal suo SHA. Quando esegui git reset --hard, cancelli un branch o riscrivi la storia con un rebase, Git non elimina quegli oggetti commit. Sposta soltanto dei puntatori -- i nomi dei branch e HEAD -- in modo che non li puntino più. I commit diventano "irraggiungibili", cioè nessun branch o tag porta più a loro, ma gli oggetti restano su disco per settimane prima ancora che la garbage collection pensi di toccarli.
Quindi il problema non è che i tuoi commit sono spariti. Il problema è che non hai più un nome per trovarli. Ti serve lo SHA. E Git tiene un diario che ha registrato in silenzio ogni SHA che il tuo repository ha visitato: il reflog.
Che cos'è davvero il reflog
Il reflog (reference log) è un diario locale, per singola macchina, di dove HEAD e la punta di ogni branch hanno puntato nel tempo. Ogni volta che HEAD si sposta -- perché fai commit, checkout, merge, rebase, reset o amend -- Git aggiunge una riga a questo diario. È la scatola nera del tuo repository.
Per vederlo, esegui:
git reflog
L'output ha questo aspetto:
e4f5a6b HEAD@{0}: reset: moving to HEAD~3
a1b2c3d HEAD@{1}: commit: add payment validation
9f8e7d6 HEAD@{2}: commit: refactor checkout flow
3c4d5e6 HEAD@{3}: checkout: moving from main to feature/payments
Leggilo dall'alto verso il basso come "prima il più recente". Ogni riga mostra lo SHA a cui puntava HEAD, un riferimento posizionale come HEAD@{1}, l'operazione che ha spostato HEAD e una breve descrizione. La sintassi HEAD@{n} significa "dove si trovava HEAD n spostamenti fa": HEAD@{0} è dove sei adesso, HEAD@{1} è un'operazione indietro, e così via. Puoi usare questi riferimenti in qualsiasi comando che accetti un commit, ed è esattamente questo a rendere le operazioni di salvataggio così dirette.
Nell'esempio qui sopra la storia è chiara: hai fatto due commit (HEAD@{2} e HEAD@{1}), poi un reset ha spostato HEAD tre commit indietro. Quei due commit non sono persi -- sono lì, a a1b2c3d.
HEAD non è l'unico riferimento ad avere un log. Ogni branch tiene il suo:
git reflog show feature/payments
Questo mostra ogni posizione occupata dalla punta di feature/payments -- utile quando vuoi la storia di un solo branch senza il rumore di ogni checkout che hai mai fatto.
Scenari di salvataggio, passo per passo
1. Sei andato troppo in là con git reset --hard
Volevi annullare un commit e ne hai spazzati via tre, oppure hai fatto reset nel punto completamente sbagliato. Il reflog ha registrato dove ti trovavi un attimo prima del reset, e quella posizione è HEAD@{1}:
git reset --hard HEAD@{1}
Tutto qui. Il reset stesso era solo un altro spostamento di HEAD, quindi riportare HEAD alla posizione precedente ripristina tutto. Se dopo il reset sbagliato hai fatto altre operazioni, esegui prima git reflog, trova la riga subito prima della voce del reset e fai reset a quello SHA. Per una panoramica più ampia delle strategie di annullamento, leggi come annullare un commit Git.
2. Hai cancellato un branch
Cancellare un branch cancella il puntatore, non i commit. Se su questa macchina hai fatto di recente checkout o commit su quel branch, la sua punta è nel reflog di HEAD. Trovala:
git reflog | grep "feature/payments"
Cerca l'ultimo commit fatto su quel branch, oppure l'ultima voce checkout: moving from feature/payments -- lo SHA su quella riga è la punta del branch. Poi ricrea il branch facendolo puntare lì:
git branch feature/payments a1b2c3d
Il branch è tornato, identico al momento in cui è stato cancellato. Bonus: quando cancelli un branch, Git stampa lo SHA della sua punta (Deleted branch feature/payments (was a1b2c3d)) -- se quel messaggio è ancora nello scrollback del tuo terminale, puoi saltare del tutto la ricerca nel reflog.
3. Un rebase è andato storto
Un rebase riscrive i commit, e a metà di uno pieno di conflitti può sembrarti che il branch sia rovinato senza rimedio. Non lo è: lo stato pre-rebase è nel reflog. Se il rebase è ancora in corso, l'uscita più pulita è git rebase --abort. Se è già terminato e il risultato non ti piace, trova la voce precedente al suo inizio:
git reflog
# cerca: "rebase (start)" -- la voce subito SOTTO
# è dove si trovava il tuo branch prima del rebase
git reset --hard HEAD@{5}
(Sostituisci {5} con la posizione che la voce pre-rebase occupa nel tuo reflog.) Il tuo branch è esattamente com'era. Se i rebase ti mettono spesso in questa situazione, la nostra guida al rebase interattivo senza paura spiega come renderli routine invece che rischio.
4. Un commit è sparito dopo --amend
git commit --amend non modifica un commit -- ne crea uno nuovo e ci sposta sopra il branch. L'originale è irraggiungibile ma intatto, e subito dopo l'amend si trova a HEAD@{1}. Per esaminarlo:
git show HEAD@{1}
Se hai fatto amend del commit sbagliato o ti serve l'originale, fai reset a quel commit oppure fanne cherry-pick dove deve stare.
5. Hai fatto commit su un HEAD detached, poi sei passato altrove
Hai fatto checkout di un commit specifico o di un tag, lì hai creato dei commit in stato di detached HEAD, poi hai fatto checkout di un branch -- e i tuoi commit sembrano evaporati. Git ti avvisa persino quando cambi posizione, stampando lo SHA orfano. In ogni caso il reflog li ha registrati:
git reflog
# trova l'ultimo commit che hai fatto prima della
# voce "checkout: moving from <sha> to <branch>"
git branch rescued-work e4f5a6b
I tuoi commit detached ora vivono su un branch vero, e puoi farne merge o rebase ovunque debbano andare.
Ispeziona prima di ripristinare
Prima di puntare qualsiasi cosa a uno SHA recuperato, guardalo. Conferma che sia davvero il commit che pensi:
git show a1b2c3d
Questo stampa messaggio del commit, autore, data e diff completo. Se vuoi sfogliare il reflog con i dettagli completi dei commit invece delle voci su una riga, usa la modalità reflog del log:
git log -g
Ed ecco l'abitudine più sicura di tutte: invece di fare reset del branch corrente a un commit recuperato, prima creaci sopra un branch temporaneo:
git branch rescue a1b2c3d
Ora il lavoro recuperato ha un nome permanente e raggiungibile. Il tuo branch corrente non è cambiato di una virgola, la garbage collection non potrà mai toccare i commit salvati, e puoi ispezionare, fare diff e merge con tutta calma. Un branch è gratis: usane uno ogni volta che non sei sicuro al 100%.
I limiti, onestamente
Il reflog è una rete di sicurezza notevole, ma ha confini reali, ed è meglio conoscerli prima di averne bisogno:
- È strettamente locale. Il reflog vive sulla tua macchina e non viene mai incluso in push, pull o clone. Un clone fresco parte con un reflog vuoto. Se hai perso commit sul tuo laptop, è il reflog del tuo laptop quello che conta -- il clone di un collega non può aiutarti, e nemmeno la copia sul server.
- Le voci scadono. Di default, le voci del reflog per commit ancora raggiungibili da un branch scadono dopo 90 giorni, mentre quelle per commit irraggiungibili dopo 30. Dopo la scadenza, la garbage collection può cancellare davvero gli oggetti irraggiungibili. In pratica è tempo più che sufficiente -- ma significa che il reflog salva l'errore del mese scorso, non quello dell'anno scorso.
- Non può recuperare ciò che non è mai stato committato. Le modifiche non committate distrutte da
git reset --hardogit checkout -- <file>non sono mai state oggetti Git, quindi nessun diario le ha registrate. L'unica eccezione parziale: i file che erano almeno in staging congit addesistono come oggetti blob, egit fsck --lost-foundpuò ripescare quei blob pendenti dentro.git/lost-found/-- contenuto senza nomi di file né storia, ma meglio di niente. È un'ultima spiaggia, non un workflow. - Gli stash droppati hanno la loro via di fuga. Uno stash che hai droppato o svuotato non è nel reflog di HEAD, ma i commit dello stash spesso sopravvivono come oggetti pendenti.
git fsck --unreachable | grep commitpiùgit showsui candidati possono trovarli. Se usi molto lo stash, la nostra guida a git stash copre i workflow più sicuri.
Prevenzione: rendi noiosi i panici futuri
Ogni scenario qui sopra aveva un prerequisito: il lavoro era stato committato. Da qui l'unico consiglio di prevenzione che conta davvero: fai commit presto e fai commit spesso. Commit piccoli, grezzi, work-in-progress non costano nulla -- puoi farne squash, riscriverne i messaggi o riordinarli più tardi. Nel momento in cui qualcosa è committato, è nel database degli oggetti e il reflog gli copre le spalle. Nel momento in cui non lo è, ti stai affidando alla fortuna.
Per i cambi di contesto rapidi in cui un commit sembra troppo pesante, usa lo stash invece di trascinarti modifiche non committate tra un checkout e l'altro -- anche uno stash è un oggetto vero che Git può ritrovare.
Dove entra in gioco GitSquid (e dove vince la CLI)
Risposta onesta: il reflog è uno di quei posti dove la riga di comando è lo strumento giusto. GitSquid non ha un browser del reflog, e quando ti serve git reflog dovresti aprire un terminale e usarlo -- i comandi di questo articolo sono il percorso di recupero.
Quello che GitSquid fa, invece, è rendere più sicuro il workflow attorno, così avrai bisogno del reflog meno spesso. La timeline dei file (clic destro su qualsiasi file e scegli Show history) ti dà una storia visuale per singolo file, che spesso risponde a "dov'è finito quel codice?" senza nessun recupero. Le operazioni distruttive come reset e cancellazione di branch passano per menu contestuali espliciti sul grafo dei commit, così vedi esattamente cosa stai per fare prima di farlo. E quando stai ricostruendo "cos'è appena successo", il log dei comandi (Cmd/Ctrl+Shift+L) mostra ogni comando Git che GitSquid ha eseguito sul tuo repository, con argomenti completi e codici di uscita -- che è esattamente la prova che vuoi accanto a un reflog quando ricostruisci un incidente.
Riferimento rapido
| Situazione | Comando |
|---|---|
| Vedere dove è stato HEAD | git reflog |
| Vedere dove è stata la punta di un branch | git reflog show <branch> |
Annullare un reset --hard sbagliato |
git reset --hard HEAD@{1} |
| Ripristinare un branch cancellato | git branch <name> <sha> |
| Annullare un rebase concluso | git reset --hard HEAD@{n} (voce pre-rebase) |
| Ispezionare un commit recuperato | git show <sha> |
| Sfogliare il reflog con tutti i dettagli | git log -g |
| Parcheggiare un salvataggio al sicuro | git branch rescue <sha> |
| Ultima spiaggia per file in staging mai committati | git fsck --lost-found |
Il reflog trasforma "ho perso tutto" in "ho smarrito un puntatore per dieci minuti". Fai commit spesso, controlla il reflog prima di farti prendere dal panico e parcheggia i recuperi su un branch temporaneo prima di toccare qualsiasi altra cosa. E se vuoi un client Git che rende esplicite le operazioni distruttive e tiene un log completo di ogni comando che esegue, scarica GitSquid gratis -- non sostituirà il tuo reflog, ma ti aiuterà ad averne bisogno meno spesso.