1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
|
== Segreti rivelati ==
Diamo ora un'occhiata sotto il cofano e cerchiamo di capire come Git
realizza i suoi miracoli. Per una descrizione approfondita fate
riferimento al
http://www.kernel.org/pub/software/scm/git/docs/user-manual.html[manuale
utente].
=== Invisibilità ===
Come fa Git ad essere così discreto? A parte qualche commit o merge
occasionale, potreste lavorare come se il controllo di versione non
esistesse. Vale a dire fino a che non è necessario, nel qual caso sarete
felici che Git stava tenendo tutto sotto controllo per tutto il tempo.
Altri sistemi di controllo di versione vi forzano costantemente a
confrontarvi con scartoffie e burocrazia. File possono essere solo
acceduti in lettura, a meno che non dite esplicitamente al server
centrale quali file intendete modificare. I comandi di base soffrono
progressivamente di problemi di performance all'aumentare del numero
utenti. Il lavoro si arresta quando la rete o il server centrale hanno
problemi.
In contrasto, Git conserva tutta la storia del vostro progetto nella
sottocartella `.git` della vostra cartella di lavoro. Questa è la vostra
copia personale della storia e potete quindi rimanere offline fino a che
non volete comunicare con altri. Avete controllo totale sul fato dei
vostri file perché Git può ricrearli ad ogni momento a partire da uno
stato salvato in `.git`.
=== Integrità ===
La maggior parte della gente associa la crittografia con la
conservazione di informazioni segrete ma un altro dei suoi importanti
scopi è di conservare l'integrità di queste informazioni. Un uso
appropriato di funzioni hash crittografiche può prevenire la corruzione
accidentale e dolosa di dati.
Un codice hash SHA1 può essere visto come un codice unico di
identificazione di 160 bit per ogni stringa di byte concepibile.
Visto che un codice SHA1 è lui stesso una stringa di byte, possiamo
calcolare un codice hash di stringe di byte che contengono altri codici
hash. Questa semplice osservazione è sorprendentemente utile: cercate ad
esempio 'hash chains'. Più tardi vedremo come Git usa questa tecnica per
garantire efficientemente l'integrità di dati.
Brevemente, Git conserva i vostri dati nella sottocartella
`.git/objects`, ma invece di normali nomi di file vi troverete solo dei
codici. Utilizzando questi codici come nomi dei file, e grazie a qualche
trucco basato sull'uso di 'lockfile' e 'timestamping', Git trasforma un
semplice sistema di file in un database efficiente e robusto.
=== Intelligenza ===
Come fa Git a sapere che avete rinominato un file anche se non
gliel'avete mai detto esplicitamente? È vero, magari avete usato *git
mv*, ma questo è esattamente la stessa cosa che usare *git rm* seguito
da *git add*.
Git possiede dei metodi euristici stanare cambiamenti di nomi e copie
tra versioni successive. Infatti, può addirittura identificare lo
spostamento di parti di codice da un file ad un altro! Pur non potendo
coprire tutti i casi, questo funziona molto bene e sta sempre
costantemente migliorando. Se non dovesse funzionare per voi, provate
le opzioni che attivano metodi di rilevamento di copie più impegnative,
e considerate l'eventualità di fare un aggiornamento
=== Indicizzazione ===
Per ogni file in gestione, Git memorizza delle informazioni, come la sua
taglia su disco, e le date di creazione e ultima modifica, un file detto
'indice'. Per determinare su un file è stato cambiato, Git paragona il
suo stato corrente con quello che è memorizzato nell'indice. Se le due
fonti di informazione corrispondono Git non ha bisogno di rileggere il
file.
Visto che l'accesso all'indice è considerabilmente più che leggere file,
se modificate solo qualche file, Git può aggiornare il suo stato quasi
immediatamente.
Prima abbiamo detto che l'indice si trova nell'area di staging. Com'è
possibile che un semplice file contenente dati su altri file si trova
nell'area di staging? Perché il comando 'add' aggiunge file nel database
di Git e aggiorna queste informazioni, mentre il comando 'commit' senza
opzioni crea un commit basato unicamente sull'indice e i file già
inclusi nel database.
=== Le origini di Git ===
Questo http://lkml.org/lkml/2005/4/6/121[messaggio della mailing list
del kernel di Linux] descrive la catena di eventi che hanno portato alla
creazione di Git. L'intera discussione è un affascinante sito
archeologico per gli storici di Git.
=== Il database di oggetti ===
Ognuna delle versioni dei vostri dati è conservata nel cosiddetto
'database di oggetti' che si trova nella sottocartella `.git/objects`;
il resto del contenuto di `.git/` rappresenta meno dati: l'indice, il
nome delle branch, le tags, le opzioni di configurazione, i logs, la
posizione attuale del commit HEAD, e così via. Il database di oggetti è
semplice ma elegante, e è la fonte della potenza di Git.
Ogni file in `.git/objects` è un 'oggetto'. Ci sono tre tipi di oggetti
che ci riguardano: oggetti 'blob', oggetti 'albero' (o `tree`) e gli
oggetti 'commit'.
=== Oggetti 'blob' ===
Prima di tutto un po' di magia. Scegliete un nome di file qualsiasi. In
una cartella vuota eseguite:
$ echo sweet > VOSTRO_FILE
$ git init
$ git add .
$ find .git/objects -type f
Vedrete +.git/objects/aa/823728ea7d592acc69b36875a482cdf3fd5c8d+.
Come posso saperlo senza sapere il nome del file? Perché il codice hash
SHA1 di:
"blob" SP "6" NUL "sweet" LF
è aa823728ea7d592acc69b36875a482cdf3fd5c8d, dove SP è uno spazio, NUL è
un carattere di zero byte e LF un passaggio a nuova linea. Potete
verificare tutto ciò digitando:
$ printf "blob 6\000sweet\n" | sha1sum
Git utilizza un sistema di classificazione per contenuti: i file non
sono archiviati secondo il loro nome, ma secondo il codice hash del loro
contenuto, in un file che chiamiamo un oggetto 'blob'. Possiamo vedere
il codice hash come identificativo unico del contenuto del file. Quindi,
in un certo senso, ci stiamo riferendo ai file rispetto al loro
contenuto. L'iniziale `blob 6` è semplicemente un'intestazione che
indica il tipo di oggetto e la sua lunghezza in bytes; serve a
semplificare la gestione interna.
Ecco come ho potuto predire il contenuto di .git. Il nome del file non
conta: solo il suo contenuto è usato per costruire l'oggetto blob.
Magari vi state chiedendo che cosa succede nel caso di file identici.
Provate ad aggiungere copie del vostro file, con qualsiasi nome. Il
contenuto di +.git/objects+ rimane lo stesso a prescindere del numero di
copie aggiunte. Git salva i dati solo una volta.
A proposito, i file in +.git/objects+ sono copressi con zlib e
conseguentemente non potete visualizzarne direttamente il contenuto.
Passatele attraverso il filtro http://www.zlib.net/zpipe.c[zpipe -d], o
eseguite:
$ git cat-file -p aa823728ea7d592acc69b36875a482cdf3fd5c8d
che visualizza appropriatamente l'oggetto scelto.
=== Oggetti 'tree' ===
Ma dove vanno a finire i nomi dei file? Devono essere salvati da qualche
parte. Git si occupa dei nomi dei file in fase di commit:
$ git commit # Scrivete un messaggio
$ find .git/objects -type f
Adesso dovreste avere tre oggetti. Ora non sono più in grado di predire
il nome dei due nuovi file, perché dipenderà in parte dal nome che avete
scelto. Procederemo assumendo che avete scelto ``rose''. Se questo non
fosse il caso potete sempre riscrivere la storia per far sembrare che lo
sia:
$ git filter-branch --tree-filter 'mv NOME_DEL_VOSTRO_FILE rose'
$ find .git/objects -type f
Adesso dovreste vedere il file
+.git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9+
perché questo è il codice hash SHA1 del contenuto seguente:
"tree" SP "32" NUL "100644 rose" NUL 0xaa823728ea7d592acc69b36875a482cdf3fd5c8d
Verificate che questo file contenga il contenuto precedente digitando:
$ echo 05b217bb859794d08bb9e4f7f04cbda4b207fbe9 | git cat-file --batch
È più facile verificare il codice hash con zpipe:
$ zpipe -d < .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9 | sha1sum
Verificare l'hash è più complicato con il comando cat-file perché il suo
output contiene elementi ulteriori oltre al file decompresso.
Questo file è un oggetto 'tree': una lista di elementi consistenti in un
tipo di file, un nome di file, e un hash. Nel nostro esempio il tipo di
file è 100644, che indica che `rose` è un file normale e il codice hash
e il codice hash è quello di un oggetto di tipo 'blob' che contiene il
contenuto di `rose`. Altri possibili tipi di file sono eseguibili, link
simbolici e cartelle. Nell'ultimo caso il codice hash si riferisce ad un
oggetto 'tree'.
Se avete eseguito filter-branch avrete dei vecchi oggetti di cui non
avete più bisogno. Anche se saranno cancellati automaticamente dopo il
periodo di ritenzione automatica, ora li cancelleremo per rendere il
nostro esempio più facile da seguire
$ rm -r .git/refs/original
$ git reflog expire --expire=now --all
$ git prune
Nel caso di un vero progetto dovreste tipicamente evitare comandi del
genere, visto che distruggono dei backup. Se volete un deposito più
ordinato, è normalmente consigliabile creare un nuovo clone. Fate
inoltre attenzione a manipolare direttamente il contenuto di +.git+: che
cosa succederebbe se un comando Git è in esecuzione allo stesso tempo, o
se se ci fosse un improvviso calo di corrente? In generale i refs
dovrebbero essere cancellati con *git update-ref -d*, anche se spesso
sembrerebbe sicuro cancella re +refs/original+ a mano.
=== Oggetti 'commit' ===
Abbiamo spiegato 2 dei 3 tipi di oggetto. Il terzo è l'oggetto
'commit'. Il suo contenuto dipende dal messaggio di commit, come anche
dalla data e l'ora in cui è stato creato. Perché far in maniera di
ottenere la stessa cosa dobbiamo fare qualche ritocco:
$ git commit --amend -m Shakespeare # Cambiamento del messaggio di commit
$ git filter-branch --env-filter 'export
GIT_AUTHOR_DATE="Fri 13 Feb 2009 15:31:30 -0800"
GIT_AUTHOR_NAME="Alice"
GIT_AUTHOR_EMAIL="alice@example.com"
GIT_COMMITTER_DATE="Fri, 13 Feb 2009 15:31:30 -0800"
GIT_COMMITTER_NAME="Bob"
GIT_COMMITTER_EMAIL="bob@example.com"' # Ritocco della data di creazione e degli autori
$ find .git/objects -type f
Dovreste ora vedere il file +.git/objects/49/993fe130c4b3bf24857a15d7969c396b7bc187+
che è il codice hash SHA1 del suo contenuto:
"commit 158" NUL
"tree 05b217bb859794d08bb9e4f7f04cbda4b207fbe9" LF
"author Alice <alice@example.com> 1234567890 -0800" LF
"committer Bob <bob@example.com> 1234567890 -0800" LF
LF
"Shakespeare" LF
Come prima potete utilizzare zpipe o cat-file per verificare voi stessi.
Questo è il primo commi, non ci sono quindi commit genitori. Ma i commit
seguenti conterranno sempre almeno una linea che identifica un commit
genitore.
=== Indistinguibile dalla magia ===
I segreti di Git sembrano troppo semplici. Sembra che basterebbe
mescolare assieme qualche script shell e aggiungere un pizzico di codice
C per preparare un sistema del genere in qualche ora: una combinazione
di operazioni di filesystem di base e hashing SHA1, guarnito con
lockfile e file di sincronizzazione per avere un po' di robustezza.
Infatti questaè un descrizione accurata le prime versioni di Git.
Malgrado ciò, a parte qualche astuta tecnica di compressione per
risparmiare spazio e di indicizzazione per risparmiare tempo, ora
sappiamo come Git cambia abilmente un sistema di file in un perfetto
database per il controllo di versione.
Ad esempio, se un file nel database degli oggetti è corrotto da un errore
sul disco i codici hash non corrisponderanno più e verremo informati del
problema. Calcolando il codice hash del codice hash di altri oggetti è
possibile garantire integrità a tutti i livelli. I commit sono atomici,
nel senso che un commit non può memorizzare modifiche parziali: possiamo
calcolare il codice hash di un commit e salvarlo in un database dopo
aver creato i relativi oggetti 'tree', 'blob' e 'commit'. Il database
degli oggetti è immune da interruzioni inaspettate dovute ad esempio a
cali di corrente.
Possiamo anche far fronte ai tentativi di attacco più maliziosi.
Supponiamo ad esempio che un avversario tenti di modificare di nascosto il
contenuto di un file in una vecchia versione di un progetto. Per rendere
il database degli oggetti coerente, il nostro avversario deve anche
modificare il codice hash dei corrispondenti oggetti blob, visto che ora
sarà una stringa di byte diversa. Questo significa che dovrà cambiare il
codice hash di tutti gli oggetti tree che fanno riferimento al file, e
di conseguenza cambiare l'hash di tutti gli oggetti commit in ognuno di
questi tree, oltre ai codici hash di tutti i discendenti di questi
commit. Questo implica che il codice hash dell'HEAD ufficiale differirà
da quello del deposito corrotto. Seguendo la traccia di codici hash
erronei possiamo localizzare con precisione il file corrotto, come anche
il primo commit ad averlo introdotto.
In conclusione, purché i 20 byte che rappresentano l'ultimo commit sono
al sicuro, è impossibile manomettere il deposito Git.
Che dire delle famose funzionalità di Git? Della creazione di branch?
Dei merge? Delle tag? Semplici dettagli. L'HEAD corrente è conservata
nel file +.git/HEAD+ che contiene un codice hash di un oggetto commit.
Il codice hash viene aggiornato durante un commit e l'esecuzione di
molti altri comandi. Le branch funzionano in maniera molto simile: sono
file in +.git/refs/heads+. La stessa cosa vale per le tag, salvate in
+.git/refs/tags+ ma sono aggiornate da un insieme diverso di comandi.
|