Introduzione
Nella parte 3 di questa serie abbiamo fatto la conoscenza di alcune caratteristiche fondamentali di Git, tra cui le tre aree in cui i files vengono a trovarsi durante le operazioni che su di essi andiamo normalmente a effettuare.
In questa puntata andremo ancora più a fondo, analizzando nel dettaglio la struttura di un commit e degli elementi che lo compongono.
Configuriamo nome ed email
Prima di tornare a parlare di commit, è necessario fare una precisazione relativa a quanto scritto nel precedente articolo. Se i lettori hanno provato per la prima volta l’esecuzione di un commit all’interno di un repository Git, potrebbero essere incappati in un messaggio come questo:
[ 1 ] /c/temp/es01 (master) $ git commit -m "File 01" *** Please tell me who you are. Run git config --global user.email "you@example.com" git config --global user.name "Your Name" to set your account's default identity. Omit --global to set the identity only in this repository. fatal: unable to auto-detect email address (got 'nando@computer.(none)')
Git pretende che ogni commit sia “firmato” dalla persona che lo ha eseguito, e ne vuole conoscere anche un recapito email. Questa imposizione è stata voluta sin dall’inizio per far sì che, per ogni modifica apportata a un progetto, se ne potesse rintracciare l’autore, da contattare in caso di necessità.
Firmare il commit
Se non diciamo a Git chi siamo, esso cerca di desumerlo dalle configurazioni del sistema; tenta quindi di andare a leggere le informazioni dell’utente e la relativa email dal sistema operativo; nel caso non riuscisse a “capirlo”, presenta il messaggio che vediamo sopra, pretendendo che si proceda a opportuna configurazione di queste informazioni prima di continuare con il commit.
Senza lasciarci distrarre troppo, per il momento procediamo come suggerito; nelle prossime puntate approfondiremo meglio il sistema di configurazione di Git.
Procediamo quindi digitando questi due comandi:
[ 1 ] /c/temp/es01 (master) $ git config --global user.email "jesus_was_rasta@yahoo.it"
[ 2 ] /c/temp/es01 (master) $ git config --global user.name "Ferdinando Santacroce"
Git non darà alcun messaggio di risposta ma semplicemente andrà a settare nella sua configurazione globale queste informazioni: d’ora in poi, in ogni repository nel quale andrete a eseguire dei commit, questi saranno “intestati” a vostro nome.
E ora torniamo all’argomento principale di questa puntata.
I commit
Nelle scorse puntate abbiamo realizzato un semplice commit, senza però indugiare troppo sulla sua struttura. Ora invece è giunto il momento di osservare più da vicino questi “mattoncini” che danno forma al nostro repository.
Se avete ancora sotto mano il repository creato alla scorsa occasione bene, altrimenti vi rimando al precedente articolo per le istruzioni sulla sua creazione ex-novo.
Una volta dentro alla cartella del repository, proviamo a digitare git log, un nuovo comando che andiamo a conoscere:
[ 1 ] /c/temp/es01 (master) $ git log commit 4004a1197c9078a8a39e92734e939631926360fc Author: Ferdinando Santacroce <jesus_was_rasta@yahoo.it> Date: Sat Feb 4 09:48:21 2017 +0100 First commit, file01
Il comando git log consente di vedere la history del nostro repository; digitato senza alcuna altra opzione, restituisce la lista dei commit effettuati, in ordine cronologico inverso. Quello che vediamo, infatti, è il primo ed unico commit che abbiamo effettuato nella scorsa puntata, contenente un file di testo.
Una nota sulla configurazione del prompt
Piccola parentesi: come avrete notato, il prompt della console attualmente in uso è leggermente diverso da quello standard; ho eliminato informazioni che ritengo poco utili, ossia la parte che riporta utente@computer — so già chi sono… — e l’ambiente di utilizzo (MINGW64); al loro posto ho aggiunto un numerino tra parentesi quadre, che si incrementa ad ogni comando: questo mi permetterà di descrivere con maggiore facilità i passaggi che compiremo durante i nostri esperimenti.
Se volete personalizzare anche voi la vostra shell MinTTY [1], vi rimando a un mio articolo [2] che ne parla diffusamente.
L’hash
Passiamo ora ad analizzare ora un po’ le informazioni che ci vengono fornite. La prima riga riporta lo SHA-1 [3] del commit, una sequenza alfanumerica di ben 40 caratteri. Questo “codice”, o hash come si è soliti chiamarlo, identifica univocamente il commit all’interno del repository, ed è grazie ad esso che d’ora in poi si potrà far riferimento nel compiere determinate azioni che fra poco andremo a testare.
Giusto per fare una prova, facciamo un secondo commit sul nostro repository e poi andiamo a rieseguire git log, per verificare quanto detto in precedenza; creiamo quindi un file02.txt e modifichiamolo a piacere:
[ 2 ] /c/temp/es01 (master) $ vim file02.txt
Subito dopo, procediamo all’aggiunta del file alla staging area, effettuando poi il suo successivo commit:
[ 3 ] /c/temp/es01 (master) $ git add file02.txt [ 4 ] /c/temp/es01 (master) $ git commit -m "Add a 2nd file" [master 0763bf3] Add a 2nd file 1 file changed, 1 insertion(+) create mode 100644 file02.txt
Ora riproviamo a digitare il comando git log:
[ 5 ] /c/temp/es01 (master) $ git log 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
Adesso compaiono due commit, con due hash distinti, ordinati in ordine cronologico inverso.
Ora proviamo a usare git log chiedendo di mostrarci solo un particolare commit, il primo, indicando i primi caratteri dell’hash:
[ 6 ] /c/temp/es01 (master) $ git log 4004a11 commit 4004a1197c9078a8a39e92734e939631926360fc Author: Ferdinando Santacroce <jesus_was_rasta@yahoo.it> Date: Sat Feb 4 09:48:21 2017 +0100 First commit, file01
Con questo piccolo esperimento abbiamo imparato due cose nuove: che git log può essere chiamato specificando l’hash del commit che si vuole vedere, ma soprattutto che non è necessario scrivere sempre tutto l’hash, ma basta indicarne i primi caratteri. Il numero minimo di caratteri che è possibile digitare è in genere 4, ed è regolato da una particolare voce di configurazione, la core.abbrev [4]; come potrete immaginare però, con l’andare del tempo è possibile che 4 caratteri non siano più sufficienti a discriminare un determinato commit: il tutto dipende da quanto grosso è il repository in cui ci stiamo muovendo: più è grosso, e più elevato è il rischio di “collisioni” fra i primi caratteri di un hash.
Ulteriori considerazioni sullo hash
Ad ogni modo, si tenga presente questa considerazione: Il kernel Linux è ad oggi uno dei progetti più grossi gestiti tramite Git, con i suoi oltre seicentomila commit. Eppure, in quel repository, sono comunque sufficienti solo 12 caratteri per identificare univocamente un commit.
Il default di Git prevede l’utilizzo dei primi 7 caratteri: se tornate indietro e ci fate caso, quando eseguiamo un commit, Git stesso riporta fra parentesi quadre i primi 7 caratteri del commit creato. Comunque non preoccupatevi: se durante l’esecuzione di qualche comando impartito Git non riesce a identificare univocamente un commit con il set di caratteri che gli avete fornito, non mancherà di avvisarvi.
Se invece siete curiosi di sapere con esattezza qual è il numero minimo di caratteri necessario per identificare un commit — anzi un qualsiasi oggetto Git, come vedremo più avanti — potete provare questo comando:
[ 7 ] /c/temp/es01 (master) $ git rev-parse --short=3 4004a1197c9078a8a39e92734e939631926360fc 4004
Il comando git rev-parse con l’opzione –short=<numero caratteri> è in grado di dirci, rispetto al numero minimo di caratteri desiderato, qual è la sequenza minima di caratteri da indicare per essere certi allo stato attuale del repository di identificare univocamente il commit in oggetto. In questo caso abbiamo provato a vedere se 3 caratteri sarebbero bastati, ma di tutto punto Git ci ha comunicato che la sequenza minima da indicare è formata dai primi 4 caratteri, 4004.
Ho divagato e introdotto questo comando per soddisfare una possibile curiosità del lettore; e a questo punto, ne approfitto per introdurre un’altra peculiarità di Git, ovvero la suddivisione fra comandi porcelain e comandi plumbing.
Comandi porcelain e plumbing
Git, come sappiamo, prevede una miriade di comandi, alcuni dei quali non sono praticamente mai usati dall’utente medio, come da esempio il succitato git rev-parse. Questi comandi prendono il nome di plumbing commands, mentre quelli che abbiamo già imparato a conoscere, tipo git add, git commit e via discorrendo sono annoverati fra i cosiddetti porcelain commands.
La metafora ha origine direttamente dalla fervida immaginazione di Linus Torvalds, papà di Git, e ha a che fare con gli idraulici. Essi, com’è noto, si occupano anche della manutenzione dei servizi igienici; prendiamo a riferimento “il trono”, la tazza dove espletiamo i nostri bisogni. Questo è formato da un manufatto in porcellana, in grado di consentirci una seduta confortevole, e da una serie di tubi e condutture (in inglese, plumbing) che permettono invece lo scarico di quanto prodotto fin giù nella rete fognaria.
Linus si è servito di questa aulica metafora per suddividere i comandi di Git in due famiglie, quelli di più alto livello, confortevoli a un utilizzatore interessato alle operazioni più comuni (porcelain), e quelli utilizzabili a discrezione dagli utenti più esperti per compiere operazioni di più basso livello (plumbing).
Possiamo dunque considerare i comandi porcelain come comandi “di interfaccia” verso l’utente, mentre quelli plumbing lavorano “a basso livello”. Questo significa anche che i comandi porcelain rimangono più “stabili” nel tempo — modalità d’uso e opzioni variano con più cautela ed in tempi dilatati —in quanto usati direttamente ma implementati anche in numerosi tool grafici, editor e così via; quelli plumbing, invece, di uso meno comune, evolvono con meno restrizioni.
Non esiste una suddivisione precisa fra queste due categorie di comandi, in quanto il confine è spesso piuttosto labile; ci serviremo comunque ancora di essi, per poter meglio osservare e capire il funzionamento interno di Git.
Ritorniamo ora direttamente sul tema della puntata, e riprendiamo l’analisi dei commit.
Anatomia di un commit
Riportiamo qui per comodità l’output del comando git log digitato all’inizio:
[ 1 ] /c/temp/es01 (master) $ git log commit 4004a1197c9078a8a39e92734e939631926360fc Author: Ferdinando Santacroce <jesus_was_rasta@yahoo.it> Date: Sat Feb 4 09:48:21 2017 +0100 First commit, file01
Oltre al già citato hash, osserviamo le altre informazioni presenti. Troviamo innanzitutto le informazioni riguardanti l’autore del commit, e successivamente la data e l’ora in cui esso risulta essere stato effettuato; per ultimo invece viene riportato il messaggio di commit, così come l’abbiamo inserito.
Le informazioni sull’autore servono per rendere rintracciabile la persona che ha eseguito le modifiche all’interno del commit; tenuto conto che Git e i sistemi di versionamento nascono anche per permettere alle persone di collaborare, la presenza di un riferimento esplicito non è certo cosa sgradita, anzi.
Ma come viene salvato un commit? E soprattutto, dove? È giunto il momento di “aprire il cofano” e mettere il naso dentro al “motore” di Git.
La .git folder
Quando creiamo un nuovo repository Git con il comando git init, avviene praticamente la creazione di una dotfolder di nome .git all’interno della cartella nella quale digitiamo il comando. Proviamo a vedere cosa c’è dentro: digitiamo il comando ll (alias del comando ls -l) nella nostra shell:
[ 2 ] /c/temp/es01 (master) $ ll .git/ total 21 drwxr-xr-x 1 san 1049089 0 Feb 4 10:03 ./ drwxr-xr-x 1 san 1049089 0 Feb 4 10:03 ../ -rw-r--r-- 1 san 1049089 15 Feb 4 10:03 COMMIT_EDITMSG -rw-r--r-- 1 san 1049089 201 Feb 4 09:48 config -rw-r--r-- 1 san 1049089 73 Feb 4 09:44 description -rw-r--r-- 1 san 1049089 23 Feb 4 09:44 HEAD drwxr-xr-x 1 san 1049089 0 Feb 4 09:44 hooks/ -rw-r--r-- 1 san 1049089 225 Feb 4 10:03 index drwxr-xr-x 1 san 1049089 0 Feb 4 09:44 info/ drwxr-xr-x 1 san 1049089 0 Feb 4 09:48 logs/ drwxr-xr-x 1 san 1049089 0 Feb 4 10:03 objects/ #cartella objects drwxr-xr-x 1 san 1049089 0 Feb 4 09:44 refs/
Concentriamoci per un attimo sulla cartella objects, e ignoriamo il resto; entriamo a vedere cosa contiene:
[ 3 ] /c/temp/es01 (master) $ ll .git/objects/ total 8 drwxr-xr-x 1 san 1049089 0 Feb 4 10:03 ./ drwxr-xr-x 1 san 1049089 0 Feb 4 10:03 ../ drwxr-xr-x 1 san 1049089 0 Feb 4 10:03 07/ drwxr-xr-x 1 san 1049089 0 Feb 4 10:03 30/ drwxr-xr-x 1 san 1049089 0 Feb 4 09:48 40/ #cartella 40 drwxr-xr-x 1 san 1049089 0 Feb 4 09:48 4b/ drwxr-xr-x 1 san 1049089 0 Feb 4 09:45 a9/ drwxr-xr-x 1 san 1049089 0 Feb 4 09:48 c2/ drwxr-xr-x 1 san 1049089 0 Feb 4 09:45 f5/ drwxr-xr-x 1 san 1049089 0 Feb 4 09:44 info/ drwxr-xr-x 1 san 1049089 0 Feb 4 09:44 pack/
Noterete che è presente una cartella chiamata “40”; vediamo dentro cosa c’è:
[ 4 ] /c/temp/es01 (master) $ ll .git/objects/40 total 5 drwxr-xr-x 1 san 1049089 0 Feb 4 09:48 ./ drwxr-xr-x 1 san 1049089 0 Feb 4 10:03 ../ -r--r--r-- 1 san 1049089 145 Feb 4 09:48 04a1197c9078a8a39e92734e939631926360fc
Mmm… C’è un file senza estensione che si chiama
04a1197c9078a8a39e92734e939631926360fc
Se però faccio:
40 + 04a1197c9078a8a39e92734e939631926360fc
ottengo
4004a1197c9078a8a39e92734e939631926360fc
ossia l’hash del primo commit che abbiamo effettuato!
Semplici cartelle organizzate
Sospetto abbiate già capito: Git salva i commit come file in semplici cartelle, organizzando queste ultime in una struttura di sotto-cartelle per renderne più rapido l’accesso dal file system.
Se proviamo ad aprire quel file con un editor, il risultato sarà un fallimento: vedremo solo una sfilza di caratteri inintelligibili. C’è però un comando plumbing che ci può dare una mano, git cat-file; proviamo a digitarlo seguito dai primi 7 caratteri dell’hash del commit, utilizzando l’opzione -p come indicato qui sotto:
[ 7 ] /c/temp/es01 (master) $ git cat-file -p 4004a11 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
Questo comando consente di andare a sbirciare dentro gli objects di Git — di cui parleremo diffusamente nella prossima puntata — e i commit rientrano a far parte di questi objects. Con l’opzione -p abbiamo chiesto di mostrare in una forma più facile da leggere quale fosse il contenuto il del suddetto object, e Git di tutto punto ci ha mostrato quanto vedete sopra.
Il contenuto del commit
È facilmente comprensibile la riga che indica l’author, che infatti riporta le informazioni indicate e già discusse in precedenza; il committer, che in questo caso coincide con l’author, rappresenta invece il soggetto che ha eseguito il commit. Ora vi starete chiedendo: ma perché due nomi diversi? Non sono sempre la stessa persona? In effetti il 99% delle volte lo sono, ma è possibile che in alcuni casi in fase di commit si voglia indicare un author diverso per qualche motivo, per esempio se stiamo per committare le modifiche passateci “offline” da qualcun altro, utilizzando l’apposita opzione –author=<author>. In casi come questi, il commit avrebbe un autore diverso dal committer, che invece non è possibile alterare.
In ultimo, troviamo una riga che dice:
tree c24e325c8a82a3750bc7853552a08735ae5d494c
Cosa vuol dire? Il tree è un altro degli objects gestiti da Git, e rappresenta un contenitore di file: immaginiamolo come se fosse una cartella.
Il contenuto del tree
Utilizzando la stessa procedura di prima, possiamo verificare che nella cartella /objects di Git esiste una sottocartella denominata c2, e al suo interno contiene un file
4e325c8a82a3750bc7853552a08735ae5d494c
Proviamo a vedere cosa contiene questo ulteriore object di Git usando sempre il comando git cat-file -p:
[ 8 ] /c/temp/es01 (master) $ git cat-file -p c24e325 100644 blob 4bc92d99949138eb83452d857e5c504471e30805 file01.txt
Questo tree, che abbiamo detto essere un qualcosa che Git usa per identificare una cartella, contiene a suo volta un ulteriore object, denominato blob: a destra è indicato però “file01.txt”, il che ci fa intuire che i blob per Git rappresentano i file. Come prima, possiamo verificare che questo blob sia salvato in una sottocartella chiamata “4b”, e possiamo di nuovo andare a vederne il contenuto:
[ 9 ] /c/temp/es01 (master) $ git cat-file -p 4bc92d9 File 01
Quel che appare sotto, “File 01”, è esattamente il testo contenuto del mio file .txt: con poca fantasia, quando l’ho creato ho scritto solo queste due parole al suo interno.
Per conferma, possiamo usare il comando cat, che in sistemi Unix consente di vedere il contenuto di un file:
[ 10 ] /c/temp/es01 (master) $ cat file01.txt File 01
Come vedete, il risultato è lo stesso.
Conclusioni
Abbiamo ancora tanto da dire per concludere questo discorso, ma lo spazio a nostra disposizione termina qui.
Nella prossima puntata vedremo in dettaglio quanto anticipato in questa ultima parte dell’articolo, ossia il sistema con cui Git organizza e salva le informazioni al suo interno (objects store). Una volta capito il modo in cui Git conserva i nostri dati, i comandi che andremo a utilizzare risulteranno un po’ meno astrusi di quanto non lo sarebbero altrimenti.