gianluca.aguzzi@unibo.it
angelo.filaseta@unibo.it
Questo materiale è ampiamente basato su quello realizzato dal Prof. Danilo Pianini, che ringrazio.
Ogni errore riscontratovi è esclusiva responsabilità degli autori di questo documento.
git
git
:
config
, init
, add
, reset
, commit
, status
, checkout
, log
, diff
.gitignore
, gestione dei caratteri di “fine linea”branch
, merge
clone
, push
, fetch
, pull
Due motivazioni principali:
Hai mai avuto bisogno di ripristinare un progetto o un compito a una versione precedente?
Come hai tracciato la storia del progetto?
Inefficace!
Hai mai avuto bisogno di sviluppare un progetto o un compito in team?
Come hai organizzato il lavoro per massimizzare la produttività?
Sistemi di Controllo di Versione (VCS): strumenti pensati per supportare lo sviluppo di progetti tramite
Due tipi principali:
Git è ora il DVCS dominante (sebbene Mercurial sia ancora in uso, ad esempio, per Python, Java, Facebook).
A prima vista, la cronologia di un progetto sembra una linea.
Anything that can go wrong will go wrong
$1^{st}$ Murphy’s law
If anything simply cannot go wrong, it will anyway $5^{th}$Murphy’s law
Tornare indietro nel tempo a uno stato precedente in cui le cose funzionavano
Quindi correggere l’errore
Se si considerano i rollback, la cronologia è un albero!
Alice e Bob lavorano insieme per un po’ di tempo, poi tornano a casa e lavorano separatamente, in parallelo
Hanno una cronologia divergente!
Se si ha la possibilità di riconciliare gli sviluppi divergenti, la cronologia diventa un grafo!
Riconciliare gli sviluppi divergenti è solitamente chiamato merge (unione)
Repository: include l’intero contenuto/cronologia del progetto (e i metadati)
Di solito, memorizzato in una cartella nascosta nella cartella principale del progetto
(o worktree, o working directory)
l’insieme di file (solitamente, all’interno di una cartella radice) che costituiscono il progetto, escludendo i metadati.
Uno stato salvato del progetto.
Una sequenza nominata di commit
Se nessun branch è stato creato al primo commit, viene utilizzato un nome predefinito.
Per poter tornare indietro nel tempo o cambiare branch, è necessario fare riferimento ai commit
tree-ish
Aggiungere ~
e un numero i
a un tree-ish valido significa “i-esimo
genitore di questo tree-ish”
L’operazione di spostamento verso un altro commit (cioè a uno snapshot o versione del progetto)
Sposta HEAD
verso il tree-ish di destinazione specificato
Proviamo a vedere cosa succede quando sviluppiamo un progetto, passo dopo passo.
Oh, no, c’è stato un errore! Dobbiamo effettuare un rollback!
6
ogni volta che vogliamo.*Ok, ma c’erano elementi utili in 5
, vorrei averli in new-branch
Nota che:
8
è un commit di merge, in quanto ha due genitori: 7
e 5
Sistema di controllo delle versioni distribuito di fatto
¹ Meno differenza ora, Facebook ha migliorato notevolmente Mercurial
Git è uno strumento a interfaccia a riga di comando (CLI)
Sebbene esistano interfacce grafiche, non ha senso imparare una GUI:
Do per scontato una conoscenza minima dello shell, per favore fammelo sapere ORA se non l’hai mai visto
La configurazione in Git avviene a due livelli:
Imposta le opzioni globali in modo ragionevole, poi sovrascrivile a livello di repository, se necessario.
git config
Il sottocomando config
imposta le opzioni di configurazione
--global
, configura lo strumento globalmentegit config [--global] category.option value
option
di category
a value
Come detto, --global
può essere omesso per sovrascrivere le impostazioni globali localmente
user.name
e user.email
Un nome e un contatto vengono sempre salvati come metadati, quindi devono essere configurati
git config --global user.name "Il tuo vero nome"
git config --global user.email "il.tuo.indirizzo.email@il.tuo.provider"
Alcune operazioni aprono un editor di testo.
È conveniente impostarlo su uno strumento che sai usare
(per evitare, ad esempio, di essere “bloccato” all’interno di vi
o vim
).
Funziona qualsiasi editor che puoi invocare dal terminale.
git config --global core.editor nano
Come denominare il ramo predefinito.
Due scelte ragionevoli sono main
e master
git config --global init.defaultbranch master
git init
.git
.git
segna la radice del repository
cd
per posizionarti all’interno della cartella che contiene (o conterrà) il progetto
mkdir
)git init
.git
.Git ha il concetto di stage (o index).
git add <files>
sposta lo stato corrente dei file nello stage come modifichegit reset <files>
rimuove le modifiche attualmente in stage dei file dallo stagegit commit
crea un nuovo changeset con il contenuto dello stageÈ estremamente importante capire chiaramente qual è lo stato attuale delle cose
git status
stampa lo stato corrente del repository, esempio di output:
❯ git status
Sul ramo master
Il tuo ramo è aggiornato con 'origin/master'.
Modifiche da impegnare:
(usa "git restore --staged <file>..." per rimuovere dallo stage)
modificato: content/_index.md
nuovo file: content/dvcs-basics/_index.md
nuovo file: content/dvcs-basics/staging.png
Modifiche non in stage per l'impegno:
(usa "git add <file>..." per aggiornare ciò che verrà impegnato)
(usa "git restore <file>..." per scartare le modifiche nella working directory)
modificato: layouts/shortcodes/gravizo.html
modificato: layouts/shortcodes/today.html
git config --global user.name 'Il tuo vero nome'
git config --global user.email 'la.tua@email.com'
git config user.name 'Il tuo vero nome'
git config user.email 'la.tua@email.com'
-m
, altrimenti Git aprirà l’ editor predefinito
git commit -m 'il mio messaggio molto chiaro ed esplicativo'
Al primo commit, non esiste alcun ramo e nessun HEAD
.
A seconda della versione di Git, potrebbe verificarsi il seguente comportamento al primo commit:
master
master
, ma avverte che si tratta di un comportamento deprecato
main
considerato più inclusivogit config --global init.defaultbranch nome-ramo-predefinito
In generale, non vogliamo tracciare tutti i file nella cartella del repository:
Naturalmente, potremmo semplicemente non aggiungerli, ma l’errore è dietro l’angolo!
Sarebbe molto meglio dire a Git di ignorare alcuni file.
Ciò si ottiene tramite un file speciale .gitignore
.
.gitignore
, nomi come foo.gitignore
o gitignore.txt
non funzioneranno
echo cosaVogliamoIgnorare >> .gitignore
(comando multipiattaforma)git add
non venga chiamato con l’opzione --force
).gitignore
# ignora la cartella bin e tutto il suo contenuto
bin/
# ignora ogni file pdf
*.pdf
# eccezione alla regola (che inizia con !): i file pdf denominati 'myImportantFile.pdf' devono essere tracciati
!myImportantFile.pdf
Andare a capo è un’operazione in due fasi:
Nelle teletipografie elettromeccaniche (e anche nelle macchine da scrivere), erano due operazioni distinte:
I terminali sono stati progettati per comportarsi come telescriventi virtuali.
tty
.LF
era sufficiente nei TTY virtuali per andare a capo.
\n
significa “a capo”.otterremmo
righe
come queste
CR
seguito da un carattere LF
: \r\n
.LF
: \n
.CR
: \r
.
\n
.Se il tuo team utilizza sistemi operativi multipli, è probabile che, per impostazione predefinita, gli editor di testo utilizzino LF
(su Unix) o CRLF
.
È anche molto probabile che, al momento del salvataggio, l’intero file venga riscritto con i terminatori di riga “localmente corretti*”.
Git cerca di affrontare questo problema convertendo automaticamente i terminatori di riga in modo che corrispondano ai terminatori di riga iniziali del file.
LF
/CRLF
.I terminatori di riga dovrebbero invece essere configurati per tipo di file!
.gitattributes
LF
ovunque, tranne che per gli script di Windows (bat
, cmd
, ps1
)..gitattributes
nella root del repository.
* text=auto eol=lf
*.[cC][mM][dD] text eol=crlf
*.[bB][aA][tT] text eol=crlf
*.[pP][sS]1 text eol=crlf
git add
aggiunge una modifica allo staging area.git add someDeletedFile
è un comando corretto, che metterà in staging area il fatto che someDeletedFile
non esiste più e che la sua eliminazione deve essere registrata al prossimo commit
.
foo
in bar
:
git add foo bar
foo
è stato eliminato e bar
è stato creato.Naturalmente, è utile visualizzare la cronologia dei commit. Git fornisce un sottocomando dedicato:
git log
HEAD
(il commit corrente) all’indietro.
git log --oneline
git log --all
git log --graph
git log --oneline --all --graph
git log --oneline --all --graph
* d114802 (HEAD -> master, origin/master, origin/HEAD) moar contribution
| * edb658b (origin/renovate/gohugoio-hugo-0.94.x) ci(deps): update gohugoio/hugo action to v0.94.2
|/
* 4ce3431 ci(deps): update gohugoio/hugo action to v0.94.1
* 9efa88a ci(deps): update gohugoio/hugo action to v0.93.3
* bf32a8b begin with build slides
* b803a65 lesson 1 looks ready
* 6a85f8f ci(deps): update gohugoio/hugo action to v0.93.2
* b474d2a write more on the introductory lesson
* 8a7105e ci(deps): update gohugoio/hugo action to v0.93.1
* 6e40642 begin writing the first lesson
<tree-ish>
In Git, un riferimento a un commit è chiamato <tree-ish>
. I <tree-ish>
validi sono:
b82f7567961ba13b1794566dde97dda1e501cf88
.b82f7567
.HEAD
, un nome speciale che fa riferimento al commit corrente (la testa, appunto).È possibile creare riferimenti relativi, ad esempio “prendimi il commit precedente a questo <tree-ish>
”,
seguendo il commit <tree-ish>
con una tilde (~
) e con il numero di genitori a cui arrivare:
<tree-ish>~STEPS
dove STEPS
è un numero intero produce un riferimento al genitore STEPS-esimo
del <tree-ish>
fornito:
b82f7567~1
fa riferimento al genitore del commit b82f7567
.some_branch~2
fa riferimento al genitore del genitore dell’ultimo commit del branch some_branch
.HEAD~3
fa riferimento al genitore del genitore del genitore del commit corrente.Nel caso di commit di merge (con più genitori), ~
seleziona il primo.
La selezione dei genitori può essere eseguita con il caret nel caso di più genitori (^
).
git rev-parse
sulla specifica della revisione è pubblicamente disponibile.Vogliamo vedere quali differenze ha introdotto un commit, o cosa abbiamo modificato in alcuni file dell’area di lavoro.
Git fornisce supporto per visualizzare le modifiche in termini di righe modificate tramite git diff
:
git diff
mostra la differenza tra lo staging area e l’area di lavoro.
git add
.git diff --staged
mostra la differenza tra HEAD
e lo staging area.
git diff <tree-ish>
mostra la differenza tra <tree-ish>
e l’area di lavoro (staging area escluso).git diff --staged <tree-ish>
mostra la differenza tra <tree-ish>
e l’area di lavoro, incluse le modifiche in staging area.git diff <da> <a>
, dove <da>
e <a>
sono <tree-ish>
, mostra le differenze tra <da>
e <a>
.git diff --word-diff
è utile quando si lavora su testo.git diff
:diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml
index b492a8c..28302ff 100644
--- a/.github/workflows/build-and-deploy.yml
+++ b/.github/workflows/build-and-deploy.yml
@@ -28,7 +28,7 @@ jobs:
# Idea: the regex matcher of Renovate keeps this string up to date automatically
# The version is extracted and used to access the correct version of the scripts
USES=$(cat <<TRICK_RENOVATE
- - uses: gohugoio/hugo@v0.94.1
+ - uses: gohugoio/hugo@v0.93.3
TRICK_RENOVATE
)
echo "Scripts update line: \"$USES\""
diff
e patch
.La navigazione nella cronologia significa concretamente spostare la testa (in Git, HEAD
) su punti arbitrari della cronologia.
In Git, questo viene eseguito con il commit checkout
:
git checkout <tree-ish>
HEAD
sul <tree-ish>
fornito.<tree-ish>
fornito.Il comando può essere utilizzato per effettuare il checkout selettivo di un file da un’altra revisione:
git checkout <tree-ish> -- foo bar baz
foo
, bar
e baz
dal commit <tree-ish>
e li aggiunge allo staging area (a meno che non ci siano modifiche non commesse che potrebbero andare perse).--
è circondato da spazi bianchi, non è un’opzione --foo
, viene semplicemente utilizzato come separatore tra il <tree-ish>
e l’elenco dei file.
<tree-ish>
e abbiamo bisogno di disambiguazione.Git non consente più head per branch
(altri DVCS lo fanno, in particolare Mercurial):
affinché un commit sia valido, HEAD
deve essere alla “fine” di un branch (sul suo ultimo commit), come segue:
Quando viene effettuato il checkout di un commit vecchio, questa condizione non è più valida!
Se eseguiamo git checkout HEAD~4
:
Il sistema entra in una modalità di lavoro speciale chiamata detached head.
Quando si è in detached head, Git permette di effettuare commit, ma questi vanno persi!
git reflog
e git cherry-pick
, argomenti che non tratteremo)Per poter iniziare nuove linee di sviluppo, è necessario creare un branch.
In Git, i branch funzionano come etichette mobili:
HEAD
HEAD
è attaccato ad essi, si muovono insieme a HEAD
I branch vengono creati con git branch branch_name
⬇️ git branch new-experiment
⬇️
HEAD
non si associa al nuovo ramo per default: è necessario un esplicito comando checkout
.
La creazione di nuovi rami permette di salvare le modifiche apportate quando ci si trova nello stato DETACHED_HEAD.
⬇️ git checkout HEAD~4
⬇️
➡️ Next: git branch new-experiment
➡️
⬇️ git branch new-experiment
⬇️
HEAD
è ancora staccato, quindi dobbiamo attaccarlo al nuovo ramo affinché memorizzi i nostri commit.
➡️ Next: git checkout new-experiment
➡️
⬇️ git checkout new-experiment
⬇️
⬇️ [changes] + git add
+ git commit
⬇️
$\Rightarrow$ HEAD
fa avanzare il nostro ramo con sé!
Come potete immaginare, creare un nuovo branch e agganciare HEAD
al branch appena creato è un’operazione piuttosto comune.
Come consuetudine per le operazioni comuni, è disponibile un comando abbreviato: git checkout -b new-branch-name
new-branch-name
dalla posizione corrente di HEAD
HEAD
a new-branch-name
⬇️ git checkout -b new-experiment
⬇️
Riunificare linee di sviluppo divergenti è molto più complicato che creare nuove linee di sviluppo.
In altre parole, l’unione è molto più complicata della creazione di branch.
In Git, git merge target
unisce il branch chiamato target
nel branch corrente (HEAD
deve essere associato).
⬇️ git merge master
⬇️
Consideriamo questa situazione:
new-experiment
includa anche le modifiche da C7
a C10
(per essere aggiornato con master
)master
contiene tutti i commit di new-experiment
new-experiment
per puntarlo a C6
Git tenta di risolvere la maggior parte dei conflitti automaticamente.
In caso di conflitto su uno o più file, Git contrassegna i file in questione come in conflitto e li modifica aggiungendo dei marcatori di merge:
<<<<<<< HEAD
Modifiche apportate sul ramo in cui si sta fondendo,
questo è il ramo attualmente selezionato (HEAD).
=======
Modifiche apportate sul ramo che si sta fondendo.
>>>>>>> other-branch-name
git add
.git commit
.
git commit --no-edit
può essere usato per accettarlo senza modificarlo.Evitare i conflitti di merge è molto meglio che risolverli
Sebbene siano inevitabili in alcuni casi, possono essere minimizzati seguendo alcune buone pratiche:
git init
.Git fornisce un sottocomando clone
che copia tutta la cronologia di un repository localmente.
git clone URI destinazione
crea la cartella destinazione
e clona il repository trovato in URI
.
destinazione
non è vuota, fallisce.destinazione
è omessa, viene creata una cartella con lo stesso nome dell’ultimo segmento di URI
.URI
può essere remoto o locale, Git supporta i protocolli file://
, https://
e ssh
.
ssh
raccomandato quando disponibile.clone
effettua il checkout del ramo remoto a cui è attaccato HEAD
(ramo predefinito).Esempi:
git clone /some/repository/on/my/file/system destinazione
destinazione
e copia il repository dalla directory locale.git clone https://somewebsite.com/someRepository.git mia_cartella
mia_cartella
e copia il repository situato all’URL specificato.git clone user@sshserver.com:SomePath/SomeRepo.git
SomeRepo
e copia il repository situato all’URL specificato.init
, nessun remoto è conosciuto.clone
, viene creato automaticamente un remoto chiamato origin
.I rami non locali possono essere referenziati come nomeRemote/nomeBranch
.
Il sottocomando remote
viene utilizzato per ispezionare e gestire i remotes:
git remote -v
elenca i remotes conosciuti.
git remote add un-remoto URI
aggiunge un nuovo remoto chiamato un-remoto
e che punta a URI
.
git remote show un-remoto
mostra informazioni estese su un-remoto
.
git remote remove un-remoto
rimuove un-remoto
(non elimina le informazioni sul remoto, localmente dimentica che esiste).
I rami remoti possono essere associati a rami locali, con il significato che il ramo locale e quello remoto sono destinati ad essere due copie dello stesso ramo.
git branch --set-upstream-to=remoto/nomeRamo
.
git branch --set-upstream-to=origin/develop
imposta l’upstream del ramo corrente su origin/develop
.clone
, il suo ramo predefinito viene effettuato il checkout localmente con lo stesso nome che ha sul remoto, e il ramo remoto viene impostato automaticamente come upstream.git clone git@somesite.com/repo.git
git@somesite.com/repo.git
è salvato come origin
HEAD
, nel nostro caso master
) su origin
viene checkoutato localmente con lo stesso nomemaster
è configurato per tracciare origin/master
come upstreamgit branch
(o git checkout -b
) può fate checkout branch remoti localmente una volta che sono stati fetchati.
➡️ git checkout -b imported-feat origin/feat/serverless
➡️
⬇️ git checkout -b imported-feat origin/feat/serverless
⬇️
imported-feat
, e origin/feat/new-client
viene impostata come suo upstream.git checkout -b feat/new-client origin/feat/new-client
git checkout feat/new-client
feat/new-client
con la branch upstream impostata su origin/feat/new-client
se:
feat/new-client
Per verificare se un remote ha degli aggiornamenti disponibili, Git fornisce il sottocomando git fetch
.
git fetch a-remote
controlla se a-remote
ha nuove informazioni. Se sì, le scarica.
git fetch
senza specificare un remote:
HEAD
è attaccato e il branch corrente ha un upstream, allora viene effettuato il fetch del remote che ospita il branch upstream.origin
, se presente.merge
.Le nuove informazioni recuperate includono nuovi commit, branch e tag.
➡️ Successivo: Vengono apportate modifiche contemporaneamente su somesite.com/repo.git
e sul nostro repository ➡️
⬇️ Vengono apportate modifiche contemporaneamente su somesite.com/repo.git
e sul nostro repository ⬇️
➡️ git fetch && git merge origin/master
(assumendo nessun conflitto o conflitti risolti) ➡️
⬇️ git fetch && git merge origin/master
(assumendo nessun conflitto o conflitti risolti) ⬇️
Se non ci fossero stati aggiornamenti localmente, avremmo assistito ad un fast-forward.
git pull
Fa fetch del remoto con il ramo upstream e poi mergiarlo è estremamente comune, così comune che esiste un sottocomando speciale che esegue questa operazione.
git pull
è equivalente a git fetch && git merge FETCH_HEAD
git pull remote
è uguale a git fetch remote && git merge FETCH_HEAD
git pull remote branch
è uguale a git fetch remote && git merge remote/branch
git pull
è usato più comunemente di git fetch
+ git merge
,
nonostante ciò, è importante capire che non è un’operazione primitiva.
Git fornisce un modo per inviare le modifiche a un remoto: git push remote branch
remote/branch
, e aggiorna il remoto HEAD
push
richiede i diritti di scrittura sul repository remotopush
fallisce se il ramo pushato non è un discendente del ramo di destinazione, il che significa:
Per impostazione predefinita, git push
non invia i tag
git push --tags
invia solo i taggit push --follow-tags
invia i commit e poi i tag➡️ Successivo: [alcune modifiche] git add . && git commit
➡️
⬇️ [alcune modifiche] git add . && git commit
⬇️
➡️ Next: git push
➡️
⬇️ git push
⬇️
origin/master
era un sottoinsieme di master
HEAD
remoto può essere fast-forwarded➡️ Successivo: qualcun altro effettua un push di una modifica ➡️
⬇️ qualcun altro effettua un push di una modifica ⬇️
➡️ Successivo: [alcune modifiche] git add . && git commit
➡️
⬇️ [alcune modifiche] git add . && git commit
⬇️
➡️ Successivo: git push
➡️
⬇️ git push
⬇️
ERROR
To somesite.com/repo.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'somesite.com/repo.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
master
non è un superset di origin/master
10
è presente in origin/master
ma non in master
, impedendo un fast-forward remoto.➡️ Next: git pull
➡️
⬇️ git pull
(assumendo nessun conflitto di merge, o dopo la risoluzione del conflitto) ⬇️
master
è un superset di origin/master
! (tutti i commit in origin/master
, più 11
e 12
)➡️ Successivo: git push
➡️
⬇️ git push
⬇️
Il push ha avuto successo!
Diversi servizi permettono la creazione di repository condivisi sul cloud. Essi arricchiscono il modello base di Git con servizi integrati nello strumento:
I repository sono identificati in modo univoco da un owner e da un nome del repository
owner/repo
è un nome unico per ogni repositorysupporta due tipi di autenticazione:
repo
su https://github.com/settings/tokens/newhttps://github.com/owner/repo.git
diventa: https://token@github.com/owner/repo.git
Avviso: questo è un modo “veloce e sporco” per generare e utilizzare le chiavi SSH.
Si consiglia vivamente di imparare come funziona e le migliori pratiche di sicurezza.
ssh-keygen
cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3Nza<snip, un sacco di caratteri apparentemente casuali>PIl+qZfZ9+M= you@your_hostname
Il gioco è fatto! Buon divertimento con l’autenticazione sicura.