Articolo originale: How not to be afraid of GIT anymore

Comprendere i meccanismi per eliminare l'incertezza

xcdstrip-1
Ti sei mai trovato in questa situazione? Web comic da XKCD

Cos'è Git?

“È un sistema di controllo di versione.”

Perché mi serve?

“Per controllare le versioni, sciocchino.”

Va bene, va bene, non sono di grande aiuto, non ancora. Ecco l'idea di base: man mano che i progetti diventano troppo grandi, con troppi collaboratori, diventa impossibile tracciare chi ha fatto cosa e quando. Qualcuno ha introdotto un cambiamento che ha "rotto" l'intero sistema? Come fai a capire di quale cambiamento si tratta? Come tornare indietro, quando le cose funzionavano? Al paese delle meraviglie non compromesso?

Farò un ulteriore passo avanti: diciamo che c'è un progetto che non ha molti collaboratori, solo un piccolo progetto con te come creatore, manutentore e distributore: crei una nuova funzionalità per questo progetto, che introduce subdoli bug che si scopriranno in seguito. Non ricordi quali modifiche hai apportato al codebase esistente per creare questa nuova funzionalità. Problemi?

La risposta a tutti questi problemi è gestire le versioni (versioning)! Avere versioni per tutto il codice che è stato scritto ti assicura di sapere chi ha apportato le modifiche, quali modifiche ed esattamente dove, dall'inizio del progetto!

Ora ti invito a smettere di pensare a git come a una scatola chiusa, aprirlo e scoprire quali tesori ti attendono. Scopri come lavora Git e non avrai mai più problemi a far funzionare le cose. Una volta arrivato in fondo a questo articolo, prometto che ti renderai conto della follia di fare ciò che dice il fumetto XKCD qui sopra. Questo è esattamente ciò che la gestione delle versioni cerca di prevenire.

Come usare Git?

Suppongo che tu conosca i comandi di base di Git, o ne abbia sentito parlare, e che li abbia usati almeno una volta. In caso contrario, ecco un vocabolario di base per aiutarti a iniziare.

repository: un posto per conservare le cose. Con Git indica la cartella in cui si trova il codice

head: un "puntatore" all'ultima versione del codice alla quale stai lavorando.

add: un'azione per dire a Git di tracciare un file.

commit: un'azione per salvare lo stato corrente, in modo che sia possibile visionare questo stato, se necessario

remote: un repository che non è locale. Può essere in un'altra cartella oppure nel cloud (ad esempio GitHub); fa sì che altre persone possano facilmente collaborare, poiché non devono ottenere una copia del codice dal tuo sistema, possono semplicemente ottenerla dal cloud. Inoltre ti assicura di avere una copia di riserva nel caso il tuo computer si rompa

pull: un'azione per ottenere il codice aggiornato dal repository remoto

push: un'azione per inviare il codice aggiornato al repository remoto

merge: un'azione per integrare due versioni differenti di codice

status: visualizza informazioni circa lo stato corrente del repository

Dove si trova Git?

La magia risiede in una cartella nascosta: .git/

In ogni repostitory git, dovresti vedere qualcosa del genere:

$ tree .git/
.git/
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags
8 directories, 14 files

Questo è il modo nel quale Git controlla e gestisce il tuo intero progetto. Esamineremo tutte le parti importanti, una a una.

Git consiste di 3 parti: l'object store, l'indice e la directory di lavoro.

Object Store

Ecco come Git conserva tutto internamente. Per ogni file nel tuo progetto che aggiungi e aggiorni con il comando add, Git genera un hash per il file e conserva il file sotto quell'hash. Per esempio, se ora creo un file helloworld ed eseguo git add helloworld (che dice a Git di aggiungere un file chiamato helloworld all'object store di git), ottengo qualcosa tipo questo:

$ tree .git/
.git/
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── objects
│   ├── a0
│   │   └── 423896973644771497bdc03eb99d5281615b51
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags
9 directories, 16 files

È stato generato un nuovo oggetto! Per quelli che sono interessati a guardare sotto il cofano, Git usa internamente il comando hash-object in questo modo:

$ git hash-object helloworld
a0423896973644771497bdc03eb99d5281615b51

Sì, è lo stesso hash che vediamo all'interno della cartella objects. Perché la sotto-directory con i primi due caratteri dell'hash? Rende la ricerca più veloce.

Successivamente, Git crea un oggetto con il nome dell'hash sopra citato, comprime il file relativo e lo conserva lì. Pertanto puoi anche vedere il contenuto dell'oggetto!

$ git cat-file a0423896973644771497bdc03eb99d5281615b51 -p
hello world!

Questo avviene tutto dietro le quinte. Non dovresti mai usare il comando cat-file nelle operazioni quotidiane di aggiunta/aggiornamento. Userai semplicemente il comando add e Git si occuperà del resto.

