Articolo originale: A Visual Guide to Git Internals — Objects, Branches, and How to Create a Repo From Scratch

Molti di noi usano git quotidianamente. Ma quanti di noi sanno cosa succede dietro le quinte?

Per esempio, cosa succede quando usiamo git commit? Cosa viene conservato dopo ogni commit? Sono solo le differenze tra il commit attuale e quello precedente? In questo caso come sono codificate le differenze? Oppure viene salvata un'istantanea del repository ogni volta? Cosa succede veramente quando usiamo git init?

Molti di coloro che usano git non conoscono le risposte alle domande poste qui sopra. Ma ha davvero importanza?

Innanzitutto, come professionisti, dovremmo impegnarci a capire gli strumenti che usiamo, specialmente così di frequente, come git.

Ma soprattutto, ho scoperto che capire come funziona effettivamente git è utile in molte situazioni, sia che si tratti di risolvere conflitti di merge, che si cerchi di eseguire un rebase interessante, o anche solo quando qualcosa va leggermente storto.

Potrai trarre vantaggio da questo post se hai già abbastanza esperienza con git per sentirti a tuo agio con comandi come git pull ,git push ,git add oppure git commit.

Ciononostante, inizieremo con uno sguardo d'insieme per essere sicuri di essere tutti allo stesso livello per quanto riguarda la conoscenza dei meccanismi di git, e in particolare con i termini che verranno usati in questo post.

Ho anche caricato su YouTube una serie che tratta degli argomenti trattati in questo post e che ti invito a guardare qui.

Cosa aspettarsi da questo tutorial

Avremo una approfondita comprensione di ciò che accade dietro le quinte durante le operazioni che facciamo quasi quotidianamente con git.

Inizieremo trattando gli oggetti —  blob, tree (alberi) e commit. Discuteremo quindi brevemente dei branch e di come vengono implementati. Analizzeremo i dettagli della directory di lavoro, dell'area di stage e del repository.

Ci assicureremo anche di capire come questi termini si relazionano rispetto ai comandi git che conosciamo e utilizziamo per creare un nuovo repository.

Successivamente, creeremo un repository da zero , senza usare git init, git add o git commit. Questo ci consentirà di approfondire la nostra comprensione di ciò che avviene dietro le quinte quando lavoriamo con git.

Creeremo anche nuovi branch, ci sposteremo tra di essi e creeremo ulteriori commit, tutto senza usare git branch o git checkout.

Alla fine di questo post, ti sentirai di aver capito git. Sei pronto per farlo?😎

Oggetti Git: blob, alberi (tree) e commit

È molto utile pensare a git come alla gestione di un file system, e in particolare a "istantanee" di un file system in un dato momento.

Un file system inizia con una directory radice (root directory), nei sistemi basati su Unix, /, che in genere contiene altre directory (per esempio /usr o /bin). Queste directory possono contenere altre directory e/o file (per esempio /usr/1.txt).

In git, il contenuto dei file viene conservato in oggetti chiamati blob (binary large objects – grandi oggetti binari).

La differenza tra blob e file è che i file contengono anche metadati. Per esempio un file "ricorda" quando è stato creato, quindi se lo sposti in un'altra directory, la sua data di creazione rimane la stessa.

I blob, diversamente, hanno solo contenuto, flussi binari di dati. Un blob non registra la sua data di creazione, il suo nome o qualsiasi altra cosa diversa dal suo contenuto.

Ogni blob in git viene identificato dal suo hash SHA-1. Gli hash SHA-1 sono di 20 byte e rappresentano in genere 40 caratteri in formato esadecimale. In questo post talvolta mostreremo solo i primi caratteri di questo hash.

Blobs have SHA-1 hashes associated with them

In git, l'equivalente di una directory è un albero (tree). Un albero in pratica è un elenco del contenuto di una directory, che può essere costituito da dei blob così come da altri alberi.

