Repository e alberi
Nei precedenti articoli abbiamo visto che un repository Git può essere immaginato come un albero il quale, partendo da una radice comune (il root-commit) si sviluppa verso l’alto attraverso uno o più rami. Questi rami, o branch come si è soliti chiamarli nei sistemi di versionamento, sono in genere contraddistinti da un nome. In questo Git non fa eccezione; se ricordate, infatti, gli esperimenti condotti fino ad ora ci hanno portato a eseguire commit sul branch master del nostro repository di test. “master” è per l’appunto il nome del branch di default di un repository Git, un po’ come “trunk” lo è per Subversion.
Le analogie con Subversion però terminano qui: vedremo ora come Git gestisce i branch, e per gli utilizzatori di Subversion sarà una vera sorpresa…
Questione di etichetta
In Git un branch non è altro che una label, un’etichetta mobile posta su di un commit; sarebbe difficile altrimenti muoversi all’interno di un repository, quando i commit non sono contraddistinti da un semplice numero progressivo come le revisioni di Subversion, bensì da un interminabile hashcode di 40 caratteri…
Proviamo a creare di nuovo un repository Git vuoto, e vediamo come si comporta il branch master man mano che andiamo a eseguire dei commit:
[1] /home $ mkdir es03 [2] /home $ cd es03 [3] /home/es03 $ git init Initialized empty Git repository in C:/Users/home/es03/.git/ [4] /home/es03 (master) $ echo "penna" >> cancelleria.txt [5] /home/es03 (master) $ git add cancelleria.txt [6] /home/es03 (master) $ git commit -m "Aggiunge una penna alla lista della cancelleria" [master (root-commit) 6cf12f4] Aggiunge una penna alla lista della cancelleria 1 file changed, 1 insertion(+) create mode 100644 cancelleria.txt
Ok, non dovrebbero servire spiegazioni: abbiamo creato una cartella e inizializzato un nuovo repository; abbiamo creato il file cancelleria.txt, contenente una penna, e abbiamo eseguito il primo commit.
Aggiungere nuovi commit al master branch
A questo punto sarebbe bello poter interagire per vedere insieme in “real-time” cosa succede al master branch quando si aggiunge un nuovo commit, ma purtroppo non siamo su YouTube… Cercherò quindi di raccontarvelo, chiedendo a voi lettori uno piccolo sforzo in più per cogliere quanto avviene.
Iniziamo verificando lo stato attuale del repository; lo facciamo usando il comando git log, aggiungendoci però qualche opzione:
[7] /home/es03 (master) $ git log --decorate --oneline * 6cf12f4 (HEAD -> master) Aggiunge una penna alla lista della cancelleria
Vediamo nel dettaglio cosa fanno le opzioni usate.
- –graph: in questo caso non fa altro che aggiungere un asterisco a sinistra, prima dell’hash del commit; quando avremo più branch, questa opzione disegnerà per noi i rami del repository dandocene una semplice ma efficace rappresentazione grafica.
- –decorate: questa opzione “decora” il messaggio di log, aggiungendo piccoli artefatti grafici che ci aiutano di nuovo a capire meglio la situazione che abbiamo davanti; in questo caso disegna la freccia -> tra le parole “HEAD” e “master”.
- –oneline: questa è facile… Riporta ogni commit utilizzando una sola riga, abbreviando ove necessario.
Bene, eseguiamo ora un secondo commit:
[8] /home/es03 (master) $ echo "matita" >> cancelleria.txt [9] /home/es03 (master) $ git commit -am "Aggiunge una matita" [master 87cacc7] Aggiunge una matita 1 file changed, 1 insertion(+)
Avete notato? Dopo aver aggiunto una matita alla nostra lista, ho eseguito un commit senza prima fare git add; il “trucco” sta nell’opzione -a (–add) aggiunta al comando di commit, che significa “aggiungi a questo commit tutti i file modificati che avevo già ho committato in precedenza”. Nel nostro caso quindi questa opzione ci ha evitato di fare un git add cancelleria.txt.
Questo insieme di “file già aggiunti in precedenza al repository” prendono il nome di file tracked.
OK, a questo punto abbiamo due commit; rieseguiamo il comando di log visto in precedenza, e vediamo cosa dice:
[10] /home/es03 (master) $ git log --graph --decorate --oneline * 87cacc7 (HEAD -> master) Aggiunge una matita * 6cf12f4 Aggiunge una penna alla lista della cancelleria
Interessante! Sia “HEAD” che “master” ora si sono spostati sul secondo commit; cosa vuol dire?
I branch sono semplici etichette
Abbiamo visto nelle scorse puntate come i commit siano legati uno all’altro da un rapporto “di parentela”: ogni commit contiene al suo interno un riferimento al commit precedente; questo significa che per “navigare” all’interno di un repository non posso partire per esempio dal primo commit e cercare di andare al successivo, perché un commit non ha un riferimento a chi viene dopo, ma a chi viene prima. Rimanendo alla nostra metafora “arborea”, questo significa che il nostro albero è navigabile solo a partire dalle “foglie”, ossia dagli estremi più “in alto” e poi giù fino ad arrivare al root-commit.
I branch non sono altro che delle etichette che stanno “sulle foglie dei rami”; preso un branch, questo sarà composto da una serie di commit che si susseguono; l’ultimo commit, la nostra foglia, deve essere sempre identificato da una etichetta affinché sia raggiungibile navigando all’interno di un repository. In caso contrario dovremmo ricordare per ogni ramo del nostro repository l’hash code dell’ultimo commit ivi effettuato, e vi lascio immaginare quanto facile possa essere…
Come funzionano i riferimenti
Quindi, ogni volta che eseguiremo un commit in un branch, la “reference” che identifica quel branch si muoverà di conseguenza al fine di rimanere sempre associata all’ultimo commit eseguito su quel branch.
Ma come farà Git a gestire questa funzionalità? Andiamo di nuovo a ficcare il naso nella “.git folder”:
[11] /home/es03 (master) $ ll .git/ total 17 drwxr-xr-x 1 san 1049089 0 Jun 28 21:32 ./ drwxr-xr-x 1 san 1049089 0 Jun 28 21:12 ../ -rw-r--r-- 1 san 1049089 20 Jun 28 21:32 COMMIT_EDITMSG -rw-r--r-- 1 san 1049089 208 Jun 28 21:13 config -rw-r--r-- 1 san 1049089 73 Jun 28 21:11 description -rw-r--r-- 1 san 1049089 23 Jun 28 21:11 HEAD drwxr-xr-x 1 san 1049089 0 Jun 28 21:11 hooks/ -rw-r--r-- 1 san 1049089 145 Jun 28 21:32 index drwxr-xr-x 1 san 1049089 0 Jun 28 21:11 info/ drwxr-xr-x 1 san 1049089 0 Jun 28 21:14 logs/ drwxr-xr-x 1 san 1049089 0 Jun 28 21:32 objects/ drwxr-xr-x 1 san 1049089 0 Jun 28 21:11 refs/ #diamo un’occhiata qui… [12] /home/es03 (master) $ ll .git/refs total 4 drwxr-xr-x 1 san 1049089 0 Jun 28 21:11 ./ drwxr-xr-x 1 san 1049089 0 Jun 28 21:32 ../ drwxr-xr-x 1 san 1049089 0 Jun 28 21:32 heads/ #qui è dove Git tiene i branch drwxr-xr-x 1 san 1049089 0 Jun 28 21:11 tags/ [13] /home/es03 (master) $ ll .git/refs/heads/ total 1 drwxr-xr-x 1 san 1049089 0 Jun 28 21:32 ./ drwxr-xr-x 1 san 1049089 0 Jun 28 21:11 ../ -rw-r--r-- 1 san 1049089 41 Jun 28 21:32 master #...infatti c’è un file “master” [14] /home/es03 (master) $ cat .git/refs/heads/master 87cacc7d1aafa56815baa14a3ca23afd339e60c7 #ma questo è l’hash del commit! [15] /home/es03 (master) $ git log --graph --decorate --oneline * 87cacc7 (HEAD -> master) Aggiunge una matita
Come si poteva immaginare, Git gestisce tutto questo articolato sistema di riferimenti… con un banale file di testo che contiene l’hash dell’ultimo commit fatto sul branch!
Oramai è passato un po’ di tempo dalla prima volta, ma continuo in ogni caso a stupirmi per quanto essenziale ed efficace sia la struttura interna di Git.
Creare un nuovo branch
Ora che ci siamo scaldati, inizia il divertimento; proviamo a vedere cosa succede quando si chiede a Git di creare un nuovo branch. Siccome nel nostro ufficio non ci facciamo mancare nulla, oltre alla cancelleria facciamo una lista per le bibite, in un branch separato di nome “bevande”:
[16] /home/es03 (master) $ git branch bevande #creo il branch [17] /home/es03 (master) $ git checkout bevande #mi sposto sul branch appena creato Switched to branch 'bevande' [18] /home/es03 (bevande) $ echo "acqua" >> bibite.txt [19] /home/es03 (bevande) $ git add bibite.txt [20] /home/es03 (bevande) $ git commit -m "Aggiunge l'acqua alle bibite" #committo un nuovo file [bevande 4072e76] Aggiunge l'acqua alle bibite 1 file changed, 1 insertion(+) create mode 100644 bibite.txt [21] /home/es03 (bevande) $ git log --graph --decorate --oneline * 4072e76 (HEAD -> bevande) Aggiunge l'acqua alle bibite * 87cacc7 (master) Aggiunge una matita * 6cf12f4 Aggiunge una penna alla lista della cancelleria
Ora le cose cominciano a farsi interessanti; in questo passaggio abbiamo introdotto due nuovi comandi:
- git branch <nome del branch>: con il comando branch, seguito da una parola, chiediamo a Git di creare un nuovo branch che abbia quel nome; a dire il vero ci sono alcune regole da rispettare e cose da sapere sul possibile nome di un branch, ma per ora accontentiamoci di quanto imparato, che è sufficiente per poter lavorare.
- git checkout <nome del branch>: con il comando checkout, seguito anche qui dal nome di un branch esistente, chiediamo a Git di spostarci sul branch appena creato.
Ulteriori informazioni
Una volta spostatici sul nuovo branch, abbiamo eseguito un nuovo commit, creando il nuovo file bibite.txt. Quel che vediamo nel log successivo ci porta all’evidenza alcuni fatti che dobbiamo assolutamente approfondire; riporto qui il precedente comando di log in modo da poterlo commentare nel dettaglio:
[21] /home/es03 (bevande) $ git log --graph --decorate --oneline * 4072e76 (HEAD -> bevande) Aggiunge l'acqua alle bibite * 87cacc7 (master) Aggiunge una matita * 6cf12f4 Aggiunge una penna alla lista della cancelleria
Come si può notare, a fronte del nuovo commit effettuato sul branch bevande, nel log vediamo una nuova riga (hash 4072e76); vicino al commit si sono spostati anche la scritta bevande e HEAD: ora ci è chiaro quindi che quella scritta che compare a destra del commit rappresenta il nome del branch su cui quel commit è presente e ne rappresenta la foglia, ossia l’ultimo commit effettuato.
L’etichetta master è rimasta invece sul commit dov’era prima: abbiamo fatto il commit su un branch diverso, e abbiamo quindi “abbandonato” master per un attimo; pertanto questo branch rimane fermo lì dove l’abbiamo lasciato.
La cosa curiosa però è questa scritta HEAD che invece ci segue in ogni nostro movimento, rimanendo sempre legata all’ultimo commit eseguito: ma cosa significa?
HEAD, ossia “voi siete qui”
Oramai penso che si sia capito; HEAD è una particolare “etichetta” che Git ci mette a disposizione per capire in ogni momento in che punto siamo all’interno del nostro repository.
HEAD generalmente punta al branch in cui siamo posizionati attualmente; è per questo infatti che nel precedente comando di log vediamo una freccia che va da HEAD al branch, per esempio HEAD -> bevande; quella freccia è il modo che Git utilizza per dirci “Ehi, sappi che HEAD punta al branch bevande; i commit che eseguirai andranno quindi ad aggiungersi a quel branch”.
Anche in questo caso Git utilizza un semplice file di testo per memorizzare questa informazione; se tornate a sbirciare nella .git folder noterete infatti la presenza di un file chiamato appunto HEAD:
[22] /home/es03 (bevande) $ cat .git/HEAD ref: refs/heads/bevande
Al suo interno, la riga ref: refs/heads/bevande ci fa capire che HEAD punta al branch bevande; una piccola differenza con i file dei branch quindi, che come abbiamo visto prima invece contengono l’hash del commit al quale puntano.
Una verifica
Facciamo ora una breve prova per fissare i concetti appena visti; proviamo a tornare sul branch master ed eseguiamo un ulteriore commit per vedere cosa succede:
[23] /home/es03 (bevande) $ git checkout master Switched to branch 'master' [24] /home/es03 (master) $ echo "Crackers" >> snack.txt [25] /home/es03 (master) $ git add snack.txt [26] /home/es03 (master) $ git log --graph --decorate --oneline * 87cacc7 (HEAD -> master) Aggiunge una matita * 6cf12f4 Aggiunge una penna alla lista della cancelleria
OK, fin qui tutto chiaro: siamo tornati su master usando il comando checkout, e possiamo osservare come la reference HEAD ci abbia “seguito”, puntando ora di fatto al branch master.
Proviamo ora a eseguire un commit:
[27] /home/es03 (master) $ echo "Crackers" >> snack.txt [28] /home/es03 (master) $ git add snack.txt [29] /home/es03 (master) $ git commit -m "Aggiunge lista snack a disposizione per l'ufficio" [master e29f5c6] Aggiunge lista snack a disposizione per l'ufficio 1 file changed, 1 insertion(+) create mode 100644 snack.txt
Abbiamo creato un nuovo file, snack.txt, e l’abbiamo aggiunto al branch master del nostro repository con il solito comando di commit; vediamo cosa dice il log, ma questa volta aggiungiamo un’ulteriore opzione, –all, che ci consente di vedere tutta la struttura del repository e non solo quella del branch corrente:
[30] /home/es03 (master) $ git log –graph --decorate --oneline --all * e29f5c6 (HEAD -> master) Aggiunge lista snack a disposizione per l'ufficio | * 4072e76 (bevande) Aggiunge l'acqua alle bibite |/ * 87cacc7 Aggiunge una matita * 6cf12f4 Aggiunge una penna alla lista della cancelleria
A-ha! Ora si vede bene cosa sta succedendo: i branch master e bevande stanno prendendo strade diverse; su bevande c’è un commit (il 4072e76) che non c’è su master, e su master c’è un commit (il e29f5c6) che non è presente su bevande.
Abbiamo quindi ora due branch distinti, con storie diverse, e sappiamo come spostarci da un branch all’altro usando il comando checkout.
Un ultimo dettaglio
Torniamo sul branch bevande e vediamo ancora una cosa prima di concludere:
[31] /home/es03 (master) $ git checkout bevande Switched to branch 'bevande' [32] /home/es03 (bevande) $ ll total 10 drwxr-xr-x 1 san 1049089 0 Jun 29 06:44 ./ drwxr-xr-x 1 san 1049089 0 Jun 28 21:11 ../ drwxr-xr-x 1 san 1049089 0 Jun 29 06:44 .git/ -rw-r--r-- 1 san 1049089 7 Jun 29 06:44 bibite.txt -rw-r--r-- 1 san 1049089 13 Jun 28 21:32 cancelleria.txt
Se torniamo sul branch bevande e andiamo a vedere quali file ci sono nella nostra directory ci accorgiamo che il file snack.txt committato sul branch master scompare, e rimangono solo i file cancelleria.txt e bibite.txt committati su master; se ritorniamo su master, il file snack.txt ricompare magicamente, mentre scompare il file bibite.txt presente solo sul branch bevande:
[33] /home/es03 (bevande) $ git checkout - Switched to branch 'master' [34] /home/es03 (master) $ ll total 10 drwxr-xr-x 1 san 1049089 0 Jun 29 06:46 ./ drwxr-xr-x 1 san 1049089 0 Jun 28 21:11 ../ drwxr-xr-x 1 san 1049089 0 Jun 29 06:46 .git/ -rw-r--r-- 1 san 1049089 13 Jun 28 21:32 cancelleria.txt -rw-r--r-- 1 san 1049089 10 Jun 29 06:46 snack.txt
Come vedete, il file bibite.txt presente sul branch bevande non c’è più ora che siamo su master. Avrete notato il comando git checkout –; come in diversi altri comandi sui sistemi Unix-like, p.e. cd, anche nel comando git checkout può essere usato il trattino per dire “torna dov’ero prima”, un trucchetto molto comodo per risparmiare tempo quando si passa spesso da un branch a un altro.
Un’ultima cosa va evidenziata; al variare della nostra posizione da un branch all’altro, il contenuto della working copy di Git, ossia la nostra cartella su disco, varia: il comando di checkout infatti non fa altro che andare a ripescare tutti i file presenti sul branch su cui ci stiamo spostando, togliere di mezzo quelli che non ci sono, e cambiare il puntamento di HEAD dal branch precedente a quello attuale.
Conclusioni
Avremo bisogno di almeno un’altra puntata per approfondire questo discorso; muoversi tra i branch di un repository non è difficile — lo avete visto — ma in Git si possono compiere operazioni ben più sofisticate una volta imparato questo modello organizzativo.
Il nostro obiettivo sarà proprio quello, al fine di acquisire la padronanza necessaria per poter poi creare branch a partire da un qualsiasi commit, per riportare un commit da un branch all’altro, di unire branch e perfino di riscrivere la storia del nostro repository, andando a unire, eliminare o modificare i commit esistenti.