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.