28 KiB
Git crash course
Davide Alberani da@erlug.linux.it 2017-2018
Non-corso per non prendere a martellate il monitor quando usate Git.
git clone https://git.lattuga.net/alberanid/git-crash-course.git
This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License: http://creativecommons.org/licenses/by-sa/4.0/
A chi è rivolto
A chi si trova a volere/dovere usare Git in un piccolo team, ed è ancora alle prime armi.
Struttura del corso
Parte 1
Le basi per lavorare in locale e con repository remoti.
Parte 2
Un workflow per lo sviluppo collaborativo, da applicare senza porsi troppe domande.
Parte 3
Una serie di strumenti avanzati, per chi ci ha preso gusto e vuole approfondire lo strumento.
Cosa verrà trattato
-
i comandi essenziali da riga di comando. Noti come porcelain ("ceramica"), sono comandi user-friendly di alto livello, contrapposti agli elementi plumbing ("tubature") di basso livello
-
come gestire i branch
-
le basi per lavorare con repository remoti
-
un workflow per collaborare con altri sviluppatori
Cosa NON verrà trattato
- gli internals (plumbing) di Git
- Github (sorry, è solo un servizio di hosting)
- le GUI
- amministrazione di un repository remoto
- flame wars (discussioni) sui tipi di workflow
Cosa è Git
Un sistema di controllo versione distribuito.
Serve per tener traccia dei cambiamenti al proprio codice e per facilitare lo sviluppo condiviso. Va ricordato che Git è nato soprattutto per aiutare chi deve integrare il codice altrui, e questo si riflette sulla sua logica.
Il resto [lo spiega meglio Wikipedia](https://it.wikipedia.org/wiki/Git%5F%28software%29).
Cosa NON è Git
- non è Subversion o CVS
- non è un sistema di backup
- non è un sistema per fare deploy (o magari sì, ma rifletteteci)
Si dice in giro
Git non avrà segreti per voi, una volta compreso...
- ...il data model (objects, blobs, trees, commits, refs, tags, ...)
- ...il fatto che tutto è locale
- ...che i commit sono in realtà snapshot, e non delta rispetto allo stato precedente
- ...una qualche astrusa teoria a caso
Onestamente?
Tutto vero, ma la sua user interface è un mezzo disastro.
Le basi: definizioni
-
Working directory: i file su cui state lavorando
-
Staging area (o Index): dove mettiamo da parte le modifiche che finiranno nel prossimo commit
-
Commit: snapshot dello stato in un certo momento
-
(fare) Checkout: aggiornare i file nella working directory ad un dato branch/commit/...
-
HEAD: il punto a cui sarà collegato il prossimo commit (di norma, il branch corrente)
-
refs: nome collettivo per riferirsi ad HEAD, branches, tags
Le basi: prepariamo l'ambiente con git config
$ git config --global user.name "Davide Alberani"
$ git config --global user.email da@erlug.linux.it
$ git config --global color.ui auto
Le configurazioni vengono registrate nei files (in ordine di lettura: i successivi sovrascrivono le impostazioni dei precedenti):
- /etc/git/config: opzioni globali, valide per tutti gli utenti
- ~/.gitconfig oppure ~/.config/git/config: opzioni valide per l'utente corrente
- .git/config nel repository corrente: opzioni locali valide solo per il repository corrente
Bonus track
- cercare un esempio di ~/.gitconfig avanzato, con qualche alias per i comandi principali, come questo o quest'altro
Le basi: alcune opzioni di configurazione
Alias comuni:
$ git config --global alias.st status
$ git config --global alias.br branch
$ git config --global alias.co checkout
Colori:
$ git config --global color.branch.current "yellow bold"
$ git config --global color.branch.local "green bold"
$ git config --global color.branch.remote "cyan bold"
$ git config --global color.status.added "green bold"
Utente di Github:
$ git config --global github.username [nome]
Parte 1
In cui forniamo le basi per lavorare in locale e con repository remoti.
Le basi: creare un repository
Creare un nuovo repository partendo da una directory (vuota o meno):
$ git init
Clonare un repository remoto esistente:
$ git clone https://git.lattuga.net/user/repo.git
creare un repository: cosa è successo?
È stata creata la directory .git (il repository); se abbiamo fatto un clone, è stato aggiunto il riferimento al remote "origin".
Bonus track
- i repository remoti, in cui non si lavora direttamente ma ricevono solo comandi push e pull, vengono di norma creati con --bare e non hanno working directory. Sono repo usati per condividere. Gli sviluppatori clonano il shared bare repo, fanno modifiche locali nelle loro working repo e fanno push nel shared bare repo per rendere le modifiche disponibili agli altri. Siccome nessuno edita direttamente nel shared bare repo, non serve avere un working tree. Anzi, questo potrebbe essere causa di conflitto.
Le basi: status
Vedere lo stato del sistema (usatelo spesso! un utile cheatsheet):
$ git status [-s]
Stati dei file
-
Untracked: nuovi file nella working directory, non ancora aggiunti
-
Unmodified: file che non sono cambiati dal commit precedente
-
Modified: modificati nella working area e non ancora aggiunti alla staging area
-
Staged: nella staging area, pronti per il prossimo commit
Le basi: add e commit
Modifichiamo un file ed aggiungiamolo alla staging area:
$ git add prova.txt
Committiamolo:
$ git commit [-m "messaggio di commit"]
Verifichiamo quanto accaduto:
$ git log
add e commit: cosa è successo?
Abbiamo aggiunto un file alla staging area, per poi salvare uno snapshot del nostro lavoro. Se - come normalmente accade - siamo in un branch, questo punta al nuovo commit (HEAD continua a puntare al branch, e di conseguenza anch'essa al nuovo commit).
Bonus track
- indovinate cosa fanno git rm e git mv
- committate spesso
- come scrivere un messaggio di commit che non susciti sgomento? Issue, titolo breve, descrizione estesa
- non salva directory vuote; se servono, aggiungete un file .gitkeep (è solo una convenzione)
- creare un file .gitignore per ignorare certi file
Cosa sono i commit
Sono uno snapshot dell'intero stato del sistema in un dato momento, identificati da un hash (e.g.: 6d7696a8b8
).
I commit hash sono generati partendo da: messaggio, committer, author, dates, tree, parent hash.
Bonus track
- è possibile abbreviare gli hash, purché rimangano univoci (e.g. 6d769)
- per dettagli, vedere anatomy of a Git commit e Git Internals
Le basi: la history
$ git log [--stat] [--patch] [--graph] [--decorate] [--color] [-2]
Rappresenta la storia dei commit dal punto corrente (o da/a qualsiasi punto indicato) fino al primo commit.
Si può limitare agli ultimi N commit con -N
Bonus track
- visualizzare solo i commit che hanno coinvolto un dato file: git log -- file.txt
- per avere informazioni su un singolo commit, si può anche usare git show
- per visualizzare quali commit hanno influenzato le singole righe di un file (e vedere chi le ha editate): git blame file.txt
Le basi: diff
Modifichiamo un file, senza aggiungerlo alla staging area:
$ git diff
Per vedere quanto è stato posto in staging area (e motivo per cui è utile usarla):
$ git diff --staged
Le basi: tag
Un tag è un puntatore ad un commit:
$ git tag -a v1.0
Bonus track
- esistono sia i tag lightweight che annotated. La differenza principale è che i primi sono solo dei puntatori, i secondi sono oggetti completi: hanno un author e possono essere firmati.
Aggiustare i danni
Modificare l'ultimo commit (cambiare il commit message o l'autore, oppure modificare un file - in questo caso va prima modificato nella working directory e fatto git add):
$ git commit --amend [--author="Name Surname <user@example.com>"]
Un file stato aggiunto per sbaglio alla staging area:
$ git reset HEAD -- file
Riportare un file modificato nell'ultimo stato committato/staged:
$ git checkout -- file
Bonus track
- notate come il commit ID viene modificato, con --amend, motivo per cui è possibile usarlo solo sull'ultimo commit
- git clean -f per rimuovere tutti i file untracked
Aggiustare i danni: più forte
Ho fatto un casino nella working directory. Riportiamo tutto allo stato dell'ultimo commit:
$ git reset --hard HEAD
Voglio creare un nuovo commit che annulla le modifiche introdotte da un commit precedente:
$ git revert [-n] <commit>
Bonus track
- maggiori informazioni sul reset
- workflow per risolvere problemi
- qualche comando utile per risolvere i guai fatti
Branches: cosa sono e perché usarli?
Sono puntatori mobili, spostati ad ogni nuovo commit.
Servono a separare diversi filoni di sviluppo e ad integrare i contributi di altri.
Branches: creazione
Creare un branch:
$ git branch fix/bug-123
Visualizzare tutti i branch:
$ git branch [-a] [-v]
Cancellare un branch locale:
$ git branch -d [--force] fix/bug-123
Branches: spostiamoci
Spostarsi su un branch:
$ git checkout fix/bug-123
Creare e spostarsi in un singolo comando (può essere usato solo se il branch non esiste ancora):
$ git checkout -b fix/bug-123
Bonus track
- nello spostarsi, Git cerca di mantenere i cambiamenti presenti nella working directory e nella staging area
Branches: approfondiamo
-
master è solamente un default (di norma si considera master "stabile")
-
dare nomi significativi; usate prefissi come bugfix/, fix/, improvement/, feature/, task/) e issue di riferimento
-
prendete l'abitudine, tutte le volte che sviluppate un fix o una nuova feature, di farlo in un nuovo branch (che di norma partirà da master)
-
possono essere logicamente suddivise: feature (o topic), release, integration branches e così via
Rimettere insieme i pezzi: merge
$ git checkout -b fix/bug-123
$ # editiamo nuovofile.txt
$ git add nuovofile.txt
$ git commit
$ git checkout master
$ git merge fix/bug-123
Merge: cosa è successo?
fast-forward!
master era più indietro rispetto a fix/bug-123, e quindi abbiamo semplicemente spostato il puntatore master. Non è stato neppure creato un nuovo commit.
Il comando commit ha le opzioni --ff-only e --no-ff per decidere come comportarsi.
Risoluzione dei conflitti
$ git branch fix/bug-123
$ git checkout fix/bug-123
$ # editiamo file.txt
$ git add file.txt
$ git commit
$ git checkout master
$ # editiamo file.txt in maniera differente, sulle stesse righe
$ git add file.txt
$ git commit
Bonus track
- quali commit fanno parte del branch fix/bug-123 e quali di master?
Risoluzione dei conflitti
Mergiamo:
$ git merge fix/bug-123
$ # risolviamo i conflitti
$ git add file.txt
$ git commit
Bonus track
- che succede al commit C se cancelliamo fix/bug-123?
Conflict files
Cercare sempre tutti i markers <<<<<<<, =======, >>>>>>>
Bonus track
- potete usare meld come GUI per risolvere i conflitti
Lavorare con repository remoti
$ git remote add origin https://git.lattuga.net/user/repo.git
$ git remote -v
Bonus track
- origin è solamente un default
- si può fare il checkout di un branch remoto con remote/branch (e.g.: git checkout origin/fix/bug-123)
Fetch & pull
Aggiornare il repository locale con i dati di un remoto:
$ git fetch --prune origin
Commit che divergono tra il master locale e quello remoto:
$ git log --left-right master...origin/master
Scaricare gli aggiornamenti dal remoto e mergiare il branch corrente:
$ git pull origin
Bonus track
- git pull è identico a git fetch ; git merge
Branches locali e remoti
-
local branch: un branch che avete solo in locale
-
remote branch: un branch che esiste su un repository remoto
-
remote tracking branch: la copia locale di un remote branch; aggiornabile con fetch, non è possibile lavorarci sopra direttamente
-
local tracking branch: un branch locale su cui è possibile lavorare direttamente, che traccia un altro branch (di norma, un remote tracking branch)
-
il local tracking di branch remoti viene effettuato in automatico, in base al nome del branch: se nel repository remoto esiste origin/branch-1, il comando git checkout branch-1 crea un local tracking branch che traccia il remote tracking branch origin/branch-1
Push
Aggiungere al repository remoto un branch locale:
$ git push --set-upstream origin local-branch-name
Inviare i cambiamenti locali ad un branch remoto:
$ git push [--tags] [origin [master]]
Bonus track
- git push di default non invia i tags, che vanno pushati separatamente aggiungendo --tags
- cancellare un branch remoto: git push --delete origin branch-name
Parlando della history remota...
Cosa da non fare MAI (salvo non ne siate davvero convinti): modificare una history che sia già stata pushata.
Questo perché se qualcuno sta lavorando sullo stesso branch remoto, le altre persone si troveranno con dei repository non coerenti.
Parte 2
In cui forniamo un workflow precotto per chi non vuole porsi troppe domande, adatto allo sviluppo collaborativo.
Quale workflow?
Nello scegliere un workflow dovrete rispondere ad alcune domande, quali:
- chi parteciperà allo sviluppo? Vengono accettati contributi da esterni o solo da un gruppo ristretto?
- qual è il mio modello di rilascio del software? Ho versioni multiple da manutenere? A partire da quanti/quali branch verranno rilasciate le nuove versioni del mio software?
- chi si occuperà dell'integrazione? Gli sviluppatori stessi o una figura dedicata?
Worflows: le alternative
I principali sono:
- centralized
- feature branch
- gitflow
- forking
- qualcosa tenuto insieme con gli elastici
Valide risorse:
- https://www.atlassian.com/git/tutorials/comparing-workflows
- https://guides.github.com/introduction/flow/
Forking workflow
Vediamo il forking workflow. Non perché sia intrinsecamente il migliore, ma perché quello più diffuso nello sviluppo su piattaforme come Github. Presupposti:
- esiste un repository ufficiale (che, dal punto di vista di un developer, chiameremo upstream) di riferimento su cui solo gli autori principali possono scrivere
- ruolo di project maintainer: la persona che si occuperà di mergiare nel repository upstream
- ruolo di developer: chi sta sviluppando un fix o una nuova feature
- ciascun developer avrà un fork remoto del repository upstream ed una copia locale su cui lavorare
Forking workflow: maintainer setup
Il project maintainer ha creato il repository upstream remoto e il proprio clone locale.
$ git clone https://git.lattuga.net/maintainer/repo.git
Forking workflow: developer setup
Il developer ora:
- crea un fork remoto del repository upstream
Bonus track
- un fork altro non è che un clone (--mirror) di un repository, sempre ospitato sul sito remoto
Forking workflow: developer setup
Developer fa un clone locale del proprio repository remoto. È una buona idea aggiungere un remote "upstream" che punti al repository del maintainer:
$ git clone https://git.lattuga.net/developer/repo.git
$ cd repo
$ git remote add upstream https://git.lattuga.net/maintainer/repo.git
Forking workflow: iniziamo lo sviluppo
Developer deve sviluppare un fix che andrà applicato sul branch master del repository upstream.
Prima di tutto è opportuno sincronizzare il proprio branch master con quello upstream, in modo da lavorare su codice recente:
$ git checkout master
$ git pull upstream master
Forking workflow: nuovo branch
$ git checkout -b fix/bug-123
Bonus track
- MAI lavorare direttamente su master: perdereste la possibilità di sincronizzarlo di nuovo con upstream, in futuro
Forking workflow: lavoriamo
$ # introdurre il fix
$ git commit
$ git push --set-upstream origin fix/bug-123
Forking workflow: pull request
Ora va sulla pagina web del proprio fork e crea una pull request.
Bonus track
- ha senso fare un rebase su upstream/master del feature branch su cui stiamo lavorando, prima di creare la pull request (se avete già pushato, servirà un push --force) in modo che il vostro lavoro sia più vicino possibile allo stato attuale di upstream/master
Forking workflow: pull request
Pull request NON è un concetto base di Git (non esattamente, almeno). È qualcosa che vi è stato costruito sopra per facilitare la collaborazione tra sviluppatori.
La pull request creata in precedenza dice: "propongo di applicare i commit del branch developer:fix/bug-123 a maintainer:master" Ora developer, project maintainer e altri possono discuterne.
Se dovesse essere necessario, developer o altri possono aggiungere altri commit semplicemente con un nuovo push.
Forking workflow: merging
Una volta soddisfatti, project maintainer potrà effettuare il merge del codice su maintainer:master.
Se il merge non presenta conflitti, lo farà direttamente dalla GUI web sul repository upstream.
Altrimenti dovrà aggiungere un remote che punti al repository di developer, fare il fetch di developer:fix/bug-123, effettuare il merge su master per poi farne il push sul repository upstream.
Forking workflow: bugia!
Github e amici non suggeriranno di aggiungere lo sviluppatore come remote, ma di fare direttamente il pull del suo topic branch. È sicuramente più pulito se ricevete molte pull request da tante persone differenti. Se invece il numero di contributori è basso (piccoli progetti, o in ambito lavorativo) ha senso aggiungere i loro repository come remotes.
Nel caso di Github, ad esempio:
- git checkout -b developer/bug-123 master
- git pull https://github.com/developer/repo.git fix/bug-123
- git checkout master
- git merge --no-ff developer/bug-123
- git push origin master
Forking workflow: sunto setup maintainer
- clone locale: git clone https://git.lattuga.net/maintainer/repo.git
Forking workflow: sunto setup developer
- fork sul web
- clone locale del fork: git clone https://git.lattuga.net/developer/repo.git
- aggiunge un remote che punta al repository upstream: git remote add upstream https://git.lattuga.net/maintainer/repo.git
Forking workflow: sunto sviluppo developer
- aggiorna il proprio master: git checkout master ; git pull upstream master
- crea un branch su cui lavorare: git checkout -b fix/bug-123
- lavora un sacco: git commit
- opzionalmente, fa il rebase: git rebase upstream/master
- invia le modifiche al proprio repository remoto: git push --set-upstream origin fix/bug-123
- crea sul web una pull request
- se serve, integra il lavoro semplicemente pushando altri commit fatti su fix/bug-123
Forking workflow: sunto lavoro del maintainer
- riceve una pull request e la valuta.
- se mergiabile senza conflitti, lo fa via web.
Altrimenti:
- se non lo ha già fatto, aggiunge un remote per il repository del developer: git remote add developer https://git.lattuga.net/developer/repo.git
- crea una local tracking branch su cui lavorare: git fetch developer fix/bug-123
- si sposta su master: git checkout master
- effettua il merge risolvendo i conflitti: git merge --no-ff fix/bug-123
- invia il master al proprio repository remoto: git push origin master
Parte 3
In cui forniremo una serie di strumenti avanzati.
Referenziare i commit
Salire di 3 livelli, seguendo sempre il primo parent commit (in caso di merge):
$ git show -s HEAD~3
Salire di un livello, seguendo il secondo parent commit (in caso di merge):
$ git show -s HEAD^2
Bonus track
- detached HEAD: ci siamo spostati su un commit che non è l'head di un branch
- questi operatori sono concatenabili: HEAD~~^2
Referenziare i commit: range
Double dot range. Usando diff mostra i cambiamenti tra "master" e "branch"; usando log mostra i commit raggiungibili da "branch" ma non da "master":
$ git diff master..branch
Triple dot range. Usando diff mostra la differenza tra il punto di biforcazione tra "master" e "branch" e "branch" stesso; usando log mostra i commit raggiungibili da "master" o "branch", ma non da entrambi:
$ git log --left-right master...branch
Referenziare i commit: range
Vedere anche questa spiegazione
Rimettere insieme i pezzi: cherry-pick
$ git checkout master
$ git cherry-pick <commit>
$ # in caso di conflitti:
$ git cherry-pick --continue
cherry-pick: cosa è successo?
Si sono prese le modifiche introdotte dai commit elencati, e sono state riapplicate sul branch corrente. Sono stati creati dei nuovi commit.
Quando usarlo?
Ad esempio per backportare un fix su diversi release branch, o se vi siete accorti che un certo commit era da fare su un altro branch.
Rimettere insieme i pezzi: rebase
Poniamoci nella stessa situazione divergente dell'esempio in cui abbiamo usato merge, e poi:
$ git checkout fix/bug-123
$ git rebase master
$ # risolviamo eventuali conflitti
$ git rebase --continue
Cosa è successo?
Abbiamo preso tutti i commit di fix/bug-123 e li abbiamo ri-applicati su master, che nel mentre era andato avanti. Tutti i commit specifici di fix/bug-123 sono cambiati. Volendo, ora si può fare un merge fast-forward in master.
Rebase: quando usarlo?
Quando dovete spostare più commit e/o per porvi nella condizione di fare un merge pulito. Questo può essere fatto dal developer prima di aprire una pull request per semplificare il lavoro al maintainer e/o dal maintainer stesso prima del merge, per ottenere una history lineare.
Quando NON usarlo?
Un rebase modifica i commit originali del branch: questo va evitato se quei commit sono già stati pushati ed altri sviluppatori li stanno usando come base per il proprio lavoro.
Modificare la history: rebase interactive
Creiamo un nuovo branch e committiamo 2 o 3 modifiche. Poi:
$ git rebase -i master
Rebase interactive: cosa è successo?
Abbiamo accorpato, scartato o invertito l'ordine dei commit.
È particolarmente utile quando abbiamo finito di lavorare su un branch, e vogliamo semplificare la history accorpando molti commit in uno solo.
Bonus track
- l'opzione nucleare: filter-branch per creare script che riscrivono la history.
Lavoro incompleto: committare a pezzi
Editiamo un file in vari punti, e poi aggiungiamolo alla staging area con --patch:
$ git add --patch
Quando usarlo?
Ad esempio quando non si vuole includere in un commit una riga di debug, che però si vuole mantenere nella working directory.
Creare e applicare patch
è possibile creare una patch usando il comando:
$ git format-patch [refs]
per poi applicarla con:
$ git apply patch-file.diff
Mettere il lavoro da parte: stash
Mettere da parte il lavoro nella working directory senza committare, e mostrare le modifiche stashed:
$ git stash
$ git stash list
Riapplicare una modifica messa in stash, ed eliminarne uno (stash pop combina i due comandi):
$ git stash apply stash@{0}
$ git stash drop stash@{0}
Quando usarlo?
Ad esempio quando vogliamo passare ad un altro branch, accantonando le modifiche nella working directory.
Storico dei cambiamenti: reflog
La history mostra solo i commit inclusi in un branch.
Per vedere TUTTI gli spostamenti di HEAD:
$ git reflog [@{2 weeks ago}]
Quando usarlo?
- a volte è utile capire come ci siamo mossi tra i branch
- fondamentale per recuperare i broken commits (non referenziati da alcun branch)
Idee sparse
- gestire file grandi: https://git-lfs.github.com/
- gestire file grandi (alternativa): https://git-annex.branchable.com/
- gestire la propria directory /etc: etckeeper
- gestire repository multipli: https://source.android.com/source/using-repo
- git repository manager: https://gogs.io/
- git repository manager: https://about.gitlab.com/
Pezzi mancanti
- git submodule: gestire altri repository come sotto-moduli
- git subtree: inserire un repository in una sottodirectory
- git bisect: cercare il commit in cui è stato introdotto un bug
- git gui e gitk: GUI per visualizzare commit e repository
- tig: interfaccia testuale
- Gitgraph.js: creare grafi di commit e branch
Risorse per imparare
- Pro Git: https://git-scm.com/book/en/
- Reference: https://git-scm.com/docs
- Learn Git Branching: http://learngitbranching.js.org/
- Git ready: http://gitready.com/
- Git Cookbook: https://git.seveas.net/
- tutorial di Atlassian: https://www.atlassian.com/git/tutorials
- A visual Git reference: https://marklodato.github.io/visual-git-guide/index-en.html
Utilità
- bash prompt: https://github.com/magicmonty/bash-git-prompt
- Meld: http://meldmerge.org/
The end
git clone https://git.lattuga.net/alberanid/git-crash-course.git
Davide Alberani da@erlug.linux.it
This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License: http://creativecommons.org/licenses/by-sa/4.0/