Questo è il nostro primo comando git, fatto e finito.

git add crea un hash, comprime il file e aggiunge  l'oggetto compresso all'object store.

La directory di lavoro

Come suggerisce il nome, è dove lavori. Tutti i file che crei e modifichi si trovano nella directory di lavoro. Ho creato un nuovo file, byeworld ed eseguito il comando git status:

$ git status
On branch master
No commits yet
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
new file:   helloworld
Untracked files:
  (use "git add <file>..." to include in what will be committed)
byeworld

I file non tracciati (untracked files) sono i file nella directory di lavoro che non abbiamo detto a git di gestire.

Se non avessimo fatto nulla nella directory di lavoro avremmo il seguente messaggio:

$ git status
On branch master
nothing to commit, working tree clean

che informa che non ci sono cambiamenti e la situazione del codebase è pulita ("nothing to commit, working tree clean"). Ignora per ora le parole "branch" e "commit". Il concetto chiave è che la directory di lavoro è pulita.

L'indice

Questo è il nucleo di Git. Anche noto come staging area. L'indice conserva la corrispondenza dei file con gli oggetti nell'object store. Qui è dove entrano in gioco i commit. Il miglior modo per vederlo è provare!

Eseguiamo un'azione di commit per l'aggiunta del file helloworld

$ git commit -m "Add helloworld"
[master (root-commit) a39b9fd] Add helloworld
 1 file changed, 1 insertion(+)
 create mode 100644 helloworld

Ora la nostra alberatura è:

$ tree .git/
.git/
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
├── objects
│   ├── a0
│   │   └── 423896973644771497bdc03eb99d5281615b51
│   ├── a3
│   │   └── 9b9fdd624c35eee08a36077f411e009da68c2f
│   ├── fb
│   │   └── 26ca0289762a454db2ef783c322fedfc566d38
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    └── tags
14 directories, 22 files

Interessante! Abbiamo due nuovi oggetti nell'object store, e alcune cose che ancora non capiamo nelle cartelle logs e refs. Affidiamoci nuovamente al nostro amico cat-file:

$ git cat-file a39b9fdd624c35eee08a36077f411e009da68c2f -p
tree fb26ca0289762a454db2ef783c322fedfc566d38
author = <=> 1537700068 +0100
committer = <=> 1537700068 +0100
Add helloworld
$ git cat-file fb26ca0289762a454db2ef783c322fedfc566d38 -p
100644 blob a0423896973644771497bdc03eb99d5281615b51 helloworld

Come puoi intuire il primo oggetto rappresenta i metadati del commit: chi ("author", "committer") ha fatto cosa e perché ("Add helloworld"), con un'alberatura ("tree"). Il secondo oggetto è l'effettiva alberatura. Se conosci il file system di unix, sai esattamente di cosa si tratta.

