Git, the stupid content tracker

V parte: blob e treedi

Git objects

Nella scorsa puntata, la quarta parte di questa serie (vedi menu a destra), abbiamo fatto la conoscenza di blob, tree e commit, tre dei quattro Git objects ossia i componenti alla base del sistema di storage utilizzato da Git.

In questo nuovo appuntamento cercheremo di capire meglio la struttura e il ruolo di blob e tree, al fine di apprendere i fondamenti su cui si erge il nostro amato sistema di versionamento.

Git fa uso di quattro differenti tipi di objects: commit, tree, blob e tag. Lasciamo da parte un attimo i tag — chi usa già un sistema di versionamento sa cosa sono — e i commit, e concentriamoci sui primi due.

 

I blob

I blob sono dei file binari, niente più e niente meno. Queste sequenze di byte, non interpretabili “ad occhio nudo”, conservano al loro interno le informazioni (compresse) appartenenti a un qualsiasi file, sia esso binario o testuale, si tratti di immagini, codice sorgente, archivi… Tutto viene compresso e trasformato in un blob prima di essere annesso a un repository Git.

Come già visto in precedenza, ogni file viene contrassegnato con un hash; questo hash identifica univocamente il file all’interno del nostro repository, ed è grazie a questo “id” che Git riesce poi a recuperarlo quando necessario e a rilevare eventuali modifiche quando lo stesso file viene alterato.

Hash univoci

Abbiamo detto che gli hash SHA-1 sono univoci; ma cosa vuol dire? Proviamo a capirlo meglio con un esempio.

Apriamo una shell Bash e proviamo a giocare un po’ con un altro comando di tipo plumbing, git hash-object:

[1] /home
$ echo "mokabyte" | git hash-object --stdin
4ee6192013be74aa054f203efd55a5e8d0f7c443

Il comando git hash-object è il comando deputato a calcolare l’hash di un qualsiasi oggetto; in questa occasione abbiamo sfruttato l’opzione --stdin per passare come argomento del comando il risultato del comando che lo precede, ossia echo “mokabyte”; in poche parole abbiamo calcolato l’hash della stringa “mokabyte”, ed è venuto fuori

4ee6192013be74aa054f203efd55a5e8d0f7c443

E sul vostro computer avete provato? Qual è il risultato? Un po’ di suspence… Incredibile, è lo stesso! Provate pure a rieseguire il comando quante volte volete, l’hash risultante sarà sempre il medesimo.

Questo ci fa capire una cosa molto importante: un oggetto, qualsiasi esso sia, avrà sempre lo stesso hash in qualsiasi repository, in qualsiasi computer sulla faccia della Terra. I più esperti e smaliziati probabilmente avevano già “mangiato la foglia” da un po’ di tempo, ma spero comunque di aver suscitato nel resto dei lettori lo stesso stupore che mi ha colto quando ho realizzato per la prima volta questo fatto, anche perché le implicazioni sono molteplici.

Analizzare gli objects

Ma facciamo un’altra prova; stavolta faremo uso del comando git cat-file, un altro comando plumbing che ci viene in soccorso quando si ha l’esigenza di analizzare gli objects.

Riprendiamo sempre il nostro vecchio repository — oppure ricreiamolo partendo dal precedente articolo — e andiamo a vedere l’hash del blob che identifica file01.txt, uno dei file committati nei nostri precedenti esperimenti. Ora creiamo un nuovo file, chiamato file99.txt, e al suo interno scriviamo la stessa cosa che c’è scritta in file01.txt: nel mio caso, “File 00”… scusate la poca fantasia.

