In questa lezione parleremo di una delle funzionalità più interessanti di Git, ovvero dei Branch. Vedremo cosa sono, come crearne uno e altre operazioni che possono tornare utili quando si lavora con Git.
Cos’è un branch
In una delle precedenti lezioni abbiamo descritto come funziona Git internamente e abbiamo iniziato ad analizzare il contenuto della directory .git che viene creata all’interno di una cartella in cui abbiamo lanciato il comando git init per inizializzare il repository.
Ricordiamo che all’interno della cartella .git era presente una sottocartella objects in cui Git salva degli oggetti che possono essere di tipo Blob, Tree, Commit o Annotated Tag. Ogni commit è sostanzialmente un’istantanea della working directory ad un certo istante di tempo. Ogni nuovo commit creato nel repository, mantiene al suo interno un riferimento a un commit precedente. Un Branch non è altro che un puntatore a un commit.
Al contrario dei tag che sono delle etichette fisse, man mano che creiamo un nuovo commit, il branch corrente si sposta e punta al nuovo commit creato. Git usa il puntatore speciale HEAD per capire qual è il branch corrente. In condizioni normali, HEAD contiene un riferimento al branch corrente. Quando creiamo un nuovo repository, Git crea in automatico il branch di default che prende il nome di master.
Git mantiene le informazioni sui branch creati nella cartella .git/refs/heads nella directory base di un progetto in cui si usa Git. Per ogni branch verrà creato un file contenente il valore di hash identificativo di un determinato commit.
$ tree .git/refs/
.git/refs/
├── heads
│ └── master
└── tags
2 directories, 1 file
Possiamo visualizzare il contenuto di un file usando il comando cat.
$ cat .git/refs/heads/master
56298e83bccf0a0142ff9984de690420f754de5b
Allo stesso modo possiamo stampare a video il contenuto del file .git/HEAD.
$ cat .git/HEAD
ref: refs/heads/master
Creare un nuovo Branch in Git
Per creare un nuovo branch in Git, basta usare un semplice comando. Vediamo un esempio in cui inseriamo in una nuova cartella un file che chiamiamo configurazione_pc.txt.
[esempio_git_branch] (master) $ git init
Initialized empty Git repository in /Users/claudio/test_git_branch/.git/
[esempio_git_branch] (master) $ touch configurazione_pc.txt
[esempio_git_branch] (master) $ git add .
[esempio_git_branch] (master) $ git commit -m 'first commit'
[master (root-commit) 403012f] first commit
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 configurazione_pc.txt
[esempio_git_branch] (master) $ cat .git/refs/heads/master
403012febcf853449e519c252582998c0ca5c4cd
[esempio_git_branch] (master) $ cat .git/HEAD
ref: refs/heads/master
Modifichiamo quindi il file configurazione_pc.txt con una prima configurazione che contiene dei componenti e i prezzi di qualche sito e-commerce.
# Configurazione PC
AMD Ryzen 3 1200 - 110€
Asrock A320M - 52€
Ballistix Sport LT 8 GB Kit DDR4 - 98€
Drevo X1 240GB - 68€
Sapphire Radeon RX 560 2GB GDDR5 - 100€
Corsair VS450 - 39€
DeepCool Tesseract Black - 49€
=======================================
Totale 516€
Eseguiamo quindi un nuovo commit dopo aver salvato il file contenente la configurazione iniziale.
[esempio_git_branch] (master) $ git add configurazione_pc.txt
[esempio_git_branch] (master) $ git commit -m 'Aggiunge la configurazione iniziale al file configurazione_pc.txt'
[master 06ecc12] Aggiunge la configurazione iniziale al file configurazione_pc.txt
1 file changed, 11 insertions(+)
[esempio_git_branch] (master) $ git log --oneline --decorate
06ecc12 (HEAD -> master) Aggiunge la configurazione iniziale al file configurazione_pc.txt
403012f first commit
[esempio_git_branch] (master) $ cat .git/refs/heads/master
06ecc12a2b408980400a4f120283df03a1d1094a
[esempio_git_branch] (master) $ cat .git/HEAD
ref: refs/heads/master
A questo punto supponiamo di chiedere il preventivo per gli stessi componenti a un primo negozio. Creiamo quindi un nuovo branch che chiamiamo primo_negozio con il comando git branch <nome_del_branch>
[esempio_git_branch] (master) $ git branch primo_negozio
Possiamo visualizzare i branch del repository digitando il comando git branch senza specificare nessuna opzione o argomento.
[esempio_git_branch] (master) $ git branch
* master
primo_negozio
Come possiamo notare, Git indica con un asterisco ‘*’ qual è il branch corrente. (Non avendo cambiato branch, il branch corrente resta master, ovvero HEAD contiene un riferimento al branch master) Possiamo visualizzare le stesse informazioni, analizzando il contenuto del file HEAD e della cartella .git/refs/heads
[esempio_git_branch] (master) $ tree .git/refs/heads
.git/refs/heads
├── master
└── primo_negozio
0 directories, 2 files
[esempio_git_branch] (master) $ cat .git/refs/heads/master
06ecc12a2b408980400a4f120283df03a1d1094a
[esempio_git_branch] (master) $ cat .git/refs/heads/primo_negozio
06ecc12a2b408980400a4f120283df03a1d1094a
[esempio_git_branch] (master) $ cat .git/HEAD
ref: refs/heads/master
A questo punto possiamo cambiare branch col seguente comando. (Dopo aver eseguito il comando, se configurato correttamente, il prompt dovrebbe mostare il nome del nuovo branch come branch corrente.)
[esempio_git_branch] (master) $ git checkout primo_negozio
In alternativa possiamo lanciare il comando git checkout -b primo_negozio che permette di creare e spostarsi immediatamente sul nuovo branch.
[esempio_git_branch] (primo_negozio) $ cat .git/HEAD
ref: refs/heads/primo_negozio
Attraverso il comando git checkout abbiamo cambiato il branch corrente, ovvero è stato cambiato il contenuto del file .git/HEAD. Il puntatore HEAD contiene ora un riferimento al branch primo_negozio che è quindi il branch corrente.
Ogni commit che creiamo da ora in poi farà parte del branch primo_negozio. Immaginiamo quindi di modificare il file configurazione_pc.txt supponendo che il primo negozio, a cui chiediamo un preventivo, conferma i prezzi che avevamo trovato online, ma dà in regalo anche un hard disk.
# Configurazione PC
AMD Ryzen 3 1200 - 110€
Asrock A320M - 52€
Ballistix Sport LT 8 GB Kit DDR4 - 98€
Drevo X1 240GB - 68€
Sapphire Radeon RX 560 2GB GDDR5 - 100€
Corsair VS450 - 39€
DeepCool Tesseract Black - 49€
=======================================
Totale 516€
Hard disk 3.5 1TB in regalo
[esempio_git_branch] (primo_negozio) $ git status
On branch primo_negozio
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: configurazione_pc.txt
no changes added to commit (use "git add" and/or "git commit -a")
[esempio_git_branch] (primo_negozio) $ git commit -am 'Aggiorna il file configurazione_pc.txt - Hard disk in regalo'
[primo_negozio ad0ebc7] Aggiorna il file configurazione_pc.txt - Hard disk in regalo
1 file changed, 3 insertions(+), 1 deletion(-)
[esempio_git_branch] (primo_negozio) $ git log --oneline --decorate
ad0ebc7 (HEAD -> primo_negozio) Aggiorna il file configurazione_pc.txt - Hard disk in regalo
06ecc12 (master) Aggiunge la configurazione iniziale al file configurazione_pc.txt
403012f first commit
Come possiamo notare dall’output del comando git log e dall’immagine, è stato creato un nuovo commit sul branch primo_negozio che non fa però parte del branch master.
Il comando git merge
Supponiamo ora di voler incorporare il nuovo commit appena creato nel branch master, d’altronde è il preventivo più vantaggioso disponibile al momento. Per incorporare uno o più commit di un branch in un altro possiamo avvalerci del comando git merge che viene utilizzato per combinare i due branch. Il branch che vogliamo incorporare (nel nostro caso primo_negozio) resterà invariato, il branch in cui vengono incorporati i commit subirà delle modifiche. Git cerca infatti di integrare le modifiche del branch primo_negozio nel branch master e modificare opportunamente i file in modo da rispecchiare tali cambiamenti. In questo modo possiamo unire le variazioni effettuate su un branch con quelle presenti in un altro branch.
Il comando git merge permette quindi di unire due branch differenti incorporando le modifiche apportate nei commit di un branch, che passiamo come argomento, nel branch corrente. Come possiamo osservare dall’immagine in alto, ci troviamo in un caso particolare in cui il percorso che unisce i due branch è lineare. Git si limiterà quindi a ‘spostare’ il branch master in avanti in modo da puntare al nuovo commit. Si parla in questo caso di Fast-forward merge che è un caso particolare in cui non viene creato un nuovo commit, ma viene solo riposizionato il branch in cui vengono incorporate le modifiche. D’altronde il branch master contiene tutti i commit eccetto l’ultimo appena creato sul branch primo_negozio.
Vediamo quindi la procedura da seguire. Quando abbiamo lasciato il nostro esempio, primo_negozio era il branch corrente. Assicuriamoci quindi che nella working directory non ci siano modifiche rimaste in sospeso.
[esempio_git_branch] (primo_negozio) $ git status
On branch primo_negozio
nothing to commit, working tree clean
Spostiamoci nuovamente sul branch master con il comando git checkout.
[esempio_git_branch] (primo_negozio) $ git checkout master
Switched to branch 'master'
[esempio_git_branch] (master) $ cat .git/HEAD
ref: refs/heads/master
[esempio_git_branch] (master) $ git log --oneline --decorate
06ecc12 (HEAD -> master) Aggiunge la configurazione iniziale al file configurazione_pc.txt
403012f first commit
Possiamo quindi incorporare, il nuovo commit in master.
[esempio_git_branch] (master) $ git merge primo_negozio
Updating 06ecc12..ad0ebc7
Fast-forward
configurazione_pc.txt | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
[esempio_git_branch] (master) $ git log --oneline --decorate
ad0ebc7 (HEAD -> master, primo_negozio) Aggiorna il file configurazione_pc.txt - Hard disk in regalo
06ecc12 Aggiunge la configurazione iniziale al file configurazione_pc.txt
403012f first commit
Se controlliamo ora il contenuto del file configurazione_pc.txt presente nella working directory, noteremo che è stato aggiornato con le modifiche che erano state apportate al file nel branch primo_negozio.
Supponiamo ora che il sito ecommerce, su cui avevamo trovato inizialmente i componenti del pc, abbassi il prezzo della scheda madre. Modifichiamo quindi il file configurazione_pc.txt con il nuovo prezzo (Asrock A320M motherboard – 49€)
# Configurazione PC
AMD Ryzen 3 1200 - 110€
Asrock A320M motherboard - 49€
Ballistix Sport LT 8 GB Kit DDR4 - 98€
Drevo X1 240GB - 68€
Sapphire Radeon RX 560 2GB GDDR5 - 100€
Corsair VS450 - 39€
DeepCool Tesseract Black - 49€
=======================================
Totale 513€
Hard disk 3.5 1TB in regalo
Effettuiamo ora un nuovo commit sul branch master.
[esempio_git_branch] (master) $ git commit -am 'Aggiorna il prezzo della scheda madre'
[master 150adc1] Aggiorna il prezzo della scheda madre
1 file changed, 2 insertions(+), 2 deletions(-)
Immaginiamo quindi di contattare il negozio per chiedere uno sconto sulla scheda madre. In risposta ci viene proposto un prezzo vantaggioso su un nuovo SSD di una marca diversa. Ci spostiamo quindi nuovamente sul branch primo_negozio e modifichiamo il file come mostrato sotto. (Samsung 850EVO 250GB – 72€)
[esempio_git_branch] (master) $ git checkout primo_negozio
# Configurazione PC
AMD Ryzen 3 1200 - 110€
Asrock A320M - 52€
Ballistix Sport LT 8 GB Kit DDR4 - 98€
Samsung 850EVO 250GB - 72€
Sapphire Radeon RX 560 2GB GDDR5 - 100€
Corsair VS450 - 39€
DeepCool Tesseract Black - 49€
=======================================
Totale 520€
Hard disk 3.5 1TB in regalo
Notate che le modifiche salvate nell’ultimo commit del branch master non sono presenti nel file che abbiamo modificato, infatti quel commit non fa parte del branch primo_negozio. Eseguiamo dunque un nuovo commit con i cambiamenti effettuati sul branch primo_negozio.
[esempio_git_branch] (primo_negozio) $ git commit -am 'Cambia il modello di SSD con 850 EVO'
[primo_negozio 9ace921] Cambia il modello di SSD con 850 EVO
1 file changed, 2 insertions(+), 2 deletions(-)
[esempio_git_branch] (primo_negozio) $ git log --oneline --decorate
9ace921 (HEAD -> primo_negozio) Cambia il modello di SSD con 850 EVO
ad0ebc7 Aggiorna il file configurazione_pc.txt - Hard disk in regalo
06ecc12 Aggiunge la configurazione iniziale al file configurazione_pc.txt
403012f first commit
Eseguendo il comando git log notiamo che non compare nessuna informazione sul branch master. Il motivo è che, come già abbiamo evidenziato in una delle lezioni precedenti, partendo dal commit corrente e andando a ritroso non incontriamo il commit a cui punta attualmente master.
In particolare abbiamo una divergenza dei due rami, in cui i due branch presenti puntano ciascuno a un commit non raggiungibile a partire dall’altro branch.
Supponiamo quindi di voler eseguire un nuovo merge perché abbiamo convinto il negoziante a pareggiare il prezzo della scheda madre praticato dallo store ecommerce. In questo caso, rimanendo sul branch primo_negozio includiamo le modifiche del branch master. Git non potrà eseguire un’unione di tipo Fast-forward, dovrà creare un nuovo commit in cui incorporare le modifiche dei due commit 150ad e 9ace9. Verrà quindi spostato il branch primo_negozio che punterà al nuovo commit.
Prima di procedere ed eseguire il comando git merge, dobbiamo soffermarci un momento a riflettere su cosa succede una volta completata l’operazione di merging. Nell’esempio visto in precedenza, infatti, ci eravamo limitati soltanto ad aggiungere una riga al file configurazione_pc.txt per cui il compito di Git era stato piuttosto semplice. Infatti, eseguendo il merge, era stata appesa la nuova riga al file. Ora potremmo chiederci cosa farà Git per ottenere la nuova versione del file configurazione_pc.txt visto che vi sono alcune righe che differiscono fra i due file. Eseguiamo il comando git diff per visualizzare le differenze fra le due versioni del file configurazione_pc.txt nei branch master e primo_negozio.
[esempio_git_branch] (primo_negozio) $ git diff master primo_negozio
diff --git a/configurazione_pc.txt b/configurazione_pc.txt
index 009e3d1..146fd98 100644
--- a/configurazione_pc.txt
+++ b/configurazione_pc.txt
@@ -1,13 +1,13 @@
# Configurazione PC
AMD Ryzen 3 1200 - 110€
-Asrock A320M motherboard - 49€
+Asrock A320M - 52€
Ballistix Sport LT 8 GB Kit DDR4 - 98€
-Drevo X1 240GB - 68€
+Samsung 850EVO 250GB - 72€
Sapphire Radeon RX 560 2GB GDDR5 - 100€
Corsair VS450 - 39€
DeepCool Tesseract Black - 49€
=======================================
-Totale 513€
+Totale 520€
Hard disk 3.5 1TB in regalo
Viene riportato il contenuto del file configurazione_pc.txt presente nel commit a cui punta master e con un segno ‘-‘ sono segnate le righe presenti in ‘master’ ma non in ‘primo_negozio’, viceversa con il segno ‘+’ sono marcate le righe presenti solo in ‘primo_negozio’. Le altre righe sono presenti in entrambi i file.
Eseguiamo allora il comando git merge e vediamo cosa succede.
[esempio_git_branch] (primo_negozio) $ git merge master
Auto-merging configurazione_pc.txt
CONFLICT (content): Merge conflict in configurazione_pc.txt
Automatic merge failed; fix conflicts and then commit the result.
[esempio_git_branch] (primo_negozio | MERGING) git status
On branch primo_negozio
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: configurazione_pc.txt
no changes added to commit (use "git add" and/or "git commit -a")
Git segnala che le due versioni del file configurazione_pc.txt, che si vogliono unire a formare un unico file, generano un conflitto. Dobbiamo essere noi a risolvere i conflitti perché il merge automatico non può essere completato. Una volta corretto il file configurazione_pc.txt possiamo generare il nuovo commit. In alternativa, Git dà la possibilità di annullare l’operazione di merge eseguendo il comando git merge –abort. Data la semplicità del file, risolviamo i conflitti manualmente.
Apriamo allora il file configurazione_pc.txt con un editor.
1 # Configurazione PC
2
3 AMD Ryzen 3 1200 - 110€
4 Asrock A320M motherboard - 49€
5 Ballistix Sport LT 8 GB Kit DDR4 - 98€
6 Samsung 850EVO 250GB - 72€
7 Sapphire Radeon RX 560 2GB GDDR5 - 100€
8 Corsair VS450 - 39€
9 DeepCool Tesseract Black - 49€
10 =======================================
11 <<<<<<< HEAD
12 Totale 520€
13 =======
14 Totale 513€
15 >>>>>>> master
16
17 Hard disk 3.5 1TB in regalo
Git riesce a risolvere senza problemi le differenze fra le due versioni dei file presenti in corrispondenza delle righe 4 e 6. Vengono infatti aggiunte nel nuovo file le ultime modifiche apportate nei due commit 150ad (Asrock A320M motherboard – 49€) e 9ace9 (Samsung 850EVO 250GB – 72€). Git però non riesce a capire quale dei due valori del totale deve usare. Vengono quindi inserite delle righe aggiuntive per aiutarci a risolvere il conflitto.
11 <<<<<<< HEAD
12 Totale 520€
13 =======
14 Totale 513€
15 >>>>>>> master
Fra la riga 11 e 13 è riportata la porzione del file del commit corrente (ovvero il commit a cui punta il branch corrente e quindi HEAD) mentre fra la riga 13 e 15 è riportato il testo presente nella versione del branch master.
Possiamo risolvere manualmente inserendo il nuovo totale. Salviamo quindi il file ed eseguiamo il commit.
1 # Configurazione PC
2
3 AMD Ryzen 3 1200 - 110€
4 Asrock A320M motherboard - 49€
5 Ballistix Sport LT 8 GB Kit DDR4 - 98€
6 Samsung 850EVO 250GB - 72€
7 Sapphire Radeon RX 560 2GB GDDR5 - 100€
8 Corsair VS450 - 39€
9 DeepCool Tesseract Black - 49€
10 =======================================
11 Totale 517€
12
13 Hard disk 3.5 1TB in regalo
[esempio_git_branch] (primo_negozio) $ git commit -am 'Risolve il conflitto del merge specificando il nuovo totale'
[primo_negozio 34613da] Risolve il conflitto del merge specificando il nuovo totale
[esempio_git_branch] (primo_negozio) $ git status
On branch primo_negozio
nothing to commit, working tree clean
Abbiamo eseguito un 3-way merge. Il nuovo commit sarà diverso dai precedenti perché avrà due genitori anziché uno, come possiamo verificare con il comando git cat-file.
[esempio_git_branch] (primo_negozio) $ git cat-file -p 34613da
tree 5083f71eb285b757f6a80ff267d4518b5800b6d3
parent 9ace92192c27a87d7359fb17d2da39b8ba3a1ae5
parent 150adc119406efc00e7009f6d84be1cd93056e9d
author Claudio M. <claudio@example.com> 1513961494 +0000
committer Claudio M. <claudio@example.com> 1513961494 +0000
Risolve il conflitto del merge specificando il nuovo totale
Possiamo usare l’opzione –graph del comando git log per visualizzare la stessa rappresentazione nella shell.
[esempio_git_branch] (primo_negozio) $ git log --oneline --decorate --graph
* 34613da (HEAD -> primo_negozio) Risolve il conflitto del merge specificando il nuovo totale
|
| * 150adc1 (master) Aggiorna il prezzo della scheda madre
* | 9ace921 Cambia il modello di SSD con 850 EVO
|/
* ad0ebc7 Aggiorna il file configurazione_pc.txt - Hard disk in regalo
* 06ecc12 Aggiunge la configurazione iniziale al file configurazione_pc.txt
* 403012f first commit
A ogni commit corrisponde un asterisco. I commit sono elencati sulla destra, ma, se notate bene, al commit 9ace921 corrisponde un asterisco sul branch sinistro.
Il riferimento HEAD
Abbiamo visto finora che Git usa HEAD per riferirsi al branch corrente. Man mano che aggiungiamo dei nuovi commit, il branch corrente cambia valore e punta all’ultimo commit. Continuiamo con il nostro esempio e creiamo dei nuovi commit dopo aver effettuato il merge del branch primo_negozio nel branch master.
[esempio_git_branch] (primo_negozio) $ git status
On branch primo_negozio
nothing to commit, working tree clean
[esempio_git_branch] (primo_negozio) $ git checkout master
Switched to branch 'master'
[esempio_git_branch] (master) $ git merge primo_negozio
Updating 150adc1..34613da
Fast-forward
configurazione_pc.txt | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
[esempio_git_branch] (master) $ git log --oneline --decorate -1
34613da (HEAD -> master, primo_negozio) Risolve il conflitto del merge specificando il nuovo totale
Aggiungiamo allora una tastiera e un mouse alla configurazione e creiamo dei nuovi commit.
[esempio_git_branch] (master) $ git commit -am 'Aggiunge una tastiera meccanica alla configurazione'
[master d8c6d9b] Aggiunge una tastiera meccanica alla configurazione
1 file changed, 2 insertions(+)
[esempio_git_branch] (master) $ git commit -am 'Aggiunge un mouse alla configurazione'
[master e807e0b] Aggiunge un mouse alla configurazione
1 file changed, 1 insertion(+)
[esempio_git_branch] (master) $ git commit -am 'Aggiorna il prezzo totale della configurazione'
[master 1f36058] Aggiorna il prezzo totale della configurazione
1 file changed, 1 insertion(+), 1 deletion(-)
[esempio_git_branch] (master) $ git log --oneline --decorate --graph
* 1f36058 (HEAD -> master) Aggiorna il prezzo totale della configurazione
* e807e0b Aggiunge un mouse alla configurazione
* d8c6d9b Aggiunge una tastiera meccanica alla configurazione
* 34613da (primo_negozio) Risolve il conflitto del merge specificando il nuovo totale
|
| * 150adc1 Aggiorna il prezzo della scheda madre
* | 9ace921 Cambia il modello di SSD con 850 EVO
|/
* ad0ebc7 Aggiorna il file configurazione_pc.txt - Hard disk in regalo
* 06ecc12 Aggiunge la configurazione iniziale al file configurazione_pc.txt
* 403012f first commit
Alla fine, il file configurazione_pc.txt sarà come quello riportato sotto.
# Configurazione PC
AMD Ryzen 3 1200 - 110€
Asrock A320M motherboard - 49€
Ballistix Sport LT 8 GB Kit DDR4 - 98€
Samsung 850EVO 250GB - 72€
Sapphire Radeon RX 560 2GB GDDR5 - 100€
Corsair VS450 - 39€
DeepCool Tesseract Black - 49€
=======================================
Tastiera meccanica 87 tasti - switch brown - 39€
Mouse 8 pulsanti programmabili - 18€
=======================================
Totale 574€
Hard disk 3.5 1TB in regalo
Lo stato detached HEAD
Supponiamo ora di aver chiesto il preventivo a un altro negozio prima di aver pensato di aggiungere il mouse e la tastiera. Quando riceviamo una risposta, vogliamo creare un nuovo branch per il secondo_negozio. Siccome la configurazione che ci è stata inviata, è simile a quella che abbiamo nel commit a cui punta primo_negozio, vogliamo partire da quella versione del file. Potremmo quindi eseguire il comando git checkout che copia il file configurazione_pc.txt del branch primo_negozio nella working directory (git checkout primo_negozio configurazione_pc.txt) senza cambiare il branch corrente. Oppure potremmo spostarci sul branch primo_negozio (git checkout primo_negozio). In tal caso cambia il branch corrente e vengono copiati i file dal repository alla Staging Area e Working Directory. Decidiamo però di illustrare un’altra opzione ed eseguire il comando git checkout passando come argomento HEAD~3.
I caratteri ^ e ~
Prima di continuare con il resto della lezione, vediamo di capire brevemente cosa significa una sintassi come master~2 o HEAD^1. Entrambi servono per riferirsi a commit precedenti.
- Appendendo il suffisso ~<n> (tilde) indichiamo che vogliamo far riferimento a un commit che è n passi indietro seguendo il primo genitore (nel caso ci siano più genitori). Se n è uguale a 1, possiamo ometterlo. (~ è equivalente a ~1). Per esempio master~2 fa riferimento al commit che è due passi indietro rispetto al branch master. Con HEAD~ indichiamo il commit che precede HEAD.
- Appendendo il suffisso ^<n> (caret) indichiamo l’ennesimo genitore di un commit, se un commit ha più genitori.
Come mostrato nell’immagine sottostante, i due caratteri possono essere usati contemporaneamente.
Tornando al nostro esempio, eseguiamo allora il seguente comando.
[esempio_git_branch] (master) $ git checkout HEAD~3
Note: checking out 'HEAD~3'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at 34613da... Risolve il conflitto del merge specificando il nuovo totale
Git mostra un messaggio per metterci al corrente che siamo nello stato ‘detached HEAD’. Ogni volta che si effettua il comando git checkout passando come argomento un commit, HEAD entra in questo particolare stato. Una prima e evidente conseguenza è rappresentata dal contenuto del file .git/HEAD che solitamente contiene un riferimento a un branch. Se proviamo a visualizzare ora il suo contenuto, visualizzeremo al suo interno il valore identificativo del commit a cui punta ora HEAD.
[esempio_git_branch] ((34613da...)) $ cat .git/HEAD
34613da784ba78e288e596d90cbf77977ae32933
Quando HEAD si trova nello stato detached, possiamo effettuare modifiche e creare nuovi commit, HEAD si sposterà e punterà all’ultimo commit creato. Però, se non creiamo un nuovo branch che punti all’ultimo di questa serie di commit, quando eseguiamo il comando git checkout e spostiamo nuovamente HEAD, i commit non più referenziati verranno eliminati dal garbage collector.
Vediamo un esempio per capire meglio cosa succede. A partire dal commit corrente (HEAD punta al commit 34613da), modifichiamo configurazione_pc.txt con i componenti e i prezzi ricevuti da un ipotetico secondo negozio.
# Configurazione PC
AMD Ryzen 3 1300x - 129€
MSI B350M - 76€
Ballistix Sport LT 8 GB Kit DDR4 - 89€
Samsung 850EVO 250GB - 72€
Zotac ZT-P10510A-10L 4GB GDDR5 - 166€
Corsair VS450 - 39€
DeepCool Tesseract Black - 49€
=======================================
Totale 620€
Hard disk 3.5 1TB in regalo
[esempio_git_branch] ((34613da...)) $ git status
HEAD detached at 34613da
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: configurazione_pc.txt
no changes added to commit (use "git add" and/or "git commit -a")
[esempio_git_branch] ((34613da...)) $ git commit -am 'Aggiorna configurazione_pc.txt con i componenti del preventivo del secondo_negozio'
[detached HEAD 807748e] Aggiorna configurazione_pc.txt con i componenti del preventivo del secondo_negozio
1 file changed, 5 insertions(+), 5 deletions(-)
Se a questo punto spostassimo HEAD, il commit 807748e resterebbe isolato e non essendo referenziato da nessun branch, entrerebbe a far parte dei commit candidati a essere eliminati dal garbage collector. Se siamo interessati a salvare il commit, possiamo invece creare un nuovo branch con il comando che ci aveva suggerito precedentemente Git.
[esempio_git_branch] ((807748e...)) $ git checkout -b secondo_negozio
Switched to a new branch 'secondo_negozio'
[esempio_git_branch] (secondo_negozio) $ git status
On branch secondo_negozio
nothing to commit, working tree clean
Il comando git stash
Abbiamo appena creato un nuovo branch. Supponiamo di chiedere uno sconto al secondo ipotetico negozio e aggiorniamo quindi i prezzi nel file contenente il preventivo.
# Configurazione PC
AMD Ryzen 3 1300x - 129€
MSI B350M - 74€
Ballistix Sport LT 8 GB Kit DDR4 - 89€
Samsung 850EVO 250GB - 72€
Zotac ZT-P10510A-10L 4GB GDDR5 - 160€
Corsair VS450 - 39€
DeepCool Tesseract Black - 49€
=======================================
Totale 612€
Hard disk 3.5 1TB in regalo
Col nuovo preventivo risparmiamo 8€, aggiungiamo il file così modificato alla Staging Area, ma non effettuiamo il commit perché vogliamo chiedere un ulteriore sconto.
[esempio_git_branch] (secondo_negozio) $ git add .
Immaginiamo di riuscire a risparmiare altri 4 euro sul processore e di aggiornare nuovamente il file.
# comando lanciato dopo aver modificato il file configurazione_pc.txt
[esempio_git_branch] (secondo_negozio) $ git status
On branch secondo_negozio
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: configurazione_pc.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: configurazione_pc.txt
Ora abbiamo delle modifiche aggiunte nella Staging Area e delle modifiche nella working directory. Immaginiamo di dover cambiare branch, ma non voler creare ancora un nuovo commit.
[esempio_git_branch] (secondo_negozio) $ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
configurazione_pc.txt
Please commit your changes or stash them before you switch branches.
Aborting
Come risulta dall’output del comando eseguito, non possiamo cambiare branch perché ci sono delle modifiche in sospeso nella working directory e index. Git consiglia di creare un nuovo commit oppure inserire le modifiche all’interno di un’area temporanea denominata stash. Riportiamo quindi la versione aggiornata di un immagine usata in una delle precedenti lezioni in cui avevamo parlato delle tre aree di lavoro (working directory, staging area e repository) e aggiungiamo una quarta area detta stash.
All’interno di quest’area possiamo inserire temporaneamente dei cambiamenti che vogliamo magari ripristinare o riutilizzare successivamente. Il comando da eseguire è git stash che prende le modifiche presenti nella working directory e nella staging area e le conserva all’interno di un’area provvisoria (con l’opzione -u possiamo chiedere a git stash di salvare anche i file Untracked) da cui potremo successivamente estrarle per riapplicarle con il comando git stash pop (le modifiche vengono rimosse dalla pila) oppure git stash apply (se vogliamo che le modifiche vengano mantenute comunque all’interno della pila).
[esempio_git_branch] (secondo_negozio) $ git stash
Saved working directory and index state WIP on secondo_negozio: 807748e Aggiorna configurazione_pc.txt con i componenti del preventivo del secondo_negozio
[esempio_git_branch] (secondo_negozio) $ git status
On branch secondo_negozio
nothing to commit, working tree clean
Visualizziamo il contenuto dell’area di accantonamento con il seguente comando.
[esempio_git_branch] (secondo_negozio) $ git stash list
stash@{0}: WIP on secondo_negozio: 807748e Aggiorna configurazione_pc.txt con i componenti del preventivo del secondo_negozio
A questo punto potremmo cambiare tranquillamente branch e continuare ad aggiornare il file configurazione_pc.txt. Se vogliamo riapplicare le modifiche al branch secondo_negozio, basterà tornare sul branch ed eseguire git stash apply o git stash pop.
[esempio_git_branch] (secondo_negozio) $ git stash apply
On branch secondo_negozio
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: configurazione_pc.txt
no changes added to commit (use "git add" and/or "git commit -a")
Notate che le modifiche inserite nella Staging Area non sono state ripristinate. Per rimediare avremmo dovuto usare l’opzione –index.
# scartiamo le modifiche applicate dalla precedente esecuzione di git stash apply
[esempio_git_branch] (secondo_negozio) $ git checkout -- configurazione_pc.txt
# possiamo indicare quale elemento della stash prelevare stash@{<revision>}
# in questo caso indichiamo stash@{0} anche se non necessario
[esempio_git_branch] (secondo_negozio) $ git stash apply --index stash@{0}
On branch secondo_negozio
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: configurazione_pc.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: configurazione_pc.txt
Come possiamo notare è stato ripristinato lo stato del branch, possiamo quindi decidere di scartare le modifiche o creare un nuovo commit.
Conclusioni
In questa lezione abbiamo illustrato numerosi concetti riguardo ai branch. Abbiamo visto come crearne uno, come spostarci da un branch all’altro e come eseguire un merge. Abbiamo parlato del concetto di detached HEAD e spiegato come poter usare il comando git stash. Nella prossima lezione parleremo di un altro argomento importante in git, vedremo infatti come usare il comando git rebase.