L'alberatura (tree) in Git corrisponde al sistema di file di Git. Tutto è un'alberatura (directory) oppure un file (blob), e a ogni commit, Git conserva anche le informazioni sull'alberatura, per dire a sé stesso: questo è come dovrebbe essere la directory di lavoro a questo punto. Nota che l'alberatura punta esattamente a uno specifico oggetto di ciascun file che contiene (l'hash).

Ora è il momento di parlare dei branch! Il nostro primo commit ha aggiunto qualcosa anche a .git/. Ora il nostro interesse si sposta verso .git/refs/heads/master:

$ cat .git/refs/heads/master 
a39b9fdd624c35eee08a36077f411e009da68c2f

Ecco quello che ti serve sapere sui branch:

Un branch in Git è un puntatore mobile e leggero verso uno di questi commit. Il nome del branch predefinito in Git è master.

Cosa? Mi piace pensare ai branch come a biforcazioni del codice. Vuoi fare qualche modifica ma non vuoi rompere le cose. Decidi di avere una demarcazione più forte di un log di commit, e qui entrano in gioco i branch. master è il branch predefinito, anche usato de-facto come branch di produzione. Da qui, la creazione del file qui sopra. Come puoi dedurre dal contenuto del file, punta al nostro primo commit. Di conseguenza, è un puntatore a un commit.

Esploriamo ulteriormente. Diciamo che ho creato un nuovo branch:

$ git branch the-ending
$ git branch
* master
  the-ending

Eccolo qui, un nuovo branch! Come puoi dedurre, una nuova voce deve essere stata aggiunta a  .git/refs/heads/ e visto che non ci sono commit ulteriori, dovrebbe puntare anche questo al nostro primo commit, proprio come master.

$ cat .git/refs/heads/the-ending
a39b9fdd624c35eee08a36077f411e009da68c2f

Sì, esattamente! Ora ricordi il file byeworld? Questo file non era ancora tracciato, quindi non importa in quale branch ti trovi, quel file sarà sempre lì. Diciamo che voglio spostarmi in questo branch ora, quindi eseguo l'azione di checkout verso il branch.

$ git checkout the-ending
Switched to branch 'the-ending'
$ git branch
  master
* the-ending

Ora, dietro le quinte, Git modificherà tutto il contenuto della directory di lavoro per farlo corrispondere al contenuto indicato dal commit del branch. Per ora, visto che è esattamente lo stesso di master, sembra uguale.

Aggiungo il file byeworld con il comando add ed eseguo il commit.

Cosa ti aspetti che cambi nella cartella objects?

Cosa ti aspetti che cambi nella cartella refs/heads?

Pensaci prima di proseguire.

$ tree .git/
.git/
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           ├── master
│           └── the-ending
├── objects
│   ├── 0b
│   │   └── 17be9dbc34c5a5fbb0b94d57680968efd035ca
│   ├── a0
│   │   └── 423896973644771497bdc03eb99d5281615b51
│   ├── a3
│   │   └── 9b9fdd624c35eee08a36077f411e009da68c2f
│   ├── b3
│   │   └── 00387d818adbbd6e7cc14945fdf4c895de6376
│   ├── d1
│   │   └── 8affe001488123b496ceb34d8b13b120ab4cb6
│   ├── fb
│   │   └── 26ca0289762a454db2ef783c322fedfc566d38
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   ├── master
    │   └── the-ending
    └── tags
17 directories, 27 files

3 nuovi oggetti, 1 per l'aggiunta del file, 2 per il commit! Ha senso? Cosa pensi che contengano gli oggetti?

  • I metadati di commit
  • Il contenuto dell'oggetto generato da add
  • La descrizione dell'alberatura

L'ultima parte del quadro è: come funzionano questi metadati di commit con i metadati del precedente. Bene, cat-file!

$ git cat-file 0b17be9dbc34c5a5fbb0b94d57680968efd035ca -p
100644 blob d18affe001488123b496ceb34d8b13b120ab4cb6 byeworld
100644 blob a0423896973644771497bdc03eb99d5281615b51 helloworld
$ git cat-file b300387d818adbbd6e7cc14945fdf4c895de6376 -p
tree 0b17be9dbc34c5a5fbb0b94d57680968efd035ca
parent a39b9fdd624c35eee08a36077f411e009da68c2f
author = <=> 1537770989 +0100
committer = <=> 1537770989 +0100
add byeworld
$ git cat-file d18affe001488123b496ceb34d8b13b120ab4cb6 -p
Bye world!
$ cat .git/refs/heads/the-ending 
b300387d818adbbd6e7cc14945fdf4c895de6376

Lo vedi? Il puntatore al genitore ("parent")! Esattamente quello che avevi pensato, una lista collegata, che collega i commit insieme!

Vedi l'implementazione del branch? Punta a un commit, l'ultimo che abbiamo effettuato dopo l'azione di checkout! Naturalmente il master dovrebbe ancora puntare al commit di helloworld, giusto?

$ cat .git/refs/heads/master
a39b9fdd624c35eee08a36077f411e009da68c2f

Va bene, abbiamo esaminato tante cose, riassumiamole fino a qui.

TL;DR

Git lavora con gli oggetti, versioni compresse dei file che chiedi a Git di tracciare.

Ogni oggetto ha un ID(entificativo), un hash generato da Git in base al contenuto del file a cui si riferisce.

Ogni volta che esegui il comando add per aggiungere/aggiornare un file, git aggiunge un nuovo oggetto all'object store. Questa è esattamente la ragione per cui non puoi avere a che fare con file molto grandi in Git – salva l'intero file ogni volta che esegui il comando add per aggiungere modifiche, non le differenze tra le versioni (contrariamente a quanto si crede comunemente).

Ogni commit crea 2 oggetti:

  1. L'alberatura: un ID per l'alberatura, che agisce esattamente come una directory unix, che punta ad altre alberature (directory) o blob (file). Questo genera la costruzione dell'intera struttura di directory in base agli oggetti presenti al momento. I blob sono rappresentati dagli oggetti correnti creati dal comando add.
  2. I metadati di commit: un ID per i commit, chi ha eseguito il commit, un alberatura che rappresenta il commit, il messaggio di commit e il commit genitore. Forma una struttura a lista collegata che mette insieme i commit.

I branch sono puntatori a metadati degli oggetti di commit, tutti conservati in .git/refs/heads

Questo è tutto per la comprensione di quanto succede dietro le quinte! Nella prossima parte, esamineremo alcune delle azioni di Git che fanno venire gli incubi alle persone:

reset, merge, pull, push, fetch e come modificano la struttura interna in .git/.

Altri articoli in questa serie:

Ti è piaciuto? Non perdere più un post — iscriviti alla mia mailing list!