Full Stack

Come si usa reflog in Git?

Autore

Manuel Ricci

Ormai è arcinoto che con Git abbiamo tutti gli strumenti per poter mantenere una cronologia delle modifiche di un progetto e usarla a nostro vantaggio in molteplici occasioni.

Bisogna però considerare che le capacità di modifica che ci vengono fornite da Git stesso possono portare a risultati inattesi.

Quando lavoriamo su una repo locale, Git tiene traccia di ogni singola modifica che influenza la posizione delle referenze. Queste modifiche vengono raccolte in uno speciale tipo di log, chiamato reflog.

Reflog è disponibile solo nelle repo locali (non viene condiviso con le repository remote) e funge da rete di sicurezza dato che ci permette, insieme ai comandi git reset, git checkout e git cherry-pick, di ripristinare il lavoro ad uno stato precedente, anche se questo sembrava perduto per sempre.

Prima di procedere con reflog, però, è giusto fare un piccolo ripasso sulle refs, perché in fondo è quello di cui si parla.

Breve ripasso sulle references

Altro fatto arcinoto è che se vogliamo fare una modifica ad un oggetto in Git, necessitiamo del suo hash. Per evitare di ricordare tutti gli hash, in alcuni casi Git interviene con delle referenze o refs (diminutivo di references).

Una referenza non è altro che un file memorizzato nella cartella .git/refs, il quale a sua volta contiene l’hash di un oggetto commit.

Questo è il contenuto della directory di lavoro che ho clonato durante la lezione sulla gestione delle repo remote

1$ ls -l .git/refs
2drwxr-xr-x 2 manuel manuel 4096 Jul 23 15:05 heads
3drwxr-xr-x 3 manuel manuel 4096 Jul 23 14:50 remotes
4drwxr-xr-x 2 manuel manuel 4096 Jul 23 14:50 tags

Nella directory heads sono presenti tutte le branch locali. Ogni file corrisponde ad una branch e dentro i singoli file si potrà leggere l’hash della commit.

La commit alla quale fa riferimento è quella alla quale punta la ref in questione (ricordate che le branch sono dei puntatori), quella che vediamo nell’editor qualora facessimo il checkout a quella determinata branch.

1$ cat .git/refs/heads/main
226a8685c3de9cbd398903c12047b8ffc08645174

Lo stesso hash lo troveremo in git log.

1$ git log --oneline
226a8685 (HEAD -> main) Aggiunto testo

Per le directory tags e remotes il discorso è lo stesso, ma gli hash nei vari file faranno riferimento ai tag e alle repository remote.

Oltre alle referenze presenti nella cartella refs, ci sono anche dei file presenti nella .git directory e sono:

  • HEAD = La commit attualmente in checkout
  • FETCH_HEAD = La branch remota che è stata scaricata di recente
  • ORIG_HEAD = Una reference di backup di HEAD nel caso venga fatto qualcosa di drastico
  • MERGE_HEAD = Le commit che sto per unire nella branch corrente con git merge
  • CHERRY_PICK_HEAD = La commit sottoposta a cherry-picking

Prendendo in esame HEAD, il file presente in .git, non contiene un hash, ma una referenza simbolica ad un file:

1$ cat .git/HEAD
2ref: refs/heads/main

Il file in .git/refs/heads/main contiene l’hash della commit alla quale fa riferimento.

E reflog?

La documentazione definisce il reflog come il log che registra quando il puntatore di una branch o altre referenze vengono aggiornate nella repository locale. Ma cerchiamo di andare più nel dettaglio.

Il comando per visualizzare il reflog è il seguente:

1$ git reflog
253998a8 (HEAD -> main) HEAD@{0}: commit: Aggiunto footer
39da1f25 HEAD@{1}: pull: Fast-forward
4c7a5ca3 HEAD@{2}: merge origin/main: Fast-forward
559b4f92 HEAD@{3}: clone: from /home/manuel/code/corso-git/rebase

L’output che vediamo è molto simile ad un git log, ma ci sono delle informazioni che non ci tornano, vediamo un pò:

  1. 53998a8 è l’hash della commit
  2. (HEAD -> main) decoratore che ci dice che HEAD punta a main
  3. La notazione HEAD@{x} serve a specificare la posizione del puntatore HEAD. Possiamo tradurla come si trovava "dove HEAD x modifiche fa". 0 è il più recente. Hash della commit
  4. Evento e ulteriori informazioni relative ad esso.

Qualora si volesse specificare per quale referenza si vuole visualizzare reflog, si può sempre aggiungere al termine del comando in questo modo:

1$ git reflog <refs>

Dove refs potrebbe essere una branch.

Detto questo quindi cerchiamo di vedere quali potrebbero essere i casi d’uso più frequenti dove reflog può salvarci le penne.

Come ripristinare una commit con reflog

Abbiamo già visto in precedenza come si fa ad annullare una commit, ma se quel ripristino non doveva essere fatto?

N.B. Questo sistema funziona molto bene quando NON sono state fatte altre commit tra la commit eliminata e quella dove ci troviamo ora.

Qualora invece fossero presenti più commit, possiamo ricorrere al comando git cherry-pick (ne parlo brevemente tra poco).

Per testare questo scenario, possiamo creare una nuova repo e fare qualche commit.

1$ git add .
2$ git commit -m 'Prima commit'
3[main (root-commit) d99f811] Prima commit
4 1 file changed, 1 insertions(+), 0 deletions(-)
5 create mode 100644 index.txt
6 
7$ git commit -am 'Aggiunto documento privacy'
8[main b949949] Aggiunto documento privacy
9 1 file changed, 1 insertion(+)
10 
11$ git log --oneline
12b949949 (HEAD -> main) Aggiunto documento privacy
13d99f811 Prima commit
14 
15$ git reflog
16b949949 (HEAD -> main) HEAD@{0}: commit: Aggiunto documento privacy
17d99f811 HEAD@{1}: commit (initial): Prima commit
18 
19$ git reset --hard HEAD~1
20HEAD is now at d99f811 Prima commit
21$ git log --oneline
22d99f811 (HEAD -> main) Prima commit
23 
24$ git reflog
25d99f811 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1
26b949949 HEAD@{1}: commit: Aggiunto documento privacy
27d99f811 (HEAD -> main) HEAD@{2}: commit (initial): Prima commit

Come potete osservare contiene i dati anche della commit che abbiamo cancellato. Possiamo quindi usare quell’hash per poter effettuare il ripristino.

1$ git reset --hard b949949
2HEAD is now at b949949 Aggiunto documento privacy
3 
4$ git log --oneline
5b949949 (HEAD -> main) Aggiunto documento privacy
6d99f811 Prima commit

Se avessimo voluto usare HEAD@{x} avremmo potuto scrivere git reset --hard HEAD@{1} e avremmo ottenuto il medesimo risultato.

Come menzionavo in precedenza se tra la commit da recuperare e la HEAD sono state fatte altre commit conviene usare il comando git cherry-pick.

1git cherry-pick b949949

Una piccola parentesi sul comando cherry-pick

1$ git cherry-pick <hash>

Cherry-pick è un comando molto potente che consente di selezionare delle commit per referenza e aggiungerle alla HEAD di lavoro corrente.

Il comando è utile quando magari si fa una commit su una branch sbagliata o, come nel nostro caso, si perde una commit.

Ricordate però che per quanto sia uno strumento potente, non è sempre una best practice. Ci sono scenari e scenari. La commit persa è uno di quegli scenari.

Ripristinare una branch cancellata con reflog

Altro caso in cui reflog torna estremamente utile è quello in cui dobbiamo ripristinare una branch cancellata.

Prendendo in considerazione quanto fatto in precedenza procediamo con la creazione di una nuova branch e facciamo qualche commit.

1$ git checkout -b test
2Switched to a new branch ‘test3 
4$ git commit -am 'Test 1'
5[test 2a4329f] Test 1
6 1 file changed, 1 insertions(+), 0 deletion(-)
7 
8$ git commit -am 'Test 2'
9[test 8fd8ba7] Test 2
10 1 file changed, 1 insertions(+), 0 deletion(-)
11 
12$ git switch main
13Switched to branch main.'
14 
15$ git branch -D test
16Deleted branch test (was 8fd8ba7).
17 
18$ git log --oneline
19b949949 (HEAD -> main) Aggiunto documento privacy
20d99f811 Prima commit
21 
22$ git reflog
23d99f811 (HEAD -> main) HEAD@{0}: checkout: moving from test to main
248fd8ba7 HEAD@{1}: commit: Test 2
252a4329f HEAD@{2}: commit: Test 1
26e99485b (HEAD -> main) HEAD@{3}: checkout: moving from main to test
27d99f811 (HEAD -> main) HEAD@{4}: reset: moving to HEAD~1
28b949949 HEAD@{5}: commit: Aggiunto documento privacy
29d99f811 (HEAD -> main) HEAD@{6}: commit (initial): Prima commit
30
31
32 
33$ git checkout -b test HEAD@{1}
34Switched to a new branch ‘test.'
35 
36$ git log --oneline
378fd8ba7 (HEAD -> test) Test 2
382a4329f Test 1
39d99f811 (main) Prima commit