Anche gli alberi sono identificati dal loro hash SHA-1. Per riferirsi a questi oggetti, siano blob o alberi, si usa il loro hash SHA-1.

A tree is a directory listing

Osserva l'albero CAFE7 che  contiene il  blob F92A0 che rappresenta pic.png. In un altro albero, quello stesso blob potrebbe avere un altro nome.

A tree may contain sub-trees, as well as blobs

Il diagramma qui sopra equivale a un file system con una directory radice che ha un file, /test.js, e una directory chiamata /docs nella quale ci sono due file: /docs/pic.png e /docs/1.txt.

Ora è tempo di prendere un'istantanea di quel file system e conservare tutti i file al momento esistenti, assieme al loro contenuto.

In git, un'istantanea è un commit. Un oggetto commit include un puntatore all'albero principale (la directory radice), e anche altri metadati come il l'esecutore del commit (committer), un messaggio di commit, e la marca temporale del commit.

In molti casi, un commit ha anche uno o più commit genitori, vale a dire le istantanee precedenti. Naturalmente anche gli oggetti commit sono identificati dal loro hash SHA-1. Questi sono gli hash mostrati quando usiamo git log.

A commit is a snapshot in time. It refers to the root tree. As this is the first commit, it has no parent(s).

Ogni commit contiene l'intera istantanea, non solo le differenze rispetto ai commit precedenti.

Come può funzionare? Non significa che dovremo conservare un gran volume di dati ad ogni commit?

Esaminiamo cosa succede se modifichiamo il contenuto di un file. Diciamo che vogliamo modificare 1.txt, aggiungendo un punto esclamativo, cioè modifichiamo il contenuto da HELLO WORLD a HELLO WORLD!.

Bene, questa modifica significa avere un nuovo blob, con un nuovo hash SHA-1. Ha senso, visto che sha1("HELLO WORLD") è diverso da sha1("HELLO WORLD!").

Changing the blob results in a new SHA-1

Poiché abbiamo un nuovo hash, allora anche il contenuto dell'albero dovrebbe cambiare, dopo tutto il nostro albero non punta più al blob 73D8A, ma al blob 62E7A . Quando modifichiamo il contenuto di un albero cambiamo anche il suo hash.

image-39
L'albero che punta al blob modificato deve essere modificato a sua volta

Ora, visto che l'hash dell'albero è diverso, dobbiamo cambiare anche l'albero genitore, visto che non punta più all'albero CAFE7, ma all'albero 24601. Ne consegue che anche l'albero genitore avrà un nuovo hash.

The root tree also changes, and so does its hash.

Siamo quasi pronti per creare un nuovo oggetto commit, e sembra che andremo a salvare un gran volume di dati, l'intero file system, ancora una volta! È davvero necessario?

In realtà, alcuni oggetti, blob nello specifico, non sono cambiati rispetto al commit precedente, il  blob F92A0 è rimasto invariato, e anche il  blob F00D1.

Ecco il trucco, fintanto che un oggetto non cambia, non lo salviamo nuovamente. In questo caso non dobbiamo salvare nuovamente i blob F92A0 e blob F00D1. Dobbiamo solo riferirci a essi usando i loro valori di hash. Quindi possiamo creare il nostro oggetto commit.

image-41
I blob che non cambiano sono referenziati tramite i loro valori hash

Visto che questo commit non è il primo commit, ha un genitore, il  commit A1337.

Per ricapitolare, abbiamo introdotto tre oggetti git:

  • blob — il contenuto di un file.
  • tree (albero) — il contenuto di una directory (di blob e alberi).
  • commit — un'istantanea dell'albero di lavoro.

Prendiamo un attimo in considerazione gli hash di questi oggetti. Supponiamo che io abbia scritto la stringa git is awesome! e da essa abbia creato un blob. Tu hai fatto lo stesso sul tuo sistema. Avremo lo stesso hash?

