In git ogni file, ogni directory e ogni commit è un oggetto, e il suo nome è l’hash SHA-1 del proprio contenuto: lo stesso valore fa da identificatore e da somma di controllo. È una scelta di architettura, e cambia il modo in cui la storia di un progetto si archivia, si verifica e si scambia fra macchine.
Scrivo a poche settimane dal primo commit pubblico dello strumento (e83c516, 7 aprile 2005, messaggio “the information manager from hell”). git è ancora un insieme grezzo di comandi a basso livello, non un sistema di controllo di versione finito. Il suo modello di archiviazione merita comunque uno sguardo, perché è la parte destinata a restare ferma anche quando l’interfaccia cambierà.
Contesto
Per anni il kernel Linux si è coordinato con BitKeeper, un sistema di controllo di versione distribuito e proprietario, dato in uso gratuito alla comunità. Nell’aprile 2005 quella concessione è saltata: dal 1° luglio chi vuole BitKeeper deve pagarne la versione commerciale. Linus Torvalds, che del kernel cura l’integrazione, si è ritrovato senza lo strumento su cui poggiava il flusso di lavoro di centinaia di sviluppatori.
Le alternative libere già pronte — CVS, Subversion — sono centralizzate: un solo server tiene la storia completa, i client ne conservano una copia di lavoro. Torvalds ha provato anche monotone, un sistema distribuito che già nominava le revisioni con hash crittografici, ma per le dimensioni del kernel l’ha trovato troppo lento (decine di migliaia di file, una storia lunga anni). Da qui la scelta di scrivere da zero il proprio strumento, tarato sul caso d’uso del kernel prima ancora che sulla generalità.
Architettura
git non archivia le differenze fra versioni. Archivia snapshot. Lo store contiene quattro tipi di oggetto:
blob— il contenuto grezzo di un file, senza nome né metadati;tree— l’elenco delle voci di una directory, ciascuna con permessi, nome e l’hash delblobo deltreefiglio;commit— un puntatore a untree(lo stato completo dell’albero in quel momento), più autore, data, messaggio e il riferimento a uno o più commit genitori;tag— un riferimento con nome a un altro oggetto.
Ogni oggetto si comprime, gli si antepone un’intestazione con tipo e lunghezza, e del tutto si calcola lo SHA-1. I primi due caratteri esadecimali dell’hash danno il nome a una directory sotto .git/objects, gli altri trentotto al file. Per recuperare un oggetto serve quindi il suo hash: lo store è indirizzato per contenuto.
Da qui discendono due proprietà, conseguenze dirette del modello e non aggiunte successive.
La prima è la deduplicazione automatica. Due file con lo stesso contenuto, in due commit diversi, danno lo stesso hash, quindi lo stesso blob, archiviato una volta sola. Un commit che tocca tre file su diecimila crea tre blob nuovi, i tree lungo il percorso modificato e un commit; tutto il resto dell’albero resta condiviso per riferimento con il commit precedente.
La seconda è l’integrità verificabile. Il nome di un oggetto è la sua somma di controllo. L’hash di un commit racchiude quello del suo tree, che racchiude gli hash dei suoi blob e dei sotto-tree, che a loro volta dipendono dal contenuto. L’hash di un commit racchiude anche quelli dei genitori. La storia diventa così una catena di hash: il nome di un commit dipende, a cascata, da ogni byte di ogni file e di ogni commit che lo precede. È la struttura di un albero di Merkle.
Punto critico
Quella catena di hash trasforma la storia in un grafo aciclico orientato (DAG) di commit, ed è il DAG a tenere coerente il modello distribuito. Manca un’autorità centrale che distribuisca numeri di revisione progressivi: due sviluppatori che lavorano senza vedersi producono commit indipendenti, e quando li uniscono git trova l’antenato comune risalendo i puntatori ai genitori, poi fa un merge a tre vie. Un branch è un file di quaranta byte che contiene l’hash di un commit, non una copia dell’albero. Crearlo, spostarlo, cancellarlo costa quanto scrivere quel file.
Tutto questo regge però sulla resistenza alle collisioni di SHA-1. Se due contenuti diversi producessero lo stesso hash, git non saprebbe distinguerli: lo store terrebbe il primo e butterebbe il secondo credendolo identico, e l’integrità della catena cederebbe proprio dove si fonda. Nella pratica oggi la funzione si dà per solida, ma non senza riserve. A febbraio 2005 Xiaoyun Wang, Yiqun Lisa Yin e Hongbo Yu hanno descritto un attacco di collisione sull’intero SHA-1 con costo stimato sotto le 2⁶⁹ operazioni, contro le 2⁸⁰ di un attacco a forza bruta. È un risultato teorico, oggi fuori dalla portata pratica: nessuno ha ancora esibito una collisione reale. Ma dice che il margine di sicurezza della funzione è più stretto di quanto si pensava, e per un sistema che lega a quella funzione la propria nozione di identità degli oggetti è una dipendenza da sorvegliare.
Implicazioni
Con uno store indirizzato per contenuto il clone si definisce in una riga: copiare tutti gli oggetti raggiungibili da un riferimento. Ne risulta un repository completo, con tutta la storia, indipendente dall’origine. Fra “server” e “client” non c’è differenza di architettura: ci sono repository, e alcuni si scambiano oggetti con altri. Un commit fatto in locale è un commit pieno, immutabile dall’istante in cui esiste, che venga pubblicato o no.
Cambia così dove cade il costo del lavoro distribuito. Per verificare l’integrità non serve fidarsi del canale di trasporto né del repository di origine: basta ricalcolare gli hash per sapere se gli oggetti arrivati sono quelli attesi. Una volta scritta, la storia non si altera in silenzio, perché alterarla cambierebbe gli hash a valle.
Limiti
git oggi è scomodo. L’interfaccia è un insieme di comandi a basso livello — quella che si comincia già a chiamare plumbing — fatti per combinarsi in script, non per l’uso a mano. Intorno al nucleo stanno crescendo strati più comodi: Petr Baudis sta costruendo Cogito sopra questi comandi. Il modello di archiviazione descritto qui è solido; l’ergonomia che ci si costruirà sopra è ancora da scrivere, e cambierà più di una volta.
C’è poi un costo che il modello a snapshot porta con sé: ogni repository contiene tutta la storia. Per progetti con file binari grossi o storie molto lunghe, “ogni clone è completo” smette di essere solo un vantaggio. È il prezzo della verificabilità distribuita, e si paga per intero.
https://lkml.org/lkml/2005/4/6/121 https://www.kernel.org/pub/software/scm/git/docs/ https://www.bitkeeper.com/ https://venge.net/monotone/ https://www.iacr.org/archive/crypto2005/36210017/36210017.pdf https://www.noze.it/insights/git-open-source/
Immagine di copertina: Linus Torvalds, uomo con occhiali e camicia chiara, fotografato a una conferenza Linux nel 2003 — foto di Alex Dawson, CC BY-SA 2.0 — https://commons.wikimedia.org/wiki/File:Linus_Torvalds,_2002,_Australian_Linux_conference.jpg