Annullare un rebase con reflog

Terza casistica per cui reflog può essere estremamente utile e che avevo già anticipato durante la lezione sul come si usa git rebase e quella in cui vogliamo annullare un rebase.

La procedura è molto simile a quella vista in precedenza. Si usa reflog per individuare il punto immediatamente prima del rebase e si fa un git reset a quel punto.

Vediamo un esempio pratico su una repository inizializzata ex-novo:

1$ git checkout -b test
2Switched to a new branch ‘test'
3
4$ git commit -am 'Test 1'
5[test 1538578] Test 1
6 1 file changed, 1 insertions(+), 0 deletion(-)
7
8$ git switch main
9Switched to branch ‘main’
10
11$ git commit -am 'Main commit'
12[main 33d2884] Main commit.
13 1 file changed, 1 insertion(+), 0 deletion(-)
14
15$ git switch test
16Switched to branch ‘test'
17
18$ git rebase main
19
20# Dovrebbe crearsi un conflitto, a meno che non abbiate usato un file differente, qualora ci fosse il conflitto risolvetelo e continuate.
21
22$ git add .
23$ git rebase --continue
24[detached HEAD 377106e] Test 1
25 1 file changed, 1 insertions(+), 0 deletion(-)
26Successfully rebased and updated refs/heads/test.
27
28$ git reflog
29377106e (HEAD -> test) HEAD@{0}: rebase (continue) (finish): returning to refs/heads/test
30377106e (HEAD -> test) HEAD@{1}: rebase (continue): Test 1
3133d2884 (main) HEAD@{2}: rebase (start): checkout main
321538578 HEAD@{3}: checkout: moving from main to test
3333d2884 (main) HEAD@{4}: commit: Main commit.
34f490ae7 HEAD@{5}: checkout: moving from test to main
351538578 HEAD@{6}: commit: Test 1
36f490ae7 HEAD@{7}: checkout: moving from main to test
37f490ae7 HEAD@{8}: commit (initial): Prima commit
38
39$ git reset --hard HEAD@{6}
40HEAD is now at 1538578 Test 1
41
42$ git log --oneline
431538578 (HEAD -> feature) Test 1
44f490ae7 Prima commit

Dove si trova reflog?

Quando abbiamo esplorato la .git directory abbiamo visto la cartella logs. Lì è dove risiedono i log di reflog, per la precisione nella directory .git/logs/refs..

Ricordate, reflog è disponibile solo a livello locale, non è condiviso con altri.

Qual è la differenza tra log e reflog?

La differenza tra log e reflog è molto semplice:

  • reflog è locale e tiene traccia di tutto, cancellazioni incluse.
  • log è pubblico, viene trasferito con la clonazione e tiene traccia solo delle modifiche nelle varie branch.

Ovviamente, come in altri comandi in Git ci sono delle funzionalità che si accavallano. Ad esempio se provassimo ad eseguire il comando git log -g verrebbe mostrato un output tipico di git log, ma se facciamo attenzione il contenuto è quello di reflog.

Ci sono dei limiti nell’utilizzare reflog?

Sì. Di default reflog va indietro fino a 90 giorni. Valore modificabile configurando il garbage collector di Git.

Dopo i 90 giorni di default, ciò che viene eliminato dal garbage collector non può essere ripristinato.

Conclusioni

Con questo si conclude il corso per principianti di Git, è stata una bella avventura.

Spero il corso ti sia stato utile e che ti sia divertito ad imparare quanto io ad insegnare.

Se hai 2 minuti vorrei sapere la tua opinione per poter migliorare la qualità in futuro. Il feedback è anonimo e per inviarmelo puoi compilare questo modulo Google. Grazie!

Al prossimo corso.

Caricamento...

Diventiamo amici di penna? ✒️

Iscriviti alla newsletter per ricevere una mail ogni paio di settimane con le ultime novità, gli ultimi approfondimenti e gli ultimi corsi gratuiti puubblicati. Ogni tanto potrei scrivere qualcosa di commerciale, ma solo se mi autorizzi, altrimenti non ti disturberò oltre.

Se non ti piace la newsletter ti ricordo la community su Discord, dove puoi chiedere aiuto, fare domande e condividere le tue esperienze (ma soprattutto scambiare due meme con me). Ti aspetto!

Ho in previsione di mandarti una newsletter ogni due settimane e una commerciale quando capita.