La risposta è  sì. Visto che i blob contengono gli stessi dati, avranno gli stessi valori di hash SHA-1.

Cosa succede se io creo un albero che fa riferimento al blob git is awesome!, e gli do nome e metadati specifici, e tu fai esattamente la stessa cosa sul tuo sistema. Avremo lo stesso hash?

Ancora una volta la risposta è sì. Visto che gli oggetti alberi sono uguali, avranno lo stesso hash.

Se creo un commit di quell'albero con il messaggio di commit Hello, e tu fai lo stesso nel tuo file system. Avremo gli stessi hash?

In questo caso la risposta è negativa. Anche se i nostri oggetti commit fanno riferimento allo stesso albero, hanno dettagli di commit diversi, la marca temporale, l'esecutore del commit, e così via.

Branch in Git

Un branch (ramo) è semplicemente un riferimento nominativo a un commit.

Possiamo sempre fare riferimento a un commit in git tramite il suo hash SHA-1, ma noi umani in genere preferiamo altri modi per denominare gli oggetti. Un branch è un modo di referenziare un commit, ma è semplicemente questo.

Nella maggior parte dei repository, la linea principale di sviluppo viene implementata in un branch chiamato master. È semplicemente un nome, creato quando usiamo git init, pertanto largamente usato. Tuttavia non riveste un significato particolare, e si potrebbe usare un qualsiasi altro nome ci vada a genio.

Tipicamente, il branch punta all'ultimo commit nella linea di sviluppo sulla quale stiamo attualmente lavorando.

A branch is just a named reference to a commit

Per creare un altro branch, in genere usiamo il comando git branch. Facendo questo creiamo in realtà un altro puntatore. Quindi se creiamo un branch chiamato test, usando il comando git branch test, stiamo in realtà creando un altro puntatore direzionato verso lo stesso commit del branch nel quale ci troviamo attualmente.

image-43
Usando git branch si crea un altro puntatore

Come fa git a sapere su quale branch ci troviamo attualmente? Mantiene un puntatore speciale, detto HEAD. In genere HEAD punta a un branch, che a sua volta punta a un commit. In alcuni casi, HEAD può anche puntare direttamente a un commit, ma non ci focalizzeremo su questo.

image-44
HEAD punta al branch nel quale ci troviamo attualmente.

Per rendere attivo il branch test, possiamo usare il comando git checkout test. Possiamo già indovinare cosa fa questo comando in realtà: cambia semplicemente il puntamento di HEAD verso test.

image-45
git checkout test modfiica il puntamento di HEAD

Possiamo anche usare  git checkout -b test prima di creare il branch test, che equivale a eseguire git checkout test, per far puntare HEAD al nuovo branch.

Cosa succede se eseguiamo qualche modifica e creiamo un nuovo commit con git commit? A quale branch verrà aggiunto il nuovo  commit?

La risposta è il branch test, visto che è il branch attivo (quello su cui punta HEAD). Successivamente, il puntatore di test verrà spostato verso il nuovo commit aggiunto. Nota che HEAD punta ancora a test.

image-46
Ogni volta che usiamo git commit, il puntatore del branch si sposta verso il nuovo commit creato.

Quindi se riattiviamo master eseguendo git checkout master, facciamo in modo che HEAD punti nuovamente a master .

image-47

Ora se creiamo un altro commit, verrà aggiunto al branch master (e il suo genitore sarà il commit B2424).

image-48

Come registrare le modifiche in Git

In genere, quando lavoriamo sul nostro codice, lo facciamo da una directory di lavoro (working dir). Una directory di lavoro o albero di lavoro, è una qualsiasi directory nel nostro file system che ha un repository associato. Contiene le cartelle e i file del nostro progetto, e una directory chiamata .git, che esamineremo più approfonditamente in seguito.

Dopo aver fatto alcune modifiche, vogliamo registrarle nel nostro repository. Un repository (repo in breve) è una collezione di commit, ciascuno dei quali rappresenta un archivio di quello che era l'albero di lavoro in una data precedente, sia sulla nostra macchina che su quella di qualcun altro.