[2] /home
$ cd es01/  #accediamo alla cartella del nostro vecchio repository
[3] /home/es01 (master)
$ git log   #usiamo git log per conoscere l’hash del primo commit
commit 0763bf3adfe5699ff890d535b69299e5db39b737
Author: Ferdinando Santacroce <jesus_was_rasta@yahoo.it>
Date:   Sat Feb 4 10:03:44 2017 +0100
Add a 2nd file
commit 4004a1197c9078a8a39e92734e939631926360fc
Author: Ferdinando Santacroce <jesus_was_rasta@yahoo.it>
Date:   Sat Feb 4 09:48:21 2017 +0100
First commit, file01
[4] /home/es01 (master)
$ git cat-file -p 4004a11    #verifichiamo il contenuto del primo commit
tree c24e325c8a82a3750bc7853552a08735ae5d494c
author Ferdinando Santacroce <jesus_was_rasta@yahoo.it> 1486198101 +0100
committer Ferdinando Santacroce <jesus_was_rasta@yahoo.it> 1486198101 +0100
First commit, file01
[5] /home/es01 (master)
$ git cat-file -p c24e325    #verifichiamo il contenuto del tree
100644 blob 4bc92d99949138eb83452d857e5c504471e30805   file01.txt
[6] /home/es01 (master)
$ cat file01.txt  #bingo! Verifichiamo il contenuto di file01.txt
File 00
[7] /home/es01 (master)
$ touch file99.txt     #creiamo un nuovo file
[8] /home/es01 (master)
$ vi file99.txt   #editiamolo, scrivendoci esattamente “File 00” come sopra
[9] /home/es01 (master)
$ git hash-object file99.txt #calcoliamone l’hash… et voilà!
4bc92d99949138eb83452d857e5c504471e30805

L’avete notato? L’hash di file99.txt è il medesimo di file01.txt!

Risparmio intelligente di spazio

Quando parlavo di implicazioni, intendevo proprio questo: Git si dimostra veramente efficace nel risparmiare spazio. Dato che esegue sempre il calcolo dell’hash prima di memorizzare un qualsiasi contenuto, nel momento in cui questo sarà lo stesso, Git “riciclerà” lo stesso blob, referenziando la medesima sequenza di byte in tutti i tree che conterranno questo file.

Verifichiamo?

[10] /home/es01 (master)
$ git status      #verifichiamo lo stato del repository
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
file99.txt
nothing added to commit but untracked files present (use "git add" to track)
[11] /home/es01 (master)
$ git add file99.txt   #aggiungiamo file99.txt alla staging area
[12] /home/es01 (master)
$ git commit -m "File 99"    #eseguiamo il commit
[master 21f5b03] File 99
1 file changed, 1 insertion(+)
create mode 100644 file99.txt
[13] /home/es01 (master)
$ git log --format=oneline   #verifichiamo i commit presenti nel repository
21f5b031a14d5a6573b281efce1e90e2ebada6d1 File 99
0763bf3adfe5699ff890d535b69299e5db39b737 Add a 2nd file
4004a1197c9078a8a39e92734e939631926360fc First commit, file01
[14] /home/es01 (master)
$ git cat-file -p 21f5   #analizziamo il contenuto dell’ultimo commit
tree ac330d0db71feeeb30878161c6a3262a96fa9206
parent 0763bf3adfe5699ff890d535b69299e5db39b737
author Ferdinando Santacroce <jesus_was_rasta@yahoo.it> 1490645210 +0200
committer Ferdinando Santacroce <jesus_was_rasta@yahoo.it> 1490645210 +0200
File 99
[15] /home/es01 (master)
$ git cat-file -p ac330 #vediamo cosa c’è nel tree…
100644 blob 4bc92d99949138eb83452d857e5c504471e30805   file01.txt
100644 blob 3068da2f51efe3cf462ba64c2bb77a8ee333297c   file02.txt
100644 blob 4bc92d99949138eb83452d857e5c504471e30805   file99.txt

Visto? All’interno del tree dell’ultimo commit Git elenca correttamente tutti e tre i file che attualmente fanno parte del nostro repository, ognuno con a fianco il proprio nome. La cosa simpatica è che file01.txt e file99.txt referenziano lo stesso blob, ossia lo stesso file; in sostanza, anche se “nel mondo reale” abbiamo 3 file distinti — ognuno con il suo bel “peso” in kilobyte — in Git sono archiviati solo due file su tre, dato che il contenuto di file01.txt e file99.txt è il medesimo.

Flessibilità del blob

E se il contenuto del file cambiasse?

