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ò:
53998a8
è l’hash della commit(HEAD -> main)
decoratore che ci dice che HEAD punta a main- La notazione
HEAD@{x}
serve a specificare la posizione del puntatoreHEAD
. Possiamo tradurla come si trovava "doveHEAD
x modifiche fa". 0 è il più recente. Hash della commit - 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 ‘test’
3
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!