Un repository include anche altre cose oltre ai nostri file che contengono il codice, come  HEAD, branch, e così via.

image-49

A differenza di altri strumenti simili che potresti avere usato, git non inserisce le modifiche effettuate dall'albero di lavoro direttamente nel repository. Viceversa le modifiche sono prima registrate in qualcosa chiamato indice (index) o area di stage (staging area).

Entrambi questi termini fanno riferimento alla stessa cosa e sono spesso usati nella documentazione di git. Useremo questi termini in modo intercambiabile all'interno di questo post.

Quando attiviamo (checkout) un branch, git popola l'indice con tutti i contenuti dei file che erano presenti l'ultima volta nella nostra directory di lavoro e come apparivano quando sono stati originariamente verificati. Quando usiamo git commit, il commit viene creato in base allo stato dell'indice.

L'uso dell'indice ci consente di preparare con cura ogni commit. Per esempio potremmo avere due file con modifiche rispetto al nostro ultimo commit nella nostra directory di lavoro. Potremmo volere aggiungere solo uno di essi all'indice (usando git add), quindi usare git commit per registrare solo quelle modifiche.

image-50

I file nella nostra directory di lavoro possono trovarsi in uno di due stati: tracciato (tracked) o non tracciato (untracked).

I file tracciati sono quelli che git conosce. Potrebbero trovarsi nell'ultima istantanea (commit) oppure essere stati portati nell'area di stage adesso.

I file non tracciati, sono tutto il resto, qualunque file o directory nella nostra directory di lavoro che non compariva nella nostra ultima istantanea (commit) e che non si trova attualmente nell'area di stage.

Come creare un repository nel modo convenzionale

Assicuriamoci di capire in che modo i termini che abbiamo introdotto si riferiscono al procedimento di creazione di un repository. Questa è solo una rapida panoramica di alto livello, prima di immergerci molto più a fondo in questo processo.

Nota:  la maggior parte delle videate con i comandi di shell mostra i comandi UNIX. Fornirò comandi sia per Windows che per UNIX, con schermate da Windows, per coprire i sistemi operativi più diffusi. Quando i comandi saranno esattamente gli stessi, li fornirò solo una volta.

Inizializzeremo un nuovo repository usando git init repo_1, quindi ci sposteremo nella directory di quel repository usando cd repo_1. Digitando tree /f .git possiamo vedere che l'esecuzione di git init ha prodotto parecchie sottodirectory all'interno di .git. (L'opzione del comando tree  /f include anche i file nel risultato dell'esecuzione del comando).

image-51

Creiamo un file all'interno della directory repo_1 (versione Windows):

image-52

Sui sistemi Linux e macOS:

image-53

Questo file si trova all'interno della nostra directory di lavoro. Tuttavia, visto che non è ancora stato inserito nell'area di stage, attualmente risulta non tracciato (untracked). Verifichiamolo con il comando git status:

image-54
Il nuovo file non è tracciato visto che non è stato ancora inserito nell'area di stage, e non era compreso in un precedente commit

Ora possiamo aggiungere questo file all'area di stage con il comando git add new_file.txt. Possiamo verificare che il file si trova nell'area di stage eseguendo git status:

image-55
Aggiunto un nuovo file nell'area di stage

Ora possiamo creare un commit con git commit:

image-56

È cambiato qualcosa nella directory .git? Eseguiamo tree /f .git per verificare:

image-57
Sono cambiate molte cose all'interno di .git

Sembra che siano cambiate parecchie cose. È ora di esaminare approfonditamente la struttura di .git e capire cosa succede dietro le quinte quando eseguiamo git init, git add oppure git commit.

Ora di andare alle fondamenta

Fino ad ora abbiamo trattato alcuni degli aspetti base di Git, ora siamo pronti per andare davvero a fondo.

Per capire a fondo come funziona git, dovremo creare un repository, questa volta però lo faremo partendo da zero.

Non useremo git init, git add o git commit, il che ci consentirà di ottenere una migliore comprensione pratica del procedimento.

Come impostare .git

Creiamo una nuova directory, portiamoci all'interno di essa ed eseguiamo git status:

image-106

Va bene, git non sembra contento visto che non ha trovato nessuna cartella .git. La cosa naturale da fare sarebbe semplicemente crearla, poi rieseguire git status:

image-107

Apparentemente, non è sufficiente creare una cartella .git . Ci serve qualche cosa da immettere in quella directory.

Un repository git ha due componenti principali:

  1. Una collezione di oggetti — blob, alberi, e commit.
  2. Un sistema di denominazione di quegli oggetti, o riferimenti (references).

Un repository potrebbe anche contenere altre cose, come gli hook di git, ma come minimo deve includere oggetti e riferimenti.

Creiamo quindi una directory per gli oggetti, .git\objects e una per i riferimenti,  .git\refs (sui sistemi UNIX .git/objects e .git/refs, rispettivamente).

image-108

Un tipo di riferimento sono i branch. Internamente, git chiama i branch con il nome head. Quindi andremo a creare una directory apposita, .git\refs\heads.

image-109

Lo stato non cambia ancora eseguendo git status:

image-110

Come fa git a sapere dove iniziare a cercare un commit nel repository? Come spiegato in precedenza, cerca HEAD, che punta al branch attivo corrente (o commit in taluni casi).

Quindi dobbiamo creare HEAD, che è semplicemente un file che si trova in ./git/HEAD. Per farlo eseguiamo:

Su Windows: > echo ref: refs/heads/master > .git\HEAD

Su sistemi UNIX: $ echo "ref: refs/heads/master" > .git/HEAD

⭐ Adesso sappiamo come viene implementato HEAD, è semplicemente un file e il suo contenuto descrive verso quale oggetto puntare.

Dopo l'esecuzione dei comandi qui sopra, git status sembra cambiare idea:

image-111
HEAD è semplicemente un file

Ora git crede che siamo nel branch chiamato master, anche se non abbiamo creato questo branch. Come detto prima,  master è solo un nome. Avremmo potuto far credere a  git di essere su un branch chiamato banana se avessimo voluto:

image-112
🍌

Per il resto di questo post torneremo a master, semplicemente per conformarci alla normale convenzione.

Ora che la nostra directory .git è pronta, possiamo iniziare le operazioni per creare un commit (come già detto, senza usare git add oppure git commit).

I comandi in Git, idraulica vs. ceramica

A questo punto sarebbe utile fare una distinzione tra due tipi di comando in git: idraulici e in ceramica. La definizione è presa in prestito da termini usati dagli idraulici quando si parla di tazza (già, questa — 🚽), che tradizionalmente è fatta di ceramica mentre l'infrastruttura (tubi e scarichi) costituisce l'idraulica.

Mantenendo l'analogia, possiamo dire che la ceramica fornisce all'utente un'interfaccia amichevole per accedere a tubature e scarichi. La maggior parte delle persone ha a che fare con la ceramica. Tuttavia, quando succede qualcosa di veramente brutto, e qualcuno vuole capirne il motivo, deve rimboccarsi le maniche e controllare le tubature e gli scarichi. (Nota: questi termini non sono miei, sono usati in modo estensivo in git).

git usa questa terminologia come analogia per distinguere i comandi di basso livello che in genere gli utenti non usano direttamente (i comandi idraulici che agiscono su tubi e scarichi) dai comandi di alto livello più amichevoli per gli utenti (comandi in ceramica).

Fino a ora abbiamo avuto a che fare con i comandi in ceramica, git init, git add o git commit. Di seguito passeremo ai comandi idraulici.

Come creare oggetti in Git

Partiamo creando un oggetto e scrivendolo nel database degli oggetti di git, che si trova in .git\objects. Troveremo un valore di hash SHA-1 per un blob usando il nostro primo comando di basso livello (idraulico), git hash-object, in questo modo:

Su Windows:

> echo git is awesome | git hash-object --stdin

Su sistemi UNIX:

$ echo "git is awesome" | git hash-object --stdin

Usando --stdin diciamo a git hash-object di prendere i suoi dati dallo standard input. Questo ci fornirà il relativo valore hash valido.

Per scrivere effettivamente quel blob nel database degli oggetti di git possiamo semplicemente aggiungere l'opzione -w a git hash-object. Successivamente possiamo verificare il contenuto della cartella .git e vedere cosa è cambiato.

image-113
Scrittura di un blob nel database degli oggetti

Adesso possiamo vedere che l'hash del nostro blob è 54f6...36. Possiamo anche vedere che è stata creata una sottodirectory in .git\objects chiamata 54, il file è stato chiamato f6...36.

Quindi git in realtà prende i primi due caratteri dell'hash SHA-1 e li usa come nome di una directory. I restanti caratteri dell'hash vengono usati come nome del file che contiene il blob.

Come mai? Immagina un repository piuttosto grande, uno che abbia 300.000 oggetti (blob, alberi, e commit) nel suo database. Può servire molto tempo per cercare un hash in una lista di 300.000 elementi. Per questo git divide semplicemente il problema per 256.

Per cercare l'hash qui sopra, git per prima cosa cerca una directory chiamata 54 all'interno di .git\objects, che potrebbe avere fino a 256 directory (da 00 a FF). Poi cerca in quella directory, restringendo la ricerca mano a mano che la stessa progredisce.

Torniamo al nostro procedimento per generare un commit. Ora che abbiamo creato un oggetto, di che tipo è? Possiamo usare un altro comando di basso livello, git cat-file -t (-t sta per “tipo”), per verificarlo:

image-114

Non c'è da sorprendersi, questo oggetto è un blob. Possiamo anche usare  git cat-file -p (-p sta per “pretty-print” - bella stampa) per vederne il contenuto:

image-115

Il procedimento di creazione di un blob in genere si verifica quando aggiungiamo qualcosa nell'area di stage, vale a dire quando usiamo git add.

Ricorda che git crea un blob dell'intero file portato in area di stage. Anche se viene modificato un solo carattere (come nell'esempio di prima quando abbiamo aggiunto un !), il file avrà un nuovo blob con un nuovo hash.

Vedremo modifiche se verifichiamo lo stato del repository usando git status?

image-116

Apparentemente no. Aggiungere un oggetto blob al database interno di git non modifica lo stato, visto che git non sa in questa fase quali file siano tracciati o meno.

Dobbiamo tracciare questo file, aggiungendolo all'area di stage. A questo scopo possiamo usare il comando di basso livello git update-index, così: git update-index --add --cacheinfo 100644 <blob-hash> <nomefile>.

Nota: cacheinfo è un file in modalità 16-bit conservato da git, seguendo le direttive di tipi e modalità POSIX. Questo va oltre lo scopo di questo post.

Eseguendo il comando qui sopra otterremo una modifica nel contenuto di .git:

image-117

Riesci a identificare le modifiche? È stato creato un nuovo file chiamato index . Eccolo, il famoso indice (o area di stage), è praticamente un file che si trova nella directory .git.

Adesso che il nostro blob è stato aggiunto all'indice, ci aspettiamo che il risultato di git status sia diverso, come questo:

image-118

Interessante! Sono successe due cose.

La prima, possiamo vedere che my_file.txt è visualizzato in verde nella sezione Changes to be committed (modifiche da portare in commit). Questo perché l'indice ora contiene my_file.txt, in attesa di essere portato in un commit.

La seconda, osserviamo che my_file.txt viene visualizzato in rosso, poiché git crede che my_file.txt sia stato eliminato e il fatto che il file sia stato eliminato non è stato registrato in area di stage.

Questo succede in quanto abbiamo aggiunto un blob con il contenuto git is awesome al database degli oggetti, e abbiamo detto all'indice che my_file.txt ha il contenuto di quel  blob, ma in realtà non abbiamo mai veramente creato quel file.

Possiamo facilmente risolvere prendendo il contenuto del blob e scrivendolo nel nostro file system, in un file chiamato my_file.txt:

image-119

Ne consegue che my_file.txt non appare più in rosso nel risultato di  git status:

image-120

È ora di creare un oggetto commit dalla nostra area di stage. Come spiegato sopra, un oggetto commit ha un riferimento a un albero, quindi dobbiamo crearlo.

Lo possiamo fare con il comando git write-tree, che registra il contenuto dell'indice in un oggetto albero. Naturalmente possiamo usare git cat-file -t per verificare che in effetti si tratti di un albero:

image-121
Creazione di un oggetto albero nell'indice

Possiamo usare git cat-file -p per vedere il contenuto:

image-122

Grande, abbiamo creato un albero, e ora ci serve creare un oggetto commit che faccia riferimento a questo albero. Per farlo possiamo usare git commit-tree <hash-albero> -m <messaggio_di_commit>:

image-123

Ora dovresti essere a tuo agio con i comandi usati per verificare il tipo di oggetto creato, e per stamparne il contenuto:

image-124
Creazione di un oggetto commit

Osserva che questo commit non ha un genitore, visto che è il primo commit. Quando aggiungeremo un altro commit, dovremo dichiarare il suo genitore, lo faremo più tardi.

L'ultimo hash che abbiamo ottenuto,  80e...8f è l'hash di un commit. Questi hash ci devono essere piuttosto familiari, ci abbiamo a che fare tutte le volte. Nota che un commit detiene un oggetto albero, con un proprio hash, che raramente specifichiamo esplicitamente.

Qualcosa è cambiato nel risultato di  git status?

image-125

Niente 🤔.

Come mai? Bene, per sapere che il nostro file è stato portato in commit, a git serve conoscere il nostro ultimo commit. Come ci riesce? Legge HEAD:

image-126
Stampare il contenuto di HEAD su Windows
image-127
Stampare il contenuto di HEAD su sistemi UNIX

HEAD punta a master, ma cos'è master? Non l'abbiamo ancora creato.

Come spiegato in precedenza, un branch è semplicemente un riferimento nominativo a un commit. In questo caso, vorremmo che master facesse riferimento al commit con hash 80e8ed4fb0bfc3e7ba88ec417ecf2f6e6324998f.

Possiamo farlo creando semplicemente un file master in  \refs\heads, con questo hash come contenuto, così:

image-128

⭐ Alla fine, un branch è semplicemente un file all'interno di .git\refs\heads, che contiene un hash del commit al quale si riferisce.

Ora, finalmente, git status e git log sembrano apprezzare i nostri sforzi:

image-129

Abbiamo creato con successo un commit senza usare i comandi di alto livello (quelli in ceramica)! Forte, non è vero? 🎉

Come lavorare con i branch in Git: dietro le quinte

Proprio come abbiamo creato un  repository e un commit senza usare git init, git add o git commit, ora possiamo creare e spostarci tra i branch senza usare i comandi di primo livello (quelli in ceramica), git branch o git checkout.

È perfettamente legittimo che tu sia eccitato, lo sono anche io 🙂

Iniziamo:

Finora abbiamo solo un branch, che si chiama master. Per crearne un altro che chiameremo test (l'equivalente di git branch test), dobbiamo semplicemente creare un file chiamato test all'interno di .git\refs\heads, e il contenuto di quel file sarà lo stesso hash del commit al quale master punta.

image-130

Se usiamo git log, possiamo vedere che questo è in effetti il caso, sia master che test puntano a questo commit:

image-131

Ora spostiamoci nel nostro branch appena creato (l'equivalente di git checkout test). A questo scopo, dovremo modificare HEAD per farlo puntare al nostro nuovo branch:

image-132
Spostamento al branch test modificando HEAD

Come possiamo vedere, sia git status che git log confermano che HEAD ora punta a  test, che è, di conseguenza, il branch attivo.

Ora possiamo avvalerci dei comandi già usati per creare un altro file e aggiungerlo all'indice:

image-133

Con i comandi qui sopra, abbiamo creato un file chiamato test.txt, che contiene la parola Testing, creato il blob corrispondente, e lo abbiamo aggiunto all'indice. Abbiamo anche creato un albero che rappresenta l'indice.

Adesso creiamo un commit che fa riferimento a questo albero. Questa volta, dovremmo anche specificare il genitore di questo commit, che sarà il commit precedente. Specifichiamo il genitore usando l'opzione -p per  git commit-tree:

image-136

Abbiamo appena creato un commit, con albero e un genitore, come possiamo vedere:

image-139

Il risultato di git log ci mostrerà il nuovo commit?

image-138

git log non ci mostra nulla di nuovo, come puoi vedere. Perché?🤔 Ricorda che git log traccia i branch per trovare commit rilevanti da mostrare. Adesso ci mostra test e il  commit a cui punta, e ci mostra anche master, che punta allo stesso commit.

Esatto, dobbiamo modificare  test per farlo puntare al nostro nuovo commit. Lo facciamo modificando semplicemente il contenuto di  .git\refs\heads\test:

image-140

Ha funzionato! 🎉🥂

git log passa per HEAD, che gli dice di andare al branch test, che a sua volta punta al commit 465...5e, il quale fa riferimento nuovamente al suo commit genitore 80e...8f.

Meraviglioso, non è vero? 😊

Questo post ti ha introdotto ai meccanismi interni di git. Abbiamo iniziato trattando gli oggetti di base, blob, alberi e commit.

Abbiamo appreso che un blob conserva il contenuto di un file. Un albero è un elenco di directory contenente blob e/o sottoalberi. Un blob è un'istantanea della nostra directory di lavoro, con alcuni metadati come la marca temporale e il messaggio di commit.

Abbiamo quindi discusso dei branch e spiegato che non sono altro che un riferimento nominativo a un commit.

Abbiamo continuato descrivendo la directory di lavoro, una directory a cui è associato un repository, l'area di stage (indice) che contiene l'albero per il commit successivo e il repository, che è una raccolta di commit.

Abbiamo chiarito come questi termini si relazionano ai comandi git che conosciamo creando un nuovo repository ed eseguendo il commit di un file utilizzando i ben noti comandi git init, git add e git commit.

Quindi ci siamo immersi senza paura in git. Abbiamo smesso di usare comandi di alto livello (in ceramica) e siamo passati a comandi di basso livello (idraulici).

Usando echo e comandi di basso livello come git hash-object, siamo stati in grado di creare un blob, aggiungerlo all'indice, creare un albero dell'indice e creare un oggetto commit che punta a quell'albero.

Siamo stati anche in grado di creare e passare da un branch all'altro. Complimenti a quelli di voi che l'hanno provato da soli!👏

Spero che dopo aver seguito questo post sentirai di aver approfondito la tua conoscenza di ciò che accade dietro le quinte quando lavori con git.

Grazie per aver letto! Se ti è piaciuto questo articolo, puoi leggere di più su questo argomento sul blog swimm.io.

Omer Rosenbaum, Chief Technology Officer per Swimm. Esperto di cyber training e fondatore di Checkpoint Security Academy. Autore di Computer Networks (in Ebraico).

Visita il mio Canale YouTube

Risorse addizionali

Molto è stato scritto e detto su git. In particolare, ho trovato utili queste risorse (in lingua inglese - n.d.t.):