[16] /home/es01 (master)
$ echo "Hey file99, it's time to grow up!" >> file99.txt
[17] /home/es01 (master)
$ git add file99.txt
[18] /home/es01 (master)
$ git commit -m "File99 modified"
[master d604050] File99 modified
1 file changed, 1 insertion(+)
[19] /home/es01 (master)
$ git log --format=oneline
d60405094aab74f81847169a4c1ec81334311b68 File99 modified
21f5b031a14d5a6573b281efce1e90e2ebada6d1 File 99
0763bf3adfe5699ff890d535b69299e5db39b737 Add a 2nd file
4004a1197c9078a8a39e92734e939631926360fc First commit, file01
[20] /home/es01 (master)
$ git cat-file -p d6040
tree 7c27b93a379d5c12d0ee3504ddb28378179104a7
parent 21f5b031a14d5a6573b281efce1e90e2ebada6d1
author Ferdinando Santacroce <jesus_was_rasta@yahoo.it> 1490646695 +0200
committer Ferdinando Santacroce <jesus_was_rasta@yahoo.it> 1490646695 +0200
File99 modified
[21] /home/es01 (master)
$ git cat-file -p 7c27b
100644 blob 4bc92d99949138eb83452d857e5c504471e30805   file01.txt
100644 blob 3068da2f51efe3cf462ba64c2bb77a8ee333297c   file02.txt
100644 blob b86472331ae95fbeadd30f0d017460a67f589eea   file99.txt

A questo punto non dovrebbero servire più spiegazioni: al variare del contenuto di un file, varia anche il suo hash: nel tree dell’ultimo commit infatti avrete notato che l’hash di file99.txt è variato. Ora Git nel suo archivio ha due file distinti, il file99.txt committato la prima volta e quello committato in ultima istanza, con tutte le sue belle differenze.

Ora che abbiamo un po’ più chiaro il ruolo dei blob, è il momento di spendere due parole sui tree.

 

I tree

I tree fungono da contenitori di blob e altri tree. Il modo più semplice per capirne il funzionamento è quello di pensare alle cartelle del proprio sistema operativo, che parimenti assolvono al compito di organizzare al loro interno file e altre sotto-cartelle.

Come tutti gli altri objects, anche i tree sono memorizzati come semplici file di testo zippati nella cartella .git/objects, e hanno un hash che li contraddistingue; anche per loro l’hash viene calcolato sulla base del contenuto del file che li rappresenta. Ne consegue che anche per quanto riguarda i tree, Git provvede a “riciclarli” non appena se ne presenta l’occasione.

Decomprimere un object

Parlando di file che rappresentano objects, mi rendo conto solo ora che ho dimenticato di dire una cosa importante, che val la pena sottolineare.

Usando il comando git cat-file -p abbiamo potuto “ficcare il naso” dentro i file degli objects; il comando in questione non fa altro che dezippare e mostrarci la parte più rilevante del contenuto del file decompresso: Git omette la sola riga di intestazione che contiene tipo e dimensione dell’object [01]. Questo significa che, se prendete un qualsiasi object e provate a decomprimerlo — usando la libreria zlib [02] — ne otterrete un semplice file di testo, che riporta le righe mostrate dal comando git cat-file.

Questo evidenzia ancora una volta la semplicità di Git: niente metadati, database interni o inutili complessità, ma semplici file e cartelle bastano per rendere possibile la gestione di un qualsiasi repository.

 

Conclusioni

E anche questa puntata è terminata. La prossima volta termineremo il discorso sugli objects, vedendo più nel dettaglio i commit. Proseguiremo poi introducendo le references, ossia le “bandierine” che Git ci consente di piantare ogni volta che vogliamo marcare un punto di particolare interesse nella storia del nostro repository.

 

Riferimenti

[01] Documentazione Git

https://git-scm.com/book/en/v2/Git-Internals-Git-Objects

 

[02] La libreria zlib per la compressione/decompressione

http://www.zlib.net/

 

Condividi

Pubblicato nel numero
227 aprile 2017
Ferdinando Santacroce lavora come programmatore presso Intré. Cominciò tutto quando, all’età di 13 anni, ricevette in regalo il suo primo computer, un Commodore64. Capì che la cosa era seria quando invece che giocare come tutti i suoi amici ai soliti giochini comprati in edicola, si divertiva a scrivere piccoli programmi…
Articoli nella stessa serie
Ti potrebbe interessare anche