1. Corso Introduttivo a Git
Federico Spinelli
fspinelli@gmail.com
github.com/Tabjones
2. Cos’è Git?
● Distributed Version Control System:
○ Tiene traccia delle modifiche a un gruppo di file nel corso del tempo.
○ Modifiche fatte da voi o da altri collaboratori.
○ Qualsiasi modifica registrata è ripristinabile in qualunque momento.
● Ogni Git repository è independente e completo.
● Non c’è un server centralizzato.
● È veloce.
● Git non memorizza i cambiamenti ai file, ma salva sempre tutti i file nella
loro interezza.
● All’occorrenza ricostruisce al volo le differenze.
3. Perchè usare Git?
● Permette di gestire “versioni” di un progetto in modo semplice e veloce.
● Permette di vedere le modifiche tra “versioni” e all’occorrenza tornare a
un punto precedente del tempo. Ovvero mantiene i “backup” di un
progetto.
● Gestisce in modo elegante modifiche apportate da persone diverse.
● È usato in tutto il mondo.
● Git è stato creato da Linus Torvald, il creatore di Linux.
5. ● Configurare Git affinchè ti riconosca
git config --global user.name "Federico Spinelli"
git config --global user.email fspinelli@gmail.com
● Creare un repository Git
mkdir ImparareGit
cd ImparareGit
mkdir libs
touch libs/foo.txt
mkdir templates
touch templates/bar.txt
git init
Comandi Base
6. ● Git mantiene un database chiave/valore di tutto il vostro progetto.
● Lo fa solo per i file che gli dite voi !
● Aggiungiamo il primo file a Git
git add libs/foo.txt
Con questo comando, git ispeziona il contenuto del file (è vuoto!) e lo memorizza nel suo database
chiave/valore, chiamato Object Database e conservato su file system nella directory nascosta .git.
Aggiungere un file a Git
7. ● Il file appena aggiunto formerà una voce nel database
Per il valore git userà il contenuto stesso del file; per la chiave, calcolerà lo SHA1
(Secure Hash Algorithm 1) del contenuto.
Per curiosità nel caso di un file vuoto, questa vale
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391, che in assenza di ambiguità
può essere abbreviata!
● Aggiungiamo anche il secondo file
git add templates/bar.txt
Ora, dato che i due file hanno lo stesso identico contenuto (sono entrambi
vuoti!), nel Database entrambi verranno conservati in un unico oggetto.
Git non ha bisogno di creare una nuova voce per accedere al contenuto.
Il Database
● I File memorizzati nel Database sono chiamati blob
8. ● Git ha memorizzato nel Database solo il contenuto dei file.
● Non il loro nome ne la loro posizione (apparentemente).
Naturalmente, però, a noi il nome dei file e la loro posizione interessano eccome. Per questo, Git
memorizza nel Database anche altri oggetti, chiamati tree che servono proprio a memorizzare il
contenuto delle varie directory e i nomi dei file.
Il Database 2
Nel nostro esempio avremo 3 tree.
Come ogni altro oggetto, anche i tree sono memorizzati
come oggetti chiave/valore e sono come delle freccie
che puntano ad altri tree oppure a blob.
9. ● Tutte le strutture del database sono raccolte in un contenitore, chiamato
Commit
Git Commit
Naturalmente, come tutti gli altri oggetti del Database
anche i Commit sono memorizzati in una voce
chiave/valore.
● lI Commit può essere visto come una
fotografia dello stato attuale del progetto
10. ● Eseguiamo il primo Commit
git commit -m "Commit A. Ecco il mio primo commit"
Con questo comando stiamo dicendo a Git di memorizzare nel repository, cioè nella storia del progetto,
il Commit, preparato in precedenza a colpi di add
Git Commit 2
Lo stato del repository adesso
contiene il primo commit. Ma cos’
è l’index ?
11. ● L’ index è una struttura che fa da cuscinetto tra il file system e il Git
repository
● il file system è la cartella che contiene i files sul computer.
● il repository è il database dove Git conserva i vari Commit.
● L’ index è uno spazio che Git ti mette a disposizione per creare il tuo
prossimo commit prima di registrarlo definitivamente nel repository.
Il comando “git add” aggiunge un file all’index, mentre il comando “git commit” salva lo stato attuale
dell’index nel repository
Index o Staging Area
12. ● Fisicamente l’ index non è molto diverso dal
repository: entrambi conservano nel
database le strutture tree e blob. Di fatto
l’index è un oggetto Commit come gli altri.
● Proviamo a fare delle modifiche a un file
echo “nel mezzo del cammin” >> libs/foo.txt
● E aggiorniamo l’ index
git add libs/foo.txt
Il contenuto di libs/foo.txt non è mai stato registrato, quindi Git
aggiunge all’Object Database un nuovo blob col nuovo
contenuto del file; contestualmente, aggiorna il tree libs perché il
puntatore chiamato foo.txt indirizzi il nuovo blob.
Index o Staging Area 2
● L’ index mantiene sempre una copia dell’ultimo commit, così che tu
possa continuare a lavorare senza interruzioni.
13. echo “happy happy joy joy” > doh.html
git add doh.html
Git aggiunge un nuovo blob object col contenuto del
file e, contestualmente, aggiunge nel tree “/” un
nuovo puntatore chiamato doh.html che punta al
nuovo blob.
● Le nuove modifiche rimangono
parcheggiate nell’ index in attesa
che voi le spediate nel repository
con il prossimo “git commit”
● L’ index non è altro che un oggetto
Commit temporaneo che è sempre
la fotografia dello stato attuale del
progetto.
Index o Staging Area 3
● Aggiungiamo anche un nuovo file nella root del progetto
14. git commit -m “Commit B. Il mio secondo commit”
L’operazione di commit può essere vista anche come:
“Ok, prendi l’attuale index e fallo diventare un nuovo commit nel repository.
Poi restituiscimi l’index così che io possa fare altre modifiche“
● Git salva anche un puntatore al commit di
provenienza, perchè ci interessa salvare anche la
storia del nostro file system
● Il commit index è sempre presente, ma alcune
interfaccie grafiche non lo mostrano a meno che
non ci siano delle modifiche non ancora registrate
nel repository, guarda:
gitk
Il mio secondo commit
● Salviamo l’attuale index nel repository con un nuovo commit
15. ● Dato che tutto è indirizzabile da una chiave posso dire a Git, che voglio far
tornare il file system com’era al tempo del “Commit A”
git checkout <<<Chiave del Commit A>>>
● Già, ma qual’è la chiave del “Commit A” ?
git log --oneline
Mostra un elenco di tutti i commit memorizzati nel repository
Tornare indietro nel tempo
● Supponiamo che la chiave
SHA1 del “Commit A” sia
56674fb, allora
git checkout 56674fb
In definitiva il checkout riporta il file system
allo stato in cui era al momento del commit
16. ● Che succede se dallo stato attuale del “Commit A” facessi altre modifiche
e creassi un nuovo Commit ?
echo “ei fu siccome immobile” > README.md
git add README.md
git commit -m “Ecco il commit C”
● Ho creato una situazione in cui C è figlio di A e non di B, ho creato una
diramazione. (Da non confondere con i branch!)
● Posso sempre muovermi tra Commit usando checkout e la chiave SHA1
del Commit corrispondente. Ma non è un po’ scomodo dover guardare
quelle chiavi tutte le volte ?
Divergere
17. ● Git ti permette di assegnare un “etichetta” alle chiavi dei Commit
git branch bob 56674fb ← questa è la chiave del Commit A
● Ora bob è una variabile che contiene il “Commit A”, posso spostarmi lì
usando bob al posto della chiave SHA1
git checkout bob
● Queste variabili si chiamano branch
● Probabilmente hai notato che Git ha già creato un branch per te, il
“master” che in questo momento punta al “Commit B”, andiamo li e
creiamo un altro branch
git checkout master
git branch dev ← Se non specifichi una chiave, Git usa quella del commit attuale (Commit B)
I Branch
18. ● Adesso cancelliamo bob, non ci serve più
git branch -d bob ← -d stà per delete, cancella
● Hai notato quel triangolino verde che ti segue ovunque ti sposti ? Quello è
un branch particolare chiamato HEAD, che punta sempre all’elemento del
repository nel quale ti trovi. Se ti sposti su dev, anche HEAD ti segue
git checkout dev
I Branch 2
19. ● Quando esegui checkout a un branch ti “attacchi” a quel branch e
l’etichetta ti seguirà se aggiungi altri commit, prova:
touch style.css
git add style.css
git commit -m “adesso ho anche il css”
git checkout master
touch angular.js
git add angular.js
git commit -m “angular.js rocks”
← Come vedi, sia dev, sia master si sono
spostati seguendo i tuoi due nuovi
commit
● Ci sono anche altre scorciatoie per muoversi agilmente tra Commits, i
reference relativi.
I Branch 3
20. ● La reference ^ significa “il commit precedente a”
git checkout master^ ← il Commit precedente a master
git checkout HEAD^^ ← 2 Commits prima di HEAD
● La reference ~n significa “n commit precedenti a”
git checkout dev~2 ← 2 Commit precedenti a dev
git checkout HEAD~3 ← 3 Commits prima di HEAD
Le Reference
21. ● Supponiamo di voler farci dire da Git quali sono le differenze tra due
commit, per esempio che modifiche ho fatto da dev a master
git diff dev master
Con questo comando stiamo chiedendo a Git: “Qual’è l’elenco delle modifiche ai file da applicare a
dev affinchè il progetto diventi identico a quello fotografato in master ?”
● Puoi vedere anche le differenze di un file tra l’index e l’ultimo commit del
repository
Le Differenze
22. ● Git cherry-pick è uno dei comandi più folli e versatili di Git.
● Applica i cambiamenti introdotti da un commit in un altro punto del
repository. Vediamo un esempio, creiamo un nuovo branch e
aggiungiamoci un commit
git checkout dev
git checkout -b experiment ←Puoi creare un nuovo branch e farci checkout con un solo
comando
touch experiment
git add experiment
git commit -m “un commit con un esperimento”
Il Cherry-Pick
23. ● Adesso consideriamo l’ultima modifica appena fatta a partire da dev
git checkout master
git cherry-pick experiment
Cherry-pick “coglie” il commit che gli indichi e lo applica sul commit dove ti trovi.
Il Cherry-Pick 2
24. ● Un uso comune di cherry-pick è correggere un bug a metà branch
● Creiamo un esempio
git checkout -b feature
touch feature && git add feature && git commit -m “feature”
touch orrore && git add orrore && git commit -m ”orrore e raccapriccio”
touch altra_feature && git add altra_feature && git commit -m “altra feature”
● Oh no! Il secondo Commit è stato un errore.
Se solo si potesse riscrivere la storia!
git checkout master
git branch --force feature ← Sposta il branch anche se era già assegnato a un altro commit
git checkout feature
● Adesso riapplichiamo solo i Commit che
ci interessano. Saltando il bug!
git cherry-pick b5041f3
git cherry-pick 841fb88
Il Cherry-Pick: Un esempio di uso
25. ● Il rebase non è altro che una macro per eseguire una serie di cherry-pick
automaticamente, senza dover spostare a mano un commit alla volta
● Creiamo un esempio, modificando dev
git checkout dev
echo “a {color:red; }” >> style.css
git commit -am ”i link sono rossi”
● Vogliamo staccare tutto il ramo dev e riappiccicarlo sopra master!
Dal branch dev (abbiamo fatto checkout precedentemente)
git rebase master
Stai chiedendo a Git: “spostami il ramo corrente (dev) sulla nuova base (master)”.
Il che è del tutto equivalente a spostare uno per uno i commit con cherry-pick. Solo, più comodo.
Il Rebase
26. ● Il rebase è estremamente utile in numerose situazioni, una molto
comune, che ti capiterà sicuramente, la simuliamo qui.
● Stacchiamo un nuovo branch da dev e lavoriamoci sopra
git checkout -b sviluppo
touch file1 && git add file1 && git commit -m”avanzamento 1”
touch file2 && git add file2 && git commit -m”avanzamento 2”
touch file3 && git add file3 && git commit -m”avanzamento 3”
● Supponiamo che nel frattempo un vostro
collega abbia lavorato a dev
git checkout dev
touch dev1 && git add dev1 && git commit -m”developer 1”
touch dev2 && git add dev2 && git commit -m”developer 2”
● Inevitabilmente vi ritrovate un questa
situazione
I mille usi del Rebase
27. ● Supponiamo di voler rendere di nuovo lineare la storia, applicando i
Commit di sviluppo dopo quelli dei vostri colleghi in dev
● Con rebase la cosa è estremamente semplice
git checkout sviluppo
git rebase dev
Di nuovo stai chiedendo a Git: “riapplica tutto il
lavoro che ho fatto nel ramo corrente (sviluppo)
dopo quello che è stato fatto in dev”
● Tutto appare come se tu avessi iniziato a lavorare a sviluppo partendo
dall’ultima versione di dev !
I mille usi del Rebase 2
28. ● Via via che prendi mano con Git ti rendi conto che puoi modificare i tuoi
commit letteralmente come vuoi, per esempio puoi
○ Invertire l’ordine di una serie di Commit
○ Spezzare in due un singolo ramo
○ Scambiare Commit tra un ramo e un altro
○ Aggiungere un Commit nel mezzo di un ramo
○ Spezzare un Commit in più di uno
○ Fondere più Commit in uno solo
● Attenzione però, se riscrivete la storia qualcun’altro potrebbe rimanerci
male !!
In generale non usate rebase o cherry-pick per riscrivere la storia se questa è stata in qualche modo
condivisa con un altro.
Nell’esempio precedente il branch sviluppo è solo locale, ovvero esiste SOLO sul vostro computer,
quindi potete modificarlo come volete senza danneggiare nessuno!
I mille usi del Rebase 3
29. ● Il rebase non è l’unico comando che vi permette di integrare due (o più)
branch separati
● Git merge funziona esattamente come uno se lo aspetta, ovvero fonde
due o più commit insieme, vediamo un esempio
git checkout dev && git branch bugfix
git checkout bugfix
touch fix1 && git add fix1 && git commit -m ”bugfixing 1”
touch fix2 && git add fix2 && git commit -m ”bugfixing 2”
● Di nuovo abbiamo creato una situazione
divergente, da cui prima o poi vorremmo integrare i due rami
● Per esempio vogliamo aggiornare sviluppo in modo che contenga i
bugfixing introdotti da bugfix
Git Merge
30. ● Fondiamo insieme i due rami
git checkout sviluppo
git merge bugfix
Con git merge bugfix hai chiesto a Git: “Procurami un
Commit che contenga tutto quello che c’è nel mio
branch corrente (sviluppo) e aggiungigci tutti i commit introdotti dal ramo bugfix”
● Git cerca nel suo database se per caso esiste un Commit che contenga
entrambi i rami
● Non lo trova perchè ripercorrendo a ritroso sviluppo non si incontra mai
bugfix
● Quindi Git crea un nuovo Commit al volo contenente tutti i Commit che gli
hai chiesto e lo aggiunge a sviluppo
● Il nuovo Commit ha quindi due genitori
Git Merge 2
31. ● Da questa situazione, cosa succederebbe se mi spostassi su dev e
chiedessi a Git un merge col ramo
sviluppo ?
git checkout dev
git merge sviluppo
Per rispondere usiamo lo stesso ragionamento di
Prima, hai chiesto a Git: “Procurami un Commit che contenga tutto quello che c’è nel mio
branch corrente (dev) e aggiungigci tutti i commit introdotti dal ramo sviluppo”
● Di nuovo Git cerca nel suo database se per caso esiste un Commit che
contenga entrambi i rami
● E lo trova! L’ultimo commit di sviluppo, sicuramente contiene se stesso,
ma contiene anche dev, perchè ripercorrendo a ritroso si incontra dev.
Quindi non c’è dubbio che quel commit contenga già le modifiche
introdotte da dev
Fast forward merge
32. ● Allora Git non ha motivo di creare un nuovo Commit e si limiterà
a spostarci sopra il branch dev
● Il branch dev è stato semplicemente spinto in avanti per contenere anche
sviluppo. Questo tipo di merge si chiama fast-forward
● Questo tipo di merge ti capiterà spessissimo, basta ricordarsi che se i due
rami sono sulla stessa linea di sviluppo, avverrà un fast-forward quando si
chiede un merge
Fast forward merge 2
33. ● Si da il caso che un commit nato da un merge non sia limitato a solo due
genitori. In realtà può avere quanti genitori volete.
● Git merge infatti può fondere tutti i branch che volete in un colpo solo.
Creiamo 4 branch e fondiamoli insieme
git branch uno
git branch due
git branch tre
git branch quattro
git checkout uno
touch uno && git add uno && git commit -m”uno”
git checkout due
touch due && git add due && git commit -m”due”
git checkout tre
touch tre && git add tre && git commit -m”tre”
git checkout quattro
touch quattro && git add quattro && git commit -m ”e quattro”
Octopus merge
34. ● Ora che abbiamo 4 rami chiediamo a dev di mergiarli tutti in un colpo solo
git checkout dev
git merge uno due tre quattro
Hai chiesto a Git di procurarti un Commit che contenesse sia dev sia tutti e quattro gli altri rami, non
avendolo trovato Git ne ha creato uno per te
● Questo tipo di merge si chiama octopus-merge
Octopus merge 2
35. ● Fino ad ora abbiamo lavorato con un solo repository locale (sul nostro
computer), ma in realtà Git è anche un sistema peer-to-peer
● Il tuo repository può entrare a far parte di una rete di repository e
scambiare informazioni con altri repository
● Un qualsiasi repository connesso al tuo è un remote
Nota che un remote non deve necessariamente essere in rete, può anche essere in un’altra cartella del
tuo computer, per esempio in un disco che usi per i backup
I remote, andiamo in rete
36. ● Ho creato un repository comune per il corso su GitHub, aggiungetelo
come remote
git remote add origin https://github.com/ImparareGit/ImparareGit.git
Hai aggiunto un remote che si chiama origin presente a quell’indirizzo, d’ora in poi per riferirti a quel
repository puoi semplicemente chiamarlo “origin”
● Puoi aggiungere quanti remote vuoi, basta che abbiano nomi diversi
● Origin adesso è connesso al tuo repository locale, con i comandi push e
fetch puoi spedire o ricevere un set di Commits
I remote
37. ● Dopo tutti gli esercizi svolti, il nostro
repository locale è in questo stato
Il branch colorato di arancione è la posizione corrente
di HEAD
● Vediamo se origin contiene lo stato
attuale di tutto il nostro repository
locale
I remote 2
38. ● Usiamo git fetch per ricevere i rami dal
repository remoto
git fetch origin
Oppure semplicemente “git fetch”, se si omette il nome del
remote, Git userà quello di default, dato che abbiamo solo
un remote, il nostro default è origin
● Dopo fetch il nostro repository non è
cambiato di una virgola, sono solo
comparse due strane etichette azzurre,
origin/feature e origin/master
● Vediamo cosa sono...
Git fetch
39. ● origin/feature e origin/master sono due
branch remoti
● I branch remoti non possono essere
modificati da locale, se provate a
cancellarne uno, Git vi darà un errore
git branch -d origin/feature
Error: branch ‘origin/feature’ not found.
● Git dice che quel branch non esiste,
infatti quel branch è sul repository
remoto origin e non sul vostro
● I branch remoti sono una sorta di
reminder che vi mostrano lo stato dei
branch sul repository remoto
Branch remoti
40. ● Quando abbiamo lanciato git fetch
abbiamo “scaricato” tutti i branch
presenti su origin.
● Dato che solo origin/feature e
origin/master sono comparsi significa
che il repository remoto non ha altri
branch
● Aggiorniamo origin con un altro ramo,
per esempio experiment
git push origin experiment
● Adesso origin ha anche experiment!
Git push
41. ● Vediamo più in dettaglio il comando push, ha la seguente sintassi
git push <remote> <branch> ← <remote> è uno dei remote collegati, <branch> è il luogo sul
remote
dove aggiungere il branch, e contemporaneamente anche il branch
locale da inviare
git push origin experiment ← Significa, prendi il branch locale experiment e invialo a origin.
Questo dovrà salvarlo nel proprio branch experiment. Se non esiste
Git lo crea.
● Sia <remote>, sia <branch> possono essere omessi, in quel caso il remote
Sarà quello di default e il branch sarà l’attuale posizione di HEAD
● In realtà l’argomento <branch> può essere ulteriormente spezzettato in
<source>:<destination>, dandoci la possibilità di specificare il “source”
branch in locale e il “destination” branch sul remote. Vediamo un esempio
Git push 2
42. git push origin experiment:nuovo_esperimento
Questo comando dice a Git di prendere il branch locale experiment e di spedirlo al remote origin che
dovrà chiamarlo nuovo_esperimento.
← Solo il branch experiment è
visibile
per brevità
● In realtà questo, viene raramente fatto, poichè crea confusione. Però è
utile sapere che se il <source> è vuoto, Git cancellerà il branch remoto
git push origin :nuovo_esperimento
nuovo_esperimento sarà cancellato, perchè gli è stato detto che deve puntare al nulla, quindi Git lo
rimuove
Git push 3
43. ● Vediamo adesso l’ultimo comando di questo corso, il pull.
In gran segreto ho aggiornato il branch origin/experiment con un nuovo
commit. Se guardate il vostro repository scoprirete che non è cambiato
nulla di una virgola.
● origin/experiment punta sempre al commit vecchio, origin è cambiato ma
il vostro repository locale ancora non lo sa!
Questo è perfettamente coerente con la natura non lineare di Git. I due repository locale e remoto sono
connessi ma si aggiornano SOLO al vostro comando.
● Chiediamo al nostro repository di scaricare lo stato di origin
git fetch origin
● Scoprendo che origin ha un nuovo commit che noi non abbiamo
← Se ci fate caso il nostro file system non è cambiato, il nostro
branch locale experiment punta sempre al commit vecchio.
Questo perchè Git vi da la possibilità di visionare i nuovi commit
remoti prima di integrarli con merge
Git pull
44. ● Volendo possiamo integrare i cambiamenti remoti con
git checkout experiment
git merge origin/experiment ← Questo merge sarà un fast-forward
● Ma Git ci mette a disposizione un comando per fare entrambe le cose
contemporaneamente, git pull
git pull origin experiment
● git pull non è altro che una scorciatoia per fare git fetch seguito da git
merge
Il comando appena lanciato significa: “Scarica tutti i commit da origin/experiment e integrali con merge
nel mio branch locale experiment”
Git pull 2
45. ● Adesso che abbiamo più chiari i comandi più importanti di Git possiamo
aggiornare il diagramma delle interazioni tra comandi
Interazione tra i comandi
46. ● Visto che abbiamo il nostro repository pubblico a cui tutti abbiamo
accesso, usiamolo come repository centralizzato.
Un esempio pratico
● Ognuno di noi crei un
commit su experiment
git checkout experiment
Create un commit (o anche piu di uno se
volete), aggiungendo/modificando files e
mettendo il messaggio che preferite
● Adesso provate a pushare
su origin
git push origin experiment
Solo il primo di voi ce la farà (il più veloce),
gli altri riceveranno un simpatico
messaggio di errore in cui Git si lamenta
che non può integrare i cambiamenti sul
remote
47. ● Cosa è successo ?
Il push del vostro collega ha fatto avanzare origin/experiment di un
commit, ma il vostro nuovo commit è figlio del vecchio experiment (locale)
che punta sempre a un commit oramai vecchio.
● Il vostro branch locale e quello remoto sono in divergenza e Git non sa
come integrare il nuovo commit, perchè per default il push accetta solo
fast-forward, Git vi suggerisce anche di eseguire un fetch prima di fare
qualunque cosa, facciamolo
git fetch
● Ora avete sostanzialmente due possibilità per integrare il vostro commit
con quello del collega
○ Merge: creare un nuovo commit che sia la fusione del vostro e di quello del collega
○ Rebase: riallineare il tuo commit dopo quello del collega
● Scegliete la soluzione che piu vi piace dopo di che fate push su origin
git push origin experiment
Un esempio pratico 2
48. ● L’help di Git:
○ Lista di tutti i comandi disponibili: git help -a
○ Help su un comando specifico: git help merge
● Learn Git Branching:
http://learngitbranching.js.org/
Una guida interattiva, composta da una serie di esercizi a difficoltà
crescente, molto divertente e ne vale la pena!
● ProGit:
https://www.git-scm.com/book/en/v2
Un libro bellissimo, pratico e considerato uno dei migliori testi su Git mai
scritti, leggetelo non ve ne pentirete.
Risorse per approfondire