This is an in-progress translation.
To help translate the book, please fork the book at GitHub and push your contributions.

Gli oggetti di Git

Git è un filesystem content-addressable. Magnifico. Ma che cosa significa? Significa che il cuore di Git è un semplice data store chiave-valore. Potete inserire qualsiasi tipo di contenuto al suo interno, e vi verrà restituita una chiave che potrete usare per recuperare quel contenuto quando vorrete. Come dimostrazione potete usare il comando plumbing hash-object, che accetta dei dati, li salva nella vostra directory .git e restituisce la chiave associata ai dati salvati. Per prima cosa inizializzate un nuovo repository Git e verificate che la directory objects non contiene nulla:

$ mkdir test
$ cd test
$ git init
Initialized empty Git repository in /tmp/test/.git/
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f
$

Git ha inizializzato la directory objects e creato le sottodirectory pack e info al suo interno, ma non ci sono file. Ora inseriamo del testo nel vostro database di Git:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

Il -w dice a hash-object di salvare l’oggetto; in caso contrario il comando restituirà semplicemente la chiave associata che verrebbe associata al soggetto. --stdin dice al comando di leggere il contenuto da stdin; se non lo specificate hash-object si aspetta il percorso di un file. L’output del comando è un checksum di 40 caratteri. La funzione di hashing è SHA-1 - un checksum del contenuto che viene salvato più un header, del quale imparerete di più tra poco. Ora potete vedere come Git ha salvato i vostri dati:

$ find .git/objects -type f 
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Nella directory objects ora è presente un file. Questo è come Git salva inizialmente il contenuto - ossia come un singolo file per ogni parte di contenuto, con nome uguale al checksum SHA-1 del contenuto stesso e del suo header. La subdirectory ha come nome i primi 2 caratteri dello SHA mentre il nome del file è costituito dai 38 caratteri rimanenti

Potete estrarre un contenuto da Git con il comando cat-file. Questo comando è una specie di coltellino svizzero per ispezionare gli oggetti Git. Passandogli -p è possibile istruire il comando cat-file a interpretare il tipo di contenuto e mostrarlvelo in modo leggibile:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

Ora potete aggiungere altro contenuto a Git ed estrarlo nuovamente. E’ possibile farlo anche con il contenuto dei file. E’ possibile, ad esempio, implementare un semplice controllo di versione su un file.

Come prima cosa create un nuovo file e salvate il suo contenuto nel database:

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt 
83baae61804e65cc73a7201a7252750c76066a30

Poi scrivete un nuovo contenuto nel file e salvatelo nuovamente:

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt 
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

Il vostro database ora contiente le due nuove versioni del file così come il primo contenuto che avete salvato:

$ find .git/objects -type f 
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Ora potete riportare il file alla prima versione:

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt 
$ cat test.txt 
version 1

o alla seconda:

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt 
$ cat test.txt 
version 2

Ricordare la chiave SHA-1 per ogni versione del vostro file non è pratico; inoltre non state salvando il nome del file nel vostro sistema - solamente il contenuto. Questo tipo di oggetto a chiamato blob. Potete fare in modo che Git vi restituisca il tipo di un oggetto al suo interno, data la sua chiave SHA-1 con cat-file -t:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

Oggetti Albero

Il prossimo argomento che guarderemo è l’albero degli oggetti, che risove il problema del salvataggio del nome del file e permette di salvare un gruppo di file insieme Git salva il contenuto in modo simile ad un filesystem UNIX, ma più semplificato. Tutto il contenuto è salvato come oggetti albero e blob, con gli alberi che corrispondono alle directory UNIX e i blob che corrispondono più o meno agli inode od i contenuti dei file. Un singolo oggetto albero contiene una o più voci, ognuna delle quali contiene un puntatore SHA-1 ad un blob od un sottoalbero con i relativi mode, type e nome del file. Ad esempio, l’albero più recente nel progetto simplegit può assogliare a questo:

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

La sintassi master^{tree} specifica che l’oggetto albero è puntato dall’ultima commit sul vostro branch master. Notate che la directory lib non è un blob ma un puntatore ad un’altro albero:

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb

Concettualmente, i dati che vengono salvati da Git sono simili a quelli in Figura 9-1.


Figura 9-1. Versione semplice del modello dei dati di Git.

Potete creare il vostro albero. Git normalmente crea un albero prendendo lo stato della vostra area di staging o indice e scrivendo l’oggetto albero a partire da questo. Quindi, per creare un oggetto albero dovete per prima cosa creare un indice mettendo in staging alcuni file. Per creare un indice con una singola voce - la prima versione del vostro file text.txt - potete usare il comando plumbing update-index. Usando questo comando aggiungete artificialmente la precedente versione el file text.txt ad una nuova area di staging. Dovete passare l’opzione --add perchè il file non esiste ancora nella vostra area di staging (in effetti non avete ancora un’area di staging) a l’opzione --cacheinfo perchè il file che state aggiungendo non è nella vostra directory ma è nel vostro database.

Per finire, specificate modo, SHA-1 ed il nome del file:

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

In questo caso, state specificando il modo 100644 il quale significa che si tratta di un normale file. Altre opzioni sono 100755, che significa che il file è eseguibile; e 120000, che specifica un link simbolico Il modo è preso dai normali modi UNIX, ma è molto meno flessibile - questi tre modi sono gli unici validi per i file (blob) in Git (anche se ci sono altri modi utilizzati per le directory ed i submodules).

Ora potete usare il comando write-tree per scrivere l’area di staging in un oggetto albero. L’opzione -w non è necessaria - l’esecuzione di write-tree crea automaticamente un oggetto albero a partire dallo stato dell’indice, se questo albero non è già esistente:

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

Potete anche verificare che si tratta di un oggetto albero:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

Ora creerete un nuovo albero con la seconda versione di test.txt e un nuovo file:

$ echo 'new file' > new.txt
$ git update-index test.txt 
$ git update-index --add new.txt 

La vostra area di staging ora contiene la nuova versione di test.txt così come il nuovo file new.txt Scrivete l’albero (registrando lo stato dell’area di staging o indice in un oggetto albero) e osservate a cosa assomiglia

$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

Notate che questo albero ha entrambe le voci ed anche che lo SHA di test.txt è lo SHA “versione 2” del precedente (1f7a7a). Solo per divertimento, aggiungerete il primo albero come sottodirectory di questo. Potete leggere gli alberi nella votra area di staging lanciando read-tree. In questo caso potete leggere un albero esistente nella vostra area di staging come sottoalbero utilizzando l’opzione --prefix di read-tree:

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

Se avete creato una directory di lavoro dal nuovo albero che avete appena scritto, otterrete i due file nel primo livello della directory e una sottodirectory chiamata bak che contiene la prima versione del file test.txt. Potete pensare che i dati contenuti da Git per questa strutture siano simili a quelli della Figura 9-2.


Figura 9-2. La struttura dei contenuti per i vostri dati di Git correnti.

Oggetti Commit

A questo punto avere tre alberi che specificano i diversi snapshot del vostro progetto del quale volete tenere traccia, ma il problema iniziale rimane: dovete ricordare tutti e tre i valori SHA-1 per poter recuperare gli snapshot. Non avete inoltre nessuna informazione su chi ha salvato gli snapshot, quando li ha salvati o perchè li ha salvati. Queste sono le informazioni di base che gli oggetti commit salvati per voi.

Per creare un oggetto commit, lanciate commit-tree e specificate un singolo albero SHA-1 e quale oggetto commit, se esiste, lo precede direttamente. Cominciate con il primo albero che avete scritto:

$ echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d

Ora potete analizzare il vostro nuovo oggetto commit con cat-file:

$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700

first commit

Il formato di un oggetto commit è semplice: specifica l’albero di primo livello per lo snapshot del progetto a quel punto; le informazioni sull’autore/colui che ha fatto la commit estratte dalle vostre impostazioni user.name and user.email, con il timestamp corrente; una linea vuota ed infine il messaggio di commit.

Di seguito, scrivete gli altri due oggetti commit, ognuno dei quali fa riferimento alla commit che le hanno preceduti:

$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit'  | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9

Ognuno dei tre oggetti commit punta ad uno dei tre alberi snapshot che avete creato. Ora avete una vera Git history che potete vedere con il comando git log, se lo lanciate sull’ultima commit SHA-1:

$ git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700

    third commit

 bak/test.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:14:29 2009 -0700

    second commit

 new.txt  |    1 +
 test.txt |    2 +-
 2 files changed, 2 insertions(+), 1 deletions(-)

commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:09:34 2009 -0700

    first commit

 test.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

Fantastico. Avete appena eseguito le operazioni di basso livello per costruire una Git history senza utilizzare nessuno dei comandi del front end. Questo è essenzialmente quello che Git fà quando lanciate i comandi git add e git commit - salva i blob per i file che sono cambiati, aggiorna l’indice, scrive gli alberi e scrive gli oggetti commit che fanno riferimento agli alberi di primo livello e le commit fatte immediatamente prima di questi. Questi tre oggetti Git principali - il blob, l’albero, e la commit - sono inizialmente slavati come file separati nella vostra directory .git/objects. Di seguito potete vedere tutti gli oggetti nella directory di esempio, commentati con quello che contengono:

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

Se seguite tutti i puntatori interni otterrete un grafo degli oggetti simile a quelli in Figura 9-3.


Figura 9-3. Tutti gli oggetti nella vostra directory Git.

Il salvataggio degli oggetti

In precendeza ho menzionato il fatto che insieme ad un contenuto viene salvato anche un header. Prendiamoci un minuto per capire come Git salva i propri oggetti. Vedrete come salvare un oggetto blob - in questo caso, la stringa “what is up, doc?” - intrerattivamente con il linguaggio di scripting Ruby. Potete lanciare Ruby in modalità interattiva con il comando irb:

$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"

Git costruisce un header che comincia con il tipo dell’oggetto, in questo caso un blob. Poi aggiunge uno spazio seguito dalla dimensione del contenuto ed infine da un null byte:

>> header = "blob #{content.length}\0"
=> "blob 16\000"

Git concatena header e contenuto originle e calcola il checksum SHA-1 del nuovo contenuto. Potete calcolare lo SHA-1 di una stringa in Ruby includendo la libreria SHA1 digest con il comando require e invocando Digest::SHA1.hexdigest():

>> store = header + content
=> "blob 16\000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"

Git comprime il nouvo contenuto con zlib, cosa che potete fare in Ruby con la libreria zlib. Prima avrete bisogno di includere la libreria ed invocare Zlib::Deflate.deflate() sul contenuto:

>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\234K\312\311OR04c(\317H,Q\310,V(-\320QH\311O\266\a\000_\034\a\235"

Infine, scriverete il vostro contenuto zlib-deflated in un oggetto sul disco. Determinerete il percorso dell’oggetto che volete scrivere (i primi due caratteri dello SHA-1 sono il nome della sottodirectory e gli ultimi 38 caratteri sono il nome del file contenuto in quella directory). I Ruby, potete usare la funzione FileUtils.mkdir_p() per creare la sottodirectory se questa non esiste. Di seguito aprite il file con File.open() e scrivete il contenuto ottenuto in precedenza nel file chiamando write() sul file handler risultante:

>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
=> true
>> FileUtils.mkdir_p(File.dirname(path))
=> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
=> 32

Questoè tutto - avete creato un oggetto Git di tipo blob valido. Tutti gli oggetti Git sono salvati nello stesso modo, solo con tipi differenti - invece della stringa “blob” l’header comincierà con “commit” o “tree”. Inoltre, seppure il contenuto blob può essere praticamente qualsiasi cosa, i contenuti commit e tree sono formttati in modo molto specifico.