48
Corso Introduttivo a Git Federico Spinelli [email protected] github.com/Tabjones

GitSlides

Embed Size (px)

Citation preview

Corso Introduttivo a GitFederico [email protected]

github.com/Tabjones

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.

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.

Istallare Git

● Windows: ○ https://git-for-windows.github.io/

● Mac:○ https://code.google.com/archive/p/git-osx-installer/downloads

● Linux:○ sudo apt-get install git

● Configurare Git affinchè ti riconosca

git config --global user.name "Federico Spinelli"git config --global user.email [email protected]

● Creare un repository Git

mkdir ImparareGitcd ImparareGitmkdir libstouch libs/foo.txtmkdir templatestouch templates/bar.txt

git init

Comandi Base

● 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

● 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

● 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.

● 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

● 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 ?

● 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

● 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.

echo “happy happy joy joy” > doh.htmlgit add doh.html

Git aggiunge un nuovo blob object col contenuto delfile 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

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

● 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 systemallo stato in cui era al momento del commit

● Che succede se dallo stato attuale del “Commit A” facessi altre modifiche e creassi un nuovo Commit ?

echo “ei fu siccome immobile” > README.mdgit add README.mdgit 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

● 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

● 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

● Quando esegui checkout a un branch ti “attacchi” a quel branch e l’etichetta ti seguirà se aggiungi altri commit, prova:

touch style.cssgit add style.cssgit commit -m “adesso ho anche il css”git checkout mastertouch angular.jsgit add angular.jsgit 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

● La reference ^ significa “il commit precedente a”

git checkout master^ ← il Commit precedente a mastergit checkout HEAD^^ ← 2 Commits prima di HEAD

● La reference ~n significa “n commit precedenti a”

git checkout dev~2 ← 2 Commit precedenti a devgit checkout HEAD~3 ← 3 Commits prima di HEAD

Le Reference

● 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

● 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 experimentgit add experimentgit commit -m “un commit con un esperimento”

Il Cherry-Pick

● Adesso consideriamo l’ultima modifica appena fatta a partire da dev

git checkout mastergit cherry-pick experiment

Cherry-pick “coglie” il commit che gli indichi e lo applica sul commit dove ti trovi.

Il Cherry-Pick 2

● Un uso comune di cherry-pick è correggere un bug a metà branch● Creiamo un esempio

git checkout -b featuretouch 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 commitgit checkout feature

● Adesso riapplichiamo solo i Commit checi interessano. Saltando il bug!

git cherry-pick b5041f3git cherry-pick 841fb88

Il Cherry-Pick: Un esempio di uso

● 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 devecho “a {color:red; }” >> style.cssgit 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

● 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 sviluppotouch 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 vostrocollega abbia lavorato a dev

git checkout devtouch 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

● 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 sviluppogit rebase dev

Di nuovo stai chiedendo a Git: “riapplica tutto illavoro 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

● 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

● 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 bugfixgit checkout bugfixtouch 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

● Fondiamo insieme i due rami

git checkout sviluppogit merge bugfix

Con git merge bugfix hai chiesto a Git: “Procurami unCommit 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

● Da questa situazione, cosa succederebbe se mi spostassi su dev e chiedessi a Git un merge col ramosviluppo ?

git checkout devgit 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

● 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

● 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 duegit branch tregit branch quattro

git checkout unotouch uno && git add uno && git commit -m”uno”

git checkout duetouch due && git add due && git commit -m”due”

git checkout tretouch tre && git add tre && git commit -m”tre”

git checkout quattrotouch quattro && git add quattro && git commit -m ”e quattro”

Octopus merge

● Ora che abbiamo 4 rami chiediamo a dev di mergiarli tutti in un colpo solo

git checkout devgit 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

● 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

● 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

● Dopo tutti gli esercizi svolti, il nostrorepository locale è in questo stato

Il branch colorato di arancione è la posizione correntedi HEAD

● Vediamo se origin contiene lo statoattuale di tutto il nostro repositorylocale

I remote 2

● Usiamo git fetch per ricevere i rami dalrepository remoto

git fetch origin

Oppure semplicemente “git fetch”, se si omette il nome del remote, Git userà quello di default, dato che abbiamo soloun 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

● origin/feature e origin/master sono duebranch remoti

● I branch remoti non possono essere modificati da locale, se provate a cancellarne uno, Git vi darà un errore

git branch -d origin/featureError: 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 deibranch sul repository remoto

Branch remoti

● Quando abbiamo lanciato git fetchabbiamo “scaricato” tutti i branch

presenti su origin.

● Dato che solo origin/feature e origin/master sono comparsi significache 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

● 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 remoteSarà 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

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

● 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 sonoconnessi 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

● 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

● Adesso che abbiamo più chiari i comandi più importanti di Git possiamo aggiornare il diagramma delle interazioni tra comandi

Interazione tra i comandi

● 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

● 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 solofast-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

● 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