Guida Git in italiano | Aulab

GUIDE PER ASPIRANTI PROGRAMMATORI

Guida Git in italiano

In questa guida scoprirai che cos’è Git, il sistema di controllo di versione indispensabile per l’organizzazione del flusso di lavoro e per la cooperazione dei web developers, e come utilizzarlo al meglio padroneggiandone ogni singola funzionalità.

Immagine di copertina

Vuoi avviare una nuova carriera o fare un upgrade?

Trova il corso Digital & Tech più adatto a te nel nostro catalogo!

1

Introduzione a Git per developers

1.1

Che cos'è Git?

Git è un sistema di controllo delle versioni (version control system o VCS, talvolta indicato anche source control management system o SCM) free e open-source, creato da Linus Torvalds (il “papà” di Linux) nel 2005. Come ogni version control system, Git permette di gestire la cronologia di tutte le modifiche apportate al proprio codice ma, a differenza di altri strumenti che lo hanno preceduto, lo fa in una maniera definita “distribuita”, facilitando quindi lo sviluppo collaborativo. Git è anche noto per essere un tool difficile da usare, o quanto meno da padroneggiare con sicurezza nel momento in cui, di tanto in tanto, è necessario compiere qualcuna delle azioni più avanzate. Niente panico, anche uno sviluppatore web esperto che utilizza Git da anni ogni tanto può avere dubbi sull’esatto comando da dare per eseguire una determinata azione. D’altro canto, Git è anche il tool che, in vari sondaggi eseguiti da community e osservatori dedicati al mondo del coding, risulta essere usato da più del 90% degli sviluppatori come version control system primario.
1.2

Nascita di Git

Ma com'è nato Git? La “nascita” di Git è legata, come talvolta succede nel mondo open-source, alla necessità di avere una alternativa free a un tool proprietario. Nel 2005 lo sviluppo del kernel Linux aveva raggiunto, in termini di righe di codice, una dimensione ragguardevole e le varie modifiche apportate venivano gestite anche tramite l’invio per email di patch (file che contengono la “differenza” tra la versione originale e la versione modificata dall’autore delle modifiche).  Già da qualche anno gli sviluppatori di Linux utilizzavano Bitkeeper, un SCM proprietario per il quale era disponibile una licenza “community”, seppur con alcune restrizioni. A causa di queste restrizioni, alcuni sviluppatori tentarono di realizzare una copia open di BitKeeper, ma tale azione fu vista come contraria ai termini d’utilizzo di BitKeeper e fu quindi ritirata la licenza “community”. Privi di un tool adeguato a gestire il carico di lavoro necessario - nel racconto di Linus Torvalds, il tempo necessario ad applicare una singola patch era circa mezzo minuto e per modifiche più elaborate, composte da diverse patch in sequenza, potevano essere necessarie fino a due ore solo per applicare le patch - Torvalds e altri sviluppatori di Linux decisero di creare un tool che fosse adeguato alle loro esigenze. Principali obiettivi erano, infatti, quello di supportare un flusso di lavoro distribuito, salvaguarda della corruzione dei dati (accidentale o intenzionale) e altissime prestazioni. Nel giro di poche settimane, da inizio aprile 2005 a metà luglio dello stesso anno, Git fu sviluppato e diventò lo strumento di gestione e pubblicazione delle release del kernel Linux. A dicembre dello stesso anno fu rilasciata la versione 1.0.
1.3

Principali caratteristiche di Git

La storia della nascita di Git ci permette di capire quelle che sono le caratteristiche principali di questo tool. In quanto version control system, lo scopo di Git è quello di permettere la gestione delle modifiche apportate a una collezione di informazioni, che si tratti del codice sorgente di un'applicazione, della pagina HTML di un sito statico o di documentazione. L’evoluzione di ogni tipo di informazioni raccolta in file testuali, per i quali è quindi possibile sapere le differenze tra due specifiche versioni, può essere gestita da un VCS. La particolarità di Git risiede nel supportare e gestire al meglio: lo sviluppo distribuito - ogni developer che partecipa a un progetto non è legato a un server centrale che mantiene la copia “originale” del progetto performance elevate per grandi progetti - lavorare su progetti con una lunga storia o progetti composti da moltissimi file è rapido come lavorare su progetti più giovani o con pochi file la salvaguardia della storia delle modifiche - l’ultima versione è data dalla sequenza esatta di tutte le modifiche apportate nel tempo, pur essendo possibile inserire una nuova modifica tra due modifiche già apportate in passato, questo cambio della sequenza degli eventi non può passare inosservato Vedremo più approfonditamente nelle successive lezioni come questi tre principi guida rendono Git uno strumento estremamente potente e versatile
1.4

Riga di comando e UI in Git

Git nasce come tool da usare dalla riga di comando (o terminale), affiancando, quindi, altri strumenti necessari nella quotidianità di uno sviluppatore web come editor, IDE, compilatori e via discorrendo. Da riga di comando l’eseguibile di Git permette di impartire uno dei vari comandi disponibili, passando le opportune opzioni (generiche) e argomenti (specifici del singolo comando) necessari a compiere l’azione desiderata. git [opzioni...] <comando> [argomenti...] Tali comandi possono essere suddivisi in diversi ambiti: avviare una working area lavorare sulle modifiche attuali esaminare cronologia e stato modificare la cronologia collaborare Nel tempo, sono state realizzate varie applicazioni grafiche dedicate che funzionano come interfaccia visiva a Git. Tali applicazioni permettono di impartire la quasi totalità dei vari comandi in una veste più accattivante e, molto spesso, offrono come visione di partenza la cronologia delle modifiche apportare al progetto stesso. Oltre alle applicazioni grafiche pensate espressamente per Git, c’è da considerare che molti IDE e editor pensati per i web developers offrono un rapido accesso ai comandi principali di Git, in modo da poter, ad esempio, visualizzare quali modifiche non sono ancora state salvate nel file che si sta modificando. In linea generale, per imparare a usare al meglio Git è sempre meglio fare riferimento alla riga di comando, ma d’altro canto non bisogna dimenticare che riga di comando e interfacce grafiche sono perfettamente interscambiabili, visto che alla fine tutte faranno riferimento alla stessa cronologia di modifiche salvate per il progetto su cui si sta lavorando

2

Il tuo primo commit in Git

2.1

Come Installare Git

Prima di scendere nel dettaglio di ognuno dei vari comandi offerti da Git, proviamo a usare un set minimo di comandi utili nella vita di tutti i giorni. Come Installare Git È possibile che sul tuo computer sia già installato Git, poiché fa parte del set di applicazioni e tool installati spesso come bundle per web developers dai vari sistemi operativi. In particolare: Distribuzione Linux - disponibile come pacchetto a sé (git), usare il package manager della propria distribuzione per installarlo macOS - incluso nei Developer Tool forniti da Apple e/o con XCode, indicativamente è possibile installarlo aprendo un terminale e dando il comando git Windows - è possibile scaricare l’installer ufficiale dal sito di Git In tutti i casi, se aprendo un terminale (o riga di comando), digitando git --help e premendo Invio viene mostrato qualcosa come il seguente, allora Git è disponibile e pronto sul computer $ git --help usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>] [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path] [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare] [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>] [--super-prefix=<path>] [--config-env=<name>=<envvar>] <command> [<args>] These are common Git commands used in various situations: [...] NOTA nel seguito di questa guida verranno mostrati esempi che usano come riferimento un sistema operativo Unix-like (Linux o macOS).
2.2

5 comandi Git per sviluppatori singoli

Come accennato, Git è un ottimo alleato sia per il lavoro del developer singolo che per la cooperazione di team di web developers. Vediamo adesso i 5 comandi principali che utilizzerà lo sviluppatore individuale e alcune loro argomentazioni. creare una nuova directory in Git mkdir primo-progetto entrare nella directory in Git cd primo-progetto inizializzare il repository Git nella cartella $ git init . Initialized empty Git repository in /home/developer/primo-progetto/.git/ creare un file vuoto in Git touch README.md aggiungere il file a quelli inclusi nel repository in Git git add README.md salvare la prima versione del file nel repository in Git $ git commit -m "creato file vuoto read me" [master (root-commit) 776795e] creato file vuoto read me 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 README.md vedere la storia del repository in Git: $ git log commit 776795ecb7a9f0e5fd9eee1ae8b21b9ac06b3716 (HEAD -> master) Author: Luca Ferretti <[email protected]> Date: Tue Dec 6 00:26:55 2022 +0100 creato file vuoto read me aprire il file README.md in un editor di testo, scrivere qualcosa e salvare il file   visualizzare le modifiche tra l’ultima versione del repository e quella attualmente sul computer in Git $ git diff diff --git a/README.md b/README.md index e69de29..41c99e9 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,3 @@ +Hello, Git! + vedere lo stato del repository in Git $ git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: README.md no changes added to commit (use "git add" and/or "git commit -a") aggiungere il file a quelli da includere nel prossimo set di modifiche da salvare nella cronologia delle modifiche in Git: git add README.md salvare il nuovo set di modifiche in Git $ git commit -m "primo contenuto del file read me" [master f0896d3] primo contenuto del file read me 1 file changed, 3 insertions(+) visualizzare la nuova cronologia, ma stavolta nel formato abbreviato: $ git log --pretty=oneline --abbrev-commit f0896d3 (HEAD -> master) primo contenuto del file read me 776795e creato file vuoto read me Complimenti, hai creato il tuo primo repository e usato i 5 comandi (e alcuni loro argomenti) che vi accompagneranno ogni giorno nel vostro viaggio assieme a Git. Cosa abbiamo fatto? Un po’ confuso? Tranquillo! Ti spieghiamo un po’ meglio cosa abbiamo fatto: la sequenza di comandi riportata nel precedente paragrafo ha compiuto le seguenti azioni: inizializzare un repository in Git - il comando git init crea un repository nella directory corrente; in pratica viene creata nella directory una sotto-directory nascosta .git in cui verranno salvati di volta in volta tutti i fine necessari a Git per conoscere la cronologia delle modifiche (history) e lo stato attuale; aggiunto un nuovo file al repository in Git - la prima sequenza dei comandi git add e git commit salva la nostra prima modifica alla history, tale modifica prende il nome di commit nel linguaggio di Git; salvata una modifica a un file già incluso nel repository in Git - con la seconda sequenza dei comandi git add e git commit si è salvato nella history un secondo commit, ovvero un secondo gruppo di modifiche di cui vogliamo tenere traccia; visualizzato lo stato del repository in Git - i comandi git status e git diff ci permettono di avere informazioni sullo stato attuale del repository, sulla eventuale presenza di modifiche a file aggiunti al repository, sulla presenza di file non gestiti ed altro; visualizzata la history del repository in Git - il comando git log permette di accedere alla cronologia dei commit eseguiti, con maggiore o minore dettaglio a seconda degli argomenti indicati È importante notare subito un paio di caratteristiche di Git (o più in generale dei version control system): non tutto il contenuto di una directory è automaticamente aggiunto al repository: va esplicitamente indicato quali sono i file da includere; ciò risulta molto utile nel caso di progetti software, per i quali è naturale voler gestire l’evoluzione dei file sorgenti ed escludere invece i file “di build” non tutte le modifiche salvate ai file di un progetto sono automaticamente incluse nel commit: va esplicitamente indicato cosa includere nei singoli commit; ciò risulta utile per includere nel commit solo le modifiche rilevanti al fine di ricostruire l’evoluzione di un progetto, escludendo magari prove intermedie il workflow tipico di Git ruota attorno allo stato attuale dei file nella directory del repository (nuovi, rimossi, modificati) e al momento in cui si vuole fissare nella history un ben specifico stato Di questa ultima annotazione parleremo più estesamente nella prossima sezione, andando a chiarire alcuni concetti fondanti di Git.
2.3

5 comandi Git per sviluppare collaborando

Come già anticipato, uno dei punti di forza di Git è il supporto allo sviluppo distribuito di progetti. Sebbene Git offra diverse possibilità per gestire lo sviluppo collaborativo, proviamo subito a presentare il caso di un web developer che voglia apportare il suo contributo a un progetto condiviso già avviato, analizzando, anche qui, i comandi principali. recuperare il repository ospitato su un server remoto $ git clone https://example.com/project.git Cloning into 'project'... remote: Enumerating objects: 19, done. remote: Counting objects: 100% (19/19), done. remote: Compressing objects: 100% (14/14), done. remote: Total 19 (delta 3), reused 16 (delta 3), pack-reused 0 Receiving objects: 100% (19/19), 112.25 KiB | 281.00 KiB/s, done. Resolving deltas: 100% (3/3), done. entrare nella directory project creata dallo step precedente cd project creare un branch git branch my-contribution passare il branch appena creato $ git checkout my-contribution Switched to branch 'my-contribution' apportare modifiche a file esistenti, oppure aggiungere un file, ed eseguirne il commit, come fatto nella precedente sezione verificare l’attuale sorgente “remota” rel repository $ git remote --verbose origin https://example.com/project.git (fetch) origin https://example.com/project.git (push) inviare le proprie modifiche su un branch del repository remoto git push origin my-contribution Complimenti, hai creato il tuo primo contributo (anche se per ora non ancora nella linea “principale” del progetto) a un progetto remoto! Che cosa abbiamo fatto? Come sopra: i comandi presentati per lo sviluppo distribuito hanno compiuto le seguenti operazioni: creazione di una copia locale del repository in Git - il comando git clone crea un clone di un repository disponibile su un server; verrà creata una directory locale che contiene la copia completa (da qui il nome clone) del repository remoto, comprensiva di tutta la history e di tutti i commit che la compongono; creazione di un branch in Git - vedremo meglio nel dettaglio cosa è un branch nel prossimo capitolo, per ora è sufficiente pensare ai branch come diverse “linee temporali” del repository; nel momento in cui si clona un repository remoto viene attivato il branch predefinito (il cui nome è tipicamente master o main), il comando git branch impartito ha creato un nuovo branch che “continua” dall’ultimo commit applicato al branch principale. selezione di un branch in Git - è possibile scegliere su quale branch lavorare tramite il comando git checkout; questo comando modifica il contenuto della directory locale del repository riportando il suo contenuto all’ultimo commit applicato su quel branch; i successivi commit verranno applicati solo al branch attualmente selezionato; visualizzazione dei “remote” in Git - il comando git remote permette di sapere a quale sorgente remota sta puntando il nostro clone locale; molto spesso si tratterà di una sola sorgente, ma in funzione della composizione e scelte del team potrebbero esserci più sorgenti remote, eventualmente distinte per sorgenti da cui recuperare le modifiche degli altri sviluppatori (fetch) e sorgenti a cui inviare le nostre modifiche (push); invio delle proprie modifiche in Git - con il comando git push è possibile inviare a un repository remoto (nell’esempio quello nominato origin) quanto presente nel clone locale; in particolare, con il comando indicato sopra, viene inviato il branch my-contribution e tutti i commit in esso contenuti; Approfondiremo più nel dettaglio il flusso di lavoro con repository remote in seguito, ma già con i comandi introdotti finora è possibile creare una serie di modifiche a un progetto preesistente, “raccolte” in un branch separato che, una volta fatto “push” è disponibile a tutti coloro che hanno accesso al repository remoto.

3

Terminologia e concetti di Git

3.1

Repository in Git

Nel precedente capitolo abbiamo illustrato i comandi base di Git per lavorare su un repository, che sia stato creato da zero sul proprio computer o che sia stato recuperato da uno già esistente ospitato su un server remoto. Prima di entrare più nel dettaglio, è opportuno chiarire meglio alcuni concetti base di Git e come questi impattino sull’effettivo flusso di lavoro. Repository in Git Un repository Git tiene traccia e salva la cronologia delle modifiche apportate ai file della directory in cui il repository è stato inizializzato. Nella pratica, il repository Git del progetto “my-project” che si trova sul nostro computer nella directory “my-project” è la directory nascosta .git dentro la quale sono salvati tutti i dati necessari a Git per gestire cronologia e stato. my-project / +- .git/ | +- index | +- refs/ | ... +- README.md +- server.php | ... È particolarmente importante notare che: eliminando la directory .git viene eliminata l’intera cronologia del progetto. La directory my-project torna ad essere una directory “normale”. non è detto che tutti i file presenti nella directory my-project facciano parte del repository (solo i file che sono stati espressamenti aggiunti al repository entrano di diritto nella history) Ci sono due modi per ottenere un repository Git: inizializzare il repository in una directory attualmente non sotto controllo di versione (git init) clonare un repository esistente (git clone)
3.2

Commit in Git

Un commit in Git rappresenta uno snapshot (istantanea) del repository in uno specifico momento nel tempo. Sebbene questa definizione possa sembrare banale, vale la pena sottolineare che: a differenza di altri version control system, il commit in Git non si limita a memorizzare le differenze tra due versioni successive del contenuto della directory, ma salva l’istantanea (compressa) del contenuto di tutti i file che al momento del commit facevano parte del repository se un file non cambia contenuto tra due commit, non viene memorizzata una nuova versione compressa, ma viene messo un riferimento all commit precedente ogni commit conosce il suo “genitore” (parent commit), ossia il commit a lui immediatamente precedente Il commit è l’entità base su cui si basano i repository Git
3.3

Working Copy in Git

Il concetto di Working Copy in Git (detta anche working directory o working tree) è strettamente legato al fatto che un repository Git gestisce la sequenza di snapshot (commit) di determinati file contenuti in una directory. Per capire cosa è e come funziona la working copy è, però, forse, più semplice farne un esempio: Nel momento in cui viene clonato un repository esistente remoto, per esempio https://example.com/project.git, Git compie le sequenti azioni: crea una directory locale vuota project copia dentro questa directory tutti gli snapshot (commit) presenti sul repository remoto (in project/.git) individua l’ultimo snapshot (commit) della history estrae il contenuto di tutti i file corrispondente all’ultimo snapshot (commit) La working copy in Git è, quindi, ciò che Git mette a disposizione dello sviluppatore web sul suo computer locale nel momento in cui gli si chiede (direttamente o indirettamente) di tirare fuori dalla history uno specifico commit. Nel momento in cui apportiamo modifiche ai file estratti dallo snapshot (commit) o ne aggiungiamo/rimuoviamo/spostiamo/copiamo alcuni, Git può stabilire quali file sono cambiati nella working copy rispetto al commit da cui è stata estratta la working copy stessa. In particolare i file nella working copy possono trovarsi nei seguenti stati: Unmodified - il file locale e lo snapshot da cui è stato estratto hanno lo stesso contenuto Untracked - il file locale non è presente nello snapshot Modified - il file locale contiene modifiche rispetto alla snapshot da cui è stato estratto Per completezza, nel glossario di Git si parla di “working tree” ed è definito come “The tree of actual checked out files.”
3.4

Staging Area in Git

La staging area in Git è il luogo “virtuale” a cui aggiungere le modifiche presenti nella working copy che si intende salvare come commit. Anche per la staging area in Git vale la considerazione che è un concetto legato al funzionamento base di Git (sequenza di snapshot nel tempo estratti in una working area). Molto spesso, infatti, non tutte le modifiche apportate alla working copy sono “buone” da essere ufficialmente salvate nella history del repository (un bravo sviluppatore web esegue commit solo di modifiche che sono funzionanti o rilevanti per l’evoluzione del progetto). La staging area in Git è il luogo in cui vengono “raccolte” le modifiche che faranno parte del prossimo commit. Da notare, in particolare, che è possibile contrassegnare sia interi file, sia singole porzioni di modifiche a un file. Se, per esempio, modifichiamo e salviamo un file, aggiungiamo il file alla staging area, modifichiamo e salviamo di nuovo il file, poi salviamo un commit, solo la prima delle due modifiche sarà inclusa nel commit, anche se si tratta dello stesso file. Rispetto all’elenco di stati elencati poco sopra, andrebbe, quindi, incluso anche lo stato Staged, anche se è opportuno pensarlo da subito come riferibile alle singole modifiche e non a interi file
3.5

Branch in Git

Abbiamo detto, poco sopra, che Git confronta i file presenti nella working copy rispetto al commit da cui sono stati estratti per capire in che stato si trovano. Più frequentemente, però, sentiremo dire (e inizieremo a dire anche noi) che le eventuali modifiche presenti nella working copy sono rispetto a un branch su cui sto lavorando. Proviamo a fare chiarezza. Sappiamo che un commit è per Git uno snapshot del contenuto del repository in un dato momento. Sappiamo che ogni commit ha un riferimento al commit che lo ha preceduto. In questo modo, la sequenza di commit collegati l’uno all’altro ci permette di conoscere e ricostruire l’intera cronologia del progetto. Supponiamo di aver portato avanti un progetto fino al rilascio della sua versione 1.0. Siamo pronti a lavorare a nuove fantastiche funzioni in vista della versione 2.0 tra qualche mese, ma, nel frattempo, magari potremmo dover fare dei rilasci “di fix” della versione 1 (1.1, 1.2, …). Come può facilitarci la vita Git Fig - Commit su branch main fino alla versione 1.0   Quando abbiamo creato nel nostro progetto un repository con git init, Git ha anche creato un branch di default (il cui nome solitamente è main o master). Man mano che abbiamo salvato i vari snapshot, ci è sembrato di “aggiungere” nuovi commit a questo branch, come se il branch main fosse composto dalla sequenza di commit. In realtà, un branch in Git è un puntatore a uno specifico commit, un puntatore con un super potere: nel momento in cui viene effettuato un nuovo commit, il puntatore si sposta dal commit precedente all’ultimo. Nel nostro esempio, potremmo creare un nuovo branch che punta al commit con cui abbiamo contrassegnato la versione 1.0 - notare che più branch possono puntare allo stesso commit - per le versioni di manutenzione 1.x   Fig - Nuovo branch punta a stesso commit   Poiché ogni branch punta a un commit - tale commit è indicato come “tip” o “head” del branch stesso - e, poiché Git sa il commit e il branch da cui è stata estratto l’attuale contenuto della working copy, nel momento in cui si aggiunge un commit, questo diventa la nuova “head” del branch. In pratica, nel nostro progetto di esempio, potremo avere due commit “figli” dello stesso commit che contrassegna la versione 1.0, ognuno nei rispettivi branch, dando vita a due history separate.   Fig - Due branch, due history   Ciò che è importante è, quindi, avere sempre chiara la distinzione tra ciò che facciamo nel pratico con Git (“estrarre un branch nella working copy” oppure “vedere la cronologia di un branch” oppure “vedere le modifiche dell’ultimo commit”) e ciò che internamente fa Git (“estrarre nella working copy il commit (snapshot) che è la HEAD di un branch” oppure “partire dalla HEAD di un branch e ricostruire tutti i commit genitori” oppure “mostrare le differenza tra l’ultimo commit (snapshot) e il suo genitore (snapshot)”). È importante annotare che Git non è prescrittivo sulle modalità d’uso dei branch, sta quindi ai team scegliere se e come sfruttare i branch per le proprie necessità. Nel tempo sono state promosse e definite alcune “branch strategies” più comuni, basate sull’uso di branch e sulla possibilità offerta da Git di “spostare” da un branch a un altro i commit. Fig - Feature branch
3.6

Remote in Git

Ultimo concetto base da chiarire è quello del o dei Remote in Git, inteso nell'accezione di repository remoto a cui in qualche modo il mio repository locale è collegato. Abbiamo visto che possiamo copiare sulla nostra macchina un repository pre-esistente su un server remoto tramite git clone. Nel momento in cui git clone crea la copia locale, salva anche il riferimento (URL) al repository da cui ha recuperato i dati (commit, branch, …). Tale riferimento prende, appunto, il nome di “remote” oppure “remote repository”. Il nome del remote iniziale di un repository clonato è spesso origin. La connessione instaurata tra il branch locale e il branch remoto è utile sia per recuperare in seguito dal remote nuovi branch e nuovi commit, sia per inviare i propri branch e i propri commit. In particolare, in modo concettualmente simile a quello che succede tra working copy e branch da cui è stato estratto il commit, i branch presenti nella propria copia locale possono essere collegati a branch presenti nel branch remote. È, quindi, possibile sapere se la propria working copy e il proprio branch hanno differenze (in più o in meno) rispetto alla controparte remota. Come vedremo meglio in seguito, quando affronteremo gli sviluppi in collaborazione, è possibile avere più remote a cui il nostro repository locale punta e scegliere da quale remote recuperare dati o a quale remote inviare dati di volta in volta. Anche in questo caso Git non è prescrittivo sull’uso dei remote, sta ai team scegliere come usare i remote per gestire diverse modalità di collaborazione o di pubblicazione del codice sorgente

4

Creare e impostare un repository in Git

4.1

Inizializzare un nuovo repository con git init

In questa sezione approfondiremo come impostare un repository Git. In particolare vedremo come inizializzare un repository per un progetto nuovo oppure già esistente. Inizializzare un nuovo repository con git init git init è il comando da usare per il setup iniziale di un repository. L’esecuzione di questo comando crea la sottodirectory .git nella directory in cui viene eseguito (la directory del progetto); nessun file viene aggiunto o modificato oltre alla sottodirectory .git. All’interno della directory .git vengono creati tutti i metadati necessari a Git (branch di default, oggetti, reference, file template, …). La maggior parte dei comandi Git sono disponibili solo se eseguiti all’interno di un repository inizializzato. È, ovviamente, possibile inizializzare una directory diversa da quella corrente indicando il percorso: git init /path/to/project/directory
4.2

Configurare le opzioni di Git con git config

Il comando git config permette di impostare alcuni valori di default che verranno poi utilizzati dagli altri comandi Git. Tali valori potranno essere globali (cioè validi per tutti i repository git presenti sul proprio computer) o per repository (cioè applicati solo al repository su cui si sta lavorando). L’utilizzo più semplice di git config è quello che ci permette di impostare il proprio nome e la propria email che verranno poi usati nella history dei commit. Facciamo un esempio: Supponiamo di avere due repository, uno personale locale e uno clonato dal server dell’azienda per cui lavoriamo: cd projects mkdir my-secret-project git init my-secret-project git clone https://company.com/company-project.git Proviamo ora a impostare due diverse “identità”: git config --global user.email [email protected] git config --global user.name "Meme The Great" cd company-project git config --local user.email [email protected] git config --local user.name "John Smith" Con questi comandi abbiamo impostato i valori user.email e user.password globali che verranno usati da ogni repository che non ha impostato un valore locale, come nell’esempio il repository corrispondente all’origin https://company.com/company-project.git. In questo modo non sarà necessario a ogni commit del repository “di lavoro” indicare la propria email di lavoro. Man mano che si procede nell’imparare Git e nel far propri certi flussi di lavoro (o, diversamente, doversi attenere a particolari flussi su repository condivisi), git config si potrà rivelare uno strumento prezioso per configurare determinati comportamenti globali o diversi in base al singolo repository.
4.3

Creare una copia di un repository remoto in Git con git clone

git clone è il comando da utilizzare per creare una copia, o clone, di un repository già esistente, in particolare disponibile tramite una connessione di rete. La clonazione di un repository remoto sulla macchina locale è una operazione una tantum; una volta effettuata la copia locale, non è più necessario eseguire git clone (vedremo più avanti come recuperare le nuove modifiche caricate da altri). Nella sua versione più semplice, il comando git clone richiede che venga fornita l'URL del repository: git clone <URL> L’ URL può essere indicata in diversi formati, a seconda del protocollo offerto dal server che ospita il repository remoto, in particolare: SSH - ssh://[user@]host.xz[:port]/path/to/repo.git/ HTTP - http[s]://host.xz[:port]/path/to/repo.git/ GIT - git://host.xz[:port]/path/to/repo.git/ In generale, in base al server che ospita il repository in remoto saranno disponibili uno o più protocolli e sarà necessario autenticarsi o fornire le necessarie credenziali. A titolo d’esempio, se consideriamo un repository ospitato sul famoso servizio GitHub è possibile: clonare un repository pubblico con la url HTTP, p.e. git clone https://github.com/php/php-src.git: in questo modo si ottiene una copia “read only”, nel senso che non sarà possibile inviare le proprie eventuali modifiche clonare un repository privato con la url HTTP: in questo caso verrà chiesto di inserire il proprio username e la propria password, sarà possibile “scrivere” sul repository remoto clonare un repository privato con la url GIT: in questo caso sarà necessario aver configurato prima su GitHub le proprie credenziali SSH (chiavi pubbliche), sarà ovviamente possibile “scrivere” sul repository remoto Una volta completato, il comando git clone avrà creato nella directory corrente una directory che ospita il clone del repository remoto e avrà creato una connessione chiamata origin che punta al repository remoto originale. Tale connessione verrà usata da altri comandi per inviare e ricevere nuove modifiche dal repository remoto. Alcune utili opzioni da usare con il comando git clone sono clonare in una specifica directory (utile anche per dare uno specifico nome al proprio clone locale: git clone <URL> <DIRECTORY> clonare solo una parte della history dei commit - in questo caso la copia locale è detta shallow clone - per esempio solo gli ultimi 10 commit: git clone --depth=10 <URL> clona solo uno specifico branch presente nel repository remoto: git clone --single-branch --branch <NAME> <URL>

5

Salvare modifiche in Git

5.1

Il comando Git add in Git

Quando si lavora con un version control system come Git, il concetto di “salvare modifiche” ha più sfumature rispetto al salvataggio di un file in un word processor o un editor di file. Abbiamo già visto come in Git l’azione “salva” è realizzabile tramite un commit. Vediamo più nel dettaglio tutti i comandi utili in Git per effettuare un “salvataggio” nella history del nostro progetto.   Git add Il comando git add aggiunge una modifica presente nella working directory alla staging area. È il modo per dire a Git quali particolari modifiche verranno inserite nel prossimo commit. Opzioni comuni: git add <file> - aggiunge tutte le modifiche a un file alla staging area git add <file> - aggiunge tutte le modifiche a una directory alla staging area git add -p - avvia una sessione interattiva che permette di scegliere quali porzioni dei file modificati vanno aggiunte alla staging area La modalità interattiva è particolarmente utile se avete apportato diverse modifiche concettualmente slegate e solo una parte di esse va effettivamente inclusa nel commit. Tale modalità visualizza gruppo di modifiche (chunk) e chiede che venga scelta l’azione da eseguire premendo una lettera (y per aggiungere il chunk, n per ignorarlo, s per dividere ulteriormente il chunk, q per uscire). NOTA un buon flusso di lavoro prevede che ogni commit contenga tutte e solo le modifiche che apportano un determinato cambiamento alla codebase, p.e. l’aggiunta di una feature
5.2

Il comando Git commit in Git

Il comando git commit fissa nella history del progetto uno snapshot dello stato attuale, ovvero una versione “sicura” o “rilevante” del progetto stesso. Le modifiche incluse nel salvataggio sono quelle che sono state esplicitamente incluse nella staging area tramite il comando git add. Se eseguito senza opzioni, viene aperto un editor di testo (configurabile tramite git config) per inserire il messaggio che descrive il commit. Una volta salvato il messaggio, viene creato il commit. Opzioni comuni: git commit -a - esegue il commit di tutte le modifiche presenti nella working directory (in pratica esegue anche il git add di tutti i file sotto controllo di versione che hanno modifiche) git commit -m "commit message" - indica il messaggio di commit, non verrà aperto l’editor di testo git commit -am "commit message" - la combinazione dei due precedenti … in generale tutte le varie opzioni possono essere combinate insieme git commit --author="Jane Doe <[email protected]>" - permette di indicare l’autore delle modifiche, distinguendo tra autore (Jane Doe) ed esecutore del commit (il propio user.name e user.email)   NOTA Git non richiede che il messaggio che descrive il commit segua uno specifico formato, ma esistono alcune pratiche condivise nel formato dei messaggi di commit per rendere più comprensibile la history. In generale la raccomandazione è di usare la prima riga del messaggio per un breve descrizione (come il subject di una email), seguita eventualmente da una riga vuota e da altre righe con maggiori dettagli (come il body di una email).
5.3

Il comando Git diff in Git

git diff è un comando Git multi-uso che permette di visualizzare le differenze tra due data source, che siano essi file, commit, branch oppure altro. In questa sezione vedremo gli utilizzi più tipici di git diff e illustreremo il formato standard con cui sono mostrate tali differenze. Ecco un esempio pratico usando un repository creato da zero: $ mkdir repo_for_diff $ cd repo_for_diff $ touch diff_test.txt $ echo "this is a git diff test example" > diff_test.txt $ git init Initialized empty Git repository $ git add diff_test.txt $ git commit -m "add test file" [master (root-commit) c5dfcdb] add test file 1 file changed, 1 insertion(+) create mode 100644 diff_test.txt Eseguendo ora git diff, non ci sarà alcun output, poiché non ci sono differenze tra il contenuto della working copy e l’ultimo commit (snapshot). Proviamo quindi a cambiare il contenuto del file tracciato nel repository: $ echo "this is a diff example" > diff_test.txt $ git diff diff --git a/diff_test.txt b/diff_test.txt index 6b0c6cf..b37e70a 100644 --- a/diff_test.txt +++ b/diff_test.txt @@ -1 +1 @@ -this is a git diff test example +this is a diff example Come leggere questo output? la prima riga mostra le data source che il comando git diff ha usato, a/diff_test.txt b/diff_test.txt (in questo caso l’ultimo snapshot del file e la versione nella working directory) la seconda riga mostra dei metadati interni di Git, in generale è una informazione che può essere ignorata la terza e quarta riga sono indicano la legenda dei due input di diff, serve per leggere la sezione successiva le ultime righe sono il “chunk” (o la lista dei chunk per quei due file) del diff, in cui nella pratica vengono mostrate solo le righe cambiate tra i file (con la legenda -/+ per indicare quale riga è presente in quale file); il chunk inizia con un head (la riga con @@, che indica un summary delle modifiche, in questo caso una riga cambiata) NOTA nella vita reale, un chunk head potrebbe mostrare qualcosa tipo @@ -34,6 +34,8 @@ che significa “a partire dalla riga 34 del file, sono state rimosse 6 righe e aggiunte 8 Opzioni utili: git diff mostra le differenze tra l’attuale contenuto della working area e l’ultimo commit git diff --staged mostra le differenze tra l’attuale contenuto della staging area e l’ultimo commit git diff ./path/to/file` mostra le differenze solo per il file indicato $ git log --pretty=oneline 957fbc92b123030c389bf8b4b874522bdf2db72c add feature ce489262a1ee34340440e55a0b99ea6918e19e7a rename some classes 6b539f280d8b0ec4874671bae9c6bed80b788006 refactor some code for feature 646e7863348a427e1ed9163a9a96fa759112f102 add some copy to body $ git diff 957fbc92b123030c389bf8b4b874522bdf2db72c ce489262a1ee34340440e55a0b99ea6918e19e7a esegue il confronto tra due commit git diff feature-branch..other-feature-branch esegue il confronto tra due branch; in questo caso, vengono confrontati gli ultimi commit dei due branch (tip) avendo indicato l’operatore “doppio punto” (esistono altri “operatori punto” che permettono di eseguire diff diversi, per esempio con “…” viene confrontato l’ultimo commit comune tra i due branch e il tip del branch indicato a destra
5.4

Il comando Git stash in Git

Il comando git stash serve per mettere da parte tutte le attuali modifiche non committate, per recuperarle in un secondo momento. In pratica, serve in tutte quelle occasioni in cui si sono effettuate una serie di modifiche “WIP” (work in progress), non è ancora il momento di effettuare un commit, ma è necessario tornare tornare temporaneamente a una situazione “pulita”. $ git status On branch master Changes to be committed: modified: style.css Changes not staged for commit: modified: index.html $ git stash Saved working directory and index state WIP on master: c5dfcdb add new logo $ git status On branch master nothing to commit, working tree clean Le modifiche (staged e non) messe da parte sono salvate nello stack degli stash e potranno essere applicate nuovamente in un secondo momento. Nota: lo stash fa parte del proprio repository locale, non sono trasferiti al server remoto. Per utilizzare al meglio gli stash è utile ricordare alcune cose: non si è limitati a salvare un singolo stash, ogni volta che si mette da parte uno stash questo finisce in cima alla pila di stash è possibile recuperare l’ultimo stash salvato togliendo dalla pila con git stash pop oppure lasciandolo nella pila con git stash apply è possibile recuperare uno degli stash precedenti usando il loro indice git stash pop stash@{2} (0 il più recente) nello stash finiscono di default solo le modifiche apportate ai file tracciati, ma è possibile usare l’opzione git stash -u per includere anche i file untracked   $ git stash list stash@{0}: WIP on main: 5002d47 our new homepage stash@{1}: WIP on main: 5002d47 our new homepage stash@{2}: WIP on main: 5002d47 our new homepage La pila degli stash ci permette di sapere rispetto a quale branch e commit è stato creato il singolo stash (nota: la pila degli stash è del repository, quindi si possono trovare stash da branch locali diversi) Alcune azioni comuni lavorando con gli stash: git stash drop stash@{1} - rimuove uno specifico stash (1) git stash clear - elimina tutti gli stash git stash show -p - mostra il diff di uno stash (nota: rispetto al commit/branch da cui è stato “creato”, non rispetto alla attuale working copy)
5.5

.gitignore : i file ignored in Git

Abbiamo visto in precedenza che Git considera i file presenti nella working copy distinguendoli in tracked (file che sono stati committati nel repository o inclusi nella staging area) e untracked (l’opposto del precedente). A questi due gruppi possiamo aggiungerne un terzo, i file ignored, ossia i file per i quali Git è stato espressamente istruito di escluderli dal controllo di versione. I file ignored sono tipicamente artefatti di build o altri file generati che possono essere ricreati a partire dai sorgenti del repository, oppure file che contengono contenuti che non devono essere inclusi nei commit per altri motivi (per esempio password, chiavi di autenticazione o altri secret). Per indicare quali file ignorare viene utilizzato il file speciale .gitignore, solitamente presente nella directory iniziale del progetto. È comunque possibile salvare un file .gitignore in ogni sottodirectory, che integra o sostituisce le regole del file “genitore” in quella directory e nelle sue “figlie”. Non esiste un comando specifico di Git, quindi sarà necessario modificare il contenuto di tale file con un editor di testo. Ovviamente, è opportuno aggiungere e committare il file .gitignore nel repository, in modo che ne sia parte integrante. Il contenuto esatto del file .gitignore dipende dalla natura del progetto. Esistono raccolte di template  con file specifici in base al linguaggio di programmazione e al framework utilizzato. Per esempio, un template per un progetto in linguaggio PHP con framework Laravel è: /vendor/ node_modules/ npm-debug.log yarn-error.log # Laravel 4 specific bootstrap/compiled.php app/storage/ # Laravel 5 & Lumen specific public/storage public/hot # Laravel 5 & Lumen specific with changed public path public_html/storage public_html/hot storage/*.key .env Homestead.yaml Homestead.json /.vagrant .phpunit.result.cache Le righe che iniziano con # sono commenti. Per quanto riguarda le altre, su ciascuna riga è indicato un pattern da ignorare (o il percorso esatto di un file). In base al pattern indicato, verranno ignorati specifici file nella directory, ad esempio: /logs - ignora il file o directory logs nella directory corrente (quindi /logs ma non /example/logs logs - ignora i file e le directory con nome logs nella directory corrente e nelle sottodirectory (quindi /logs, /example/logs, /debug/logs/debug.log, ma non /build/log/today logs/ - ignora solo le directory con nome logs (quindi la directory /build/logs e tutti i file contenuti ma non il file debug/logs) È anche possibile indicare quali file non ignorare se un altro pattern dovesse indicare il contrario tramite un !. Ad esempio # ignora tutti i file con estensione .log *.log # NON ignorare i "debug.log" !debug.log È buona pratica creare un file .gitignore globale che vale per tutti i repository Git presenti sulla propria macchina. In tale file possono essere indicati i pattern che vanno ignorati in ogni progetto, per esempio i file creati dal proprio sistema operativo (come .DS_Store and thumbs.db) o le directory create dal proprio IDE/Editor. touch ~/.gitignore git config --global core.excludesFile ~/.gitignore Con questi comandi creiamo un file .gitignore nella propria directory home e istruiamo Git a usarlo come file di ignore globale. È possibile aggiungere ai file ignorati un file che è stato in passato aggiunto al repository, tramite i seguenti comandi: $ echo debug.log >> .gitignore $ git rm --cached debug.log rm 'debug.log' $ git commit -m "rimosso debug.log dal repository" Attenzione però: in tutti i commit precedenti il file resterà ovviamente disponibile (Git conserva la history).

6

Interrogare lo stato in Git

6.1

Il comando Git status in Git

Il comando git status mostra lo stato attuale della working directory e della staging area, permettendo, quindi, di vedere quali file modificati sono stati aggiunti alla staging area, quali file modificati non sono stati aggiunti alla staging area e quali file non sono tracciati nel repository. L’output di git status è uno dei più ricchi di informazioni tra i comandi di Git. Offre, infatti, indicazioni su quali altri comandi utilizzare per spostare le modifiche tra i vari stati: $ git status On branch main Your branch is up to date with 'origin/main'. Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: 001-working-locally/02-save-changes.md Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: 001-working-locally/02-save-changes.md modified: 001-working-locally/03-query-status.md Untracked files: (use "git add <file>..." to include in what will be committed) preview.html In questo caso ci dice che: alcune modifiche al file 02-save-changes.md sono nella staging area alcune modifiche allo stesso file 02-save-changes.md sono solo nella working area tuttle le mofifiche al file 03-query-status.md sono nella working area il file preview.html è ancora untracked Vedremo in seguito come git status ci permette di sapere lo stato di avanzamento di alcune operazioni avanzate come il rebase, sempre fornendo il comando da eseguire per procedere o tornare indietro.
6.2

Il comando Git tag in Git

Git permette di aggiungere un tag a uno specifico commit, in modo da poter usare tale tag come riferimento per uno specifico punto nella history del repository. Utilizzo tipico dei tag è quello di contrassegnare un determinato rilascio o versione, per poter in un secondo momento accedervi con semplicità. A differenza dei branch, infatti, una volta creato un tag non vengono “aggiunti” i nuovi successivi commit al tag. Il comando che permette di gestire i tag Git è git tag, vediamone alcuni usi. git tag <TAG_NAME> Associa al commit attualmente estratto nella working copy il tag con nome TAG_NAME. Non avendo specificato alcuna opzione, Git creerà un tag lightweight, ovvero un tag senza particolari metadati aggiunti (ad esempio. chi ha creato il tag). I tag lightweight sono essenzialmente dei “segnalibri” per singoli commit, è buona pratica non usarli per contrassegnare rilasci pubblici, ma usarli solo localmente per contrassegnare particolari commit ritenuti importanti in fase di sviluppo. git tag -a <TAG_NAME> Crea un tag annotated, cioè oltre al nome fornito per il tag Git salverà nel repository metadati aggiuntivi come nome ed email di chi ha creato il tag, data, … Nel momento in cui si crea un tag annotated, come per i commit, viene richiesto un messaggio, che può essere fornito direttamente con il comando tramite l’opzione -m "messaggio del tag" $ git tag 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.1-alpha ... $ git tag -l "*beta*" v1.1.1-beta v1.1.1-beta2 v1.1.1-beta3 Se eseguito senza alcuna opzione, il comando git tag mostra i tag attualmente presenti nel repository. È possibile filtrare solo i tag contenenti una determinata espressione usando l’opzione -l e una espressione con wildcard. $ git tag -a v1.2.0 0d52aaab4479697da7686c15f77a3d64d9165190 Aggiungendo l’id di un commit al comando usato per creare un tag, il tag verrà applicato al commit indicato invece che a quello corrente. $ git tag -d <TAG_NAME> L’opzione -d permette di rimuovere uno specifico tag esistente. $ git tag v1 v1.0.0 $ git add package.json $ git commit -m "release version 1.1.0" $ git tag v1.1.0 -m "tagging version 1.1.0" $ git tag -f v1 -m "moving tag v1 to latest release" È ovviamente possibile avere più tag per ogni commit, così come, nel caso sia necessario, “spostare” un tag esistente da un commit a un’altro. Nei comandi indicati nell’esempio qui sopra supponiamo infatti che esista un commit che è stato precedentemente taggato con i tag v1.0.0 (la versione esatta) e un generico v1 (ovvero un tag che indica l’ultimo rilascio della versione “1” del progetto). Nel momento in cui rilasciamo la versione 1.1.0 possiamo creare un nuovo tag sul nuovo commit per la versione specifica v1.1.0 e forzare l’update del tag esistente v1 in modo che punti anch’esso all’ultimo commit appena creato. Notare che queste convenzioni sono di progetto e non di Git. Un’ ultima annotazione: come ogni altro riferimento a commit nella history, Git permette di usare il nome di un tag in molti altri comandi, ad esempio: git checkout v1.3.0 - esegue il checkout del commit taggato come v1.3.0 git diff v1.1.1-beta..v1.1.1-beta2 mostra il diff tra due tag esistenti
6.3

Il comando Git blame in Git

A concludere l’elenco dei comandi utili in Git per interrogare lo stato e la storia di un repository troviamo il comando git blame La traduzione letterale del verbo “to blame” è “incolpare” e infatti questo comando git viene usato per rispondere alla domanda “chi ha cambiato questa riga???”. È ovviamente utile nel caso di più persone che concorrono allo sviluppo di una codebase. Vediamone subito un esempio: $ git blame gradle.properties 2a0629fa2 (John Doe 2021-03-04 19:55:36 +0100 1) kotlin.code.style=official f9fba3a0e (Jane Foo 2021-05-27 14:13:45 +0200 2) ## for local development, set GRADLE_OPTS in pipeline to change f9fba3a0e (Jane Foo 2021-05-27 14:13:45 +0200 3) org.gradle.caching=true f9fba3a0e (Jane Foo 2021-05-27 14:13:45 +0200 4) org.gradle.daemon=true 8692fb277 (John Doe 2022-03-08 09:15:06 +0100 5) 77e32fc3b (Jane Doe 2022-11-07 18:45:22 +0100 6) ktor_version=2.1.3 Innanzitutto, il comando git blame richiede che venga passato al comando il nome di un file. Di tale file vengono stampate tutte righe, ognuna con indicato l’id del commit che ha modificato quella riga, il rispettivo autore del commit la data del commit il numero della riga il contenuto della riga È possibile indicare alcune opzioni aggiuntive per evidenziare alcuni tipi di modifica (-w per ignorare gli spazi, -M per ignorare le righe spostate all’interno del file e -C per rilevare righe spostate o copiate da altri file). In generale, comunque, va ricordato che: git blame e git log possono essere usati per trovare l’autore di una determinata modifica, la scelta dell’uno o dell’altro dipende da cosa si sta cercando git blame mostra solo chi ha “toccato” per ultimo una certa riga di un file, non dice cosa ha cambiato (per cui la persona che git blame incolpa potrebbe non essere quella che ha introdotto un bug)
6.4

il comando Git log in Git

Il comando git log permette di visualizzare la cronologia dei commit, filtrarla e cercare per specifiche modifiche. Così come git status opera sul working directory e staging area, git log opera sulla history dei commit. git log offre moltissime opzioni per personalizzare la visualizzazione dei commit e per filtrare. Qui di seguito alcune delle opzioni e configurazioni più utili. git log mostra l’intera cronologia usando la formattazione di default formata dal commit ID (o SHA), l’autore, la data e il messaggio completo di commit. È possibile premere spazio per scorrere e q per uscire. git log -n <LIMITE> Mostra solo il numero indicato di commit, ovviamente i più recenti git log --oneline Condensa ogni commit in una singola riga, utile per avere una panoramica delle modifiche git log --stat Aggiunge informazioni su quali file sono cambiati per ogni commit e il numero di rihe aggiunte/modificate git log -p Mostra le effettive differenze apportate per ogni file di ogni commit. git log --author="<pattern>" Filtra i commit, mostrando solo quelli che includono <pattern> nella email dell’autore del commit. git log --grep="<pattern>" Filtra i commit, mostrando solo quelli che includono <pattern> nel testo del messaggio di commit. git log <file> Filtra i commit, mostrando solo quelli che hanno apportato modifiche al file <file> git log <inizio>..<fine> Filtra i commit, mostrando solo quelli tra <inizio> e <fine> (notare l’operatore ..). <inizio> e <fine> possono essere un commit ID, un nome di branch, un nome di tag o qualunque altro riferimento di revision. git log --graph --decorate --oneline Versione “avanzata” per visualizzare una panoramica più completa di un repository. L’opzione --graph include delle “linee” che permettono di vedere le varie ramificazioni dei commit, mentre --decorate aggiunge nomi di branch e tag sui singoli commit. Le opzioni offerte da git log sono molteplici, tutto dipende da cosa state cercando nella history del progetto. Per esempio, se voleste sapere quali sono stati i vostri commit negli ultimi 5 giorni sul repository, potreste provare un git log --since '3 days ago' --oneline --author $(git config user.email)

7

Annullare modifiche in Git

7.1

Il comando Git revert in Git

Il comando git revert in Git permette di “rimuovere” dal repository una modifica indesiderata. Poiché, però, Git è pensato per preservare la history di un progetto e l’integrità della cronologia delle revisioni, il comando di revert opera aggiungendo un nuovo commit che è l’esatto opposto del commit che si vuole rimuovere. Supponiamo di avere un progetto con la seguente history: $ git log --oneline * 1e6ed4a (HEAD -> master) add second feature * cc7f96f improve performance on first feature * 1e1bf33 add first feature * f6a76e4 initialize project Ci accorgiamo solo dopo aver implementato la seconda funzionalità del nostro progetto che in realtà il commit con cui abbiamo provato a migliorare le performance ha introdotto un bug in certe condizioni. Vogliamo, quindi, rilasciare una nuova versione senza quell’errore. In questo caso, se le modifiche effettivamente apportate lo permettono, possiamo eseguire un revert del commit contenente l’errore: $ git revert cc7f96f $ git log --oneline * 517c168 (HEAD -> master) Revert "improve performance on first feature" * 1e6ed4a add second feature * cc7f96f improve performance on first feature * 1e1bf33 add first feature * f6a76e4 initialize project Git prende il contenuto del commit indicato nel git revert e lo “inverte”, chiedendo, poi, di indicare solo il messaggio con cui salverà nella history il commit “inverso”
7.2

Il comando Git rm in Git

Il comando git rm in Git  serve per istruire Git a rimuovere un file dal tracciamento; è, in pratica, il comando opposto e complementare a git add. Ovviamente, in virtù del funzionamento di Git, sarà sempre possibile accedere alle varie versioni del file facendo checkout dei vari commit in cui quel file era ancora incluso tra quelli inclusi nel repository. Così come accade per il comando git add, una volta istruito Git a rimuovere un file con git rm, sarà necessario eseguire git commit per salvare tale rimozione nella history. Per comodità, git rm si occupa anche di rimuovere il file dalla working directory. L’utilizzo del comando è estremamente semplice: git rm [OPTIONS] <FILE...> dove FILE è un file, una directory o un elenco di file/directory. Alcune opzioni utili, a seconda degli intenti e casi pratici, sono: --cached - mantiene il file nella working directory come untracked --recursive - in caso di directory, rimuove anche le eventuali sottodirectory --force - Git può effettuare git rm solo per file per i quali non ci siano modifiche (working o staged) rispetto all’ultimo commit; con --force viene saltato questo controllo Nota aggiuntiva: poiché git rm opera sul contenuto della staging area, finché non si esegue il commit è sempre possibile annullare l’effetto di git rm tramite git reset o git checkou
7.3

Il comando Git reset in Git

Similmente al precedente, il comando git reset in Git permette di annullare commit di un repository, ma lo fa in maniera distruttiva. In particolare, git reset può operare sulla history dei commit, quindi permette di alterare la history di un determinato branch. È un comando da usare con attenzione se non si vuole perdere parte del proprio lavoro. Infatti, il comando è descritto ufficialmente come git-reset - Reset current HEAD to the specified state e in base all’effettivo utilizzo permette di effettuare il “reset” di diverse situazioni. Infatti, oltre che sulla history dei commit, ha effetto anche sulla staging area e sulla working directory a seconda della modalità con cui viene eseguito (soft, mixed o hard). La versione estesa del comando è la seguente: git reset [--soft|--mixed|--hard] [<REFERENCE>] in particolare, se non indicati, viene usata di default la modalità mixed e viene usata la HEAD attuale come reference. Le tre modalità differiscono nel seguente modo: soft - non tocca le modifiche in staging area, non tocca le modifiche nella working directory mixed - togliere le modifiche dalla staging aree e le riporta nella working directory, non tocca le altre eventuali modifiche nella working directory hard - annulla le modifiche in staging area, annulla le modifiche in working directory (cioè rimuove per sempre ogni modifica non salvata come commit) $ git log --oneline * 517c168 (HEAD -> master) Revert "improve performance on first feature" * 1e6ed4a add second feature * cc7f96f improve performance on first feature * 1e1bf33 add first feature * f6a76e4 initialize project $ git add second.py $ git status --short M first.py M second.py Se supponiamo che il nostro repository ha una certa history e che abbiamo alcune modifiche in corso, sia nella working directory che nella staging area, e che non siamo in uno stato “detached”, indicando solo la modalità avremmo come risultato che: soft -> praticamente invariato mixed -> pulisce la staging area hard -> rimuove tutte le modifiche e torna allineato con HEAD Se però indicassimo come reference a cui fare reset uno dei commit: $ git reset --hard cc7f96f HEAD is now at cc7f96f improve performance on first feature $ git log --oneline cc7f96f (HEAD -> master) improve performance on first feature 1e1bf33 add first feature f6a76e4 initialize project $ git status --short ?? second.py In questo caso, sono stati rimossi dalla history locale del branch tutti i commit successivi a quello indicato nel comando git reset. Il file second.py è tornato ad essere untracked, poiché non esisteva in quella versione del progetto. In pratica, siamo passati da questa situazione: [a]<--[b]<--[c]<--[d]<--[e]::{main} | HEAD a una situazione in cui i commit successivi a quello a cui si è fatto il reset non fanno più parte del branch: [d]<--[e] [a]<--[b]<--[c]::{main} | HEAD A cosa può essere utile il comando git reset in Git? In generale, è utile ad annullare modifiche locali che non sono mai state pubblicate su una origin condivisa. Rimuovere un commit da un repository su cui altri web developers possono aver continuato a sviluppare crea seri problemi alla collaborazione. I casi d’uso possibile possono, quindi, essere i seguenti: git reset --hard: rimuove ogni propria modifica e riporta la working directory in sync con l’ultimo commit git reset: “pulisce” la staging area, mantenendo ogni differenza con l’ultimo commit nella working directory git reset <FILE>: come sopra, ma solo per uno specifico file e non per tutte le modifiche git reset --hard <COMMIT>: nel caso in cui ci accorgessimo che a partire da un certo commit in poi, e solo nel caso in cui tali commit sono rimasti sul proprio clone locale, il lavoro è completamente da scartare senza remore, riporta il progetto a tale commit “buono”, buttando via il resto

8

Riscrivere la history in Git

8.1

L'opzione Git commit –amend in Git

Abbiamo detto più volte che lo scopo principale di Git è quello di evitare la perdita in una modifica committata (cioè salvata nella history). D’altro canto Git è anche pensato per offrire il controllo più completo sul proprio workflow di sviluppo, ivi inclusa la possibilità di definire esattamente la history del proprio progetto. Prendiamo,ad esempio, questa cronologia di commit di un repository: $ git log --oneline 5dd0865 make variable names more clear 35dec0e increase version af75ac9 update indentation for checks from linter edb709f profile mostly works, missing phone number b2b95ad wip - unfinished 447cbfe (tag: v0.1.0) chore: release 0.1.0 67b2fd8 feat: implement login 66c8a99 feat: implement signup 8305e6c chore: init project I commit successivi a quello con tag v0.1.0 sono stati fatti man mano che parti del lavoro venivano completate, per non perdere alcuni avanzamenti funzionanti. A distanza di mesi o anni, non tutti quei commit sarebbero interessanti da mantenere come “istantanea” del progetto. Per situazioni come queste, Git offre dei comandi per poter riscrivere la history, rendendo però esplicito che l’uso di tali comandi può portare alla perdita di contenuto (anche se, in verità molto rara e molto raramente dovuta a Git stesso). Git offre due possibili “ambiti” di riscrittura della history: l’ultimo commit appena creato (tramite l’opzione --amend del comando git commit) oppure uno qualsiasi dei commit nella history (tramite il comando git amend) Scopriamo come rendere la history d’esempio di sopra qualcosa di simile al seguente: $ git log --oneline 9b23ede (tag: v0.2.0) chore: release 0.2.2 7a250a0 feat: implement profile page 447cbfe (tag: v0.1.0) chore: release 0.1.0 67b2fd8 feat: implement login 66c8a99 feat: implement signup 8305e6c chore: init project Git commit –amend in Git L’opzione --amend del comando git commit in Git è una scorciatoia che ci permette di unire nuove modifiche presenti nella staging area all’ultimo commit appena salvato. In linea con la filosofia di Git, le nuove modifiche non vengono “aggiunte” al commit esistente, ma viene creato un nuovo commit che include tutte le modifiche. [...] $ git commit -m "implement send email button" $ git log -n 2 --oneline cc7f96f (HEAD -> main) implement new feature a3478b1 fix a bug in old feature $ git add public/icons/send_email_button.png $ git commit --amend --no-edit $ git log -n 2 --oneline e34a234 (HEAD -> main) implement new feature a3478b1 fix a bug in old feature Nella sequenza di comandi qui sopra è stato incluso un file all’ultimo commit effettuato. Il file da includere è stato aggiunto alla staging area (git add) e poi è stato eseguito git commit --amend. In questa situazione, in cui si è semplicemente dimenticato di includere un file in un commit (l’intero file o modifiche a file già sotto controllo di versione), è d’aiuto anche l’opzione --no-edit che istruisce Git a mantenere l’esistente messaggio di commit. Notare l’output di git log prima e dopo dell’amend: l’id dell’ultimo commit è cambiato, per Git si tratta di una nuovo snapshot che sostituisce il precedente. Se non si indica l’opzione --no-edit, il comando git commit --amend può anche essere utile per correggere eventuali errori o mancanza nel solo messaggio di commit.
8.2

Git rebase –interactive in Git

Una doverosa precisazione: l’uso del comando git rebase in Git che vedremo in questa sezione sarà focalizzato solo sulla possibilità offerta di modificare uno o più commit più vecchi dell’ultimo salvato, ovvero l’esecuzione in modalità interactive. Vedremo la modalità standard git rebase (e il suo vero significato) in un altro capitolo. Come suggerisce il nome, la modalità interactive richiede di interagire con il comando durante la sua esecuzione, per istruirlo sul da farsi. Per prima cosa è necessario individuare il commit o il gruppo di commit da modificare nelle history, in particolare “quanti commit fa” è il primo commit che si vuole cambiare. Nell’esempio di history mostrato a inizio della sezione, vorremmo poter cambiare i commit a partire dal quinto più vecchio (35dec0e trying adding profile page). In questo caso si userà il comando git rebase --interactive HEAD~5 In cui la dicitura HEAD~5 istruisce Git a prepararsi per modificare uno o tutti gli ultimi 5 commit. Nel momento in cui viene avviato il procedimento interattivo di git rebase, viene aperto l’editor di default con un contenuto simile al seguente: pick b2b95ad wip - unfinished pick edb709f profile mostly works, missing phone number pick af75ac9 update indentation for checks from linter pick 35dec0e increase version pick 5dd0865 make variable names more clear # # Commands: # p, pick <commit> = use commit # r, reword <commit> = use commit, but edit the commit message # e, edit <commit> = use commit, but stop for amending # s, squash <commit> = use commit, but meld into previous commit [...] Nella prima parte è presente l’elenco dei commit selezionati, dal più vecchio al più recente - quindi in ordine inverso rispetto a git log - ognuno dei quali è preceduto da comando di rebase, di default pick. Ciascuna riga è così composta: <COMANDO> <COMMIT_ID> <COMMIT_MESSAGE>. Solo i primi due sono necessari a git rebase, il messaggio di commit serve a chi andrà a chiedere quali modifiche fare per avere un riferimento del contenuto del commit. Nella seconda parte è riportata la spiegazione dei vari comandi di rebase sotto forma di commento. Salvando e chiudendo questo testo in modo analogo a quanto succede con la scrittura del messaggio di commit, git rebase proverà ad applicare a ciascuno dei 5 commit selezionati il comando fornito. Se non si applicano modifiche, git rebase si limiterà a ri-applicare le stesse modifiche. In pratica quindi non farà nulla. Nel caso del nostro esempio, il nostro intento era quello di ottenere due commit: uno con tutte le modifiche necessarie a implementare una certa feature e uno con le sole modifiche che salvano nel progetto la nuova versione. In base al nostro intento e al contenuto dei commit effettivamente a disposizione, le indicazioni da dare a git rebase cambieranno quindi di volta in volta. Proviamo a delineare come procedere. Nel nostro caso vogliamo riunire insieme tutti i commit che cambiano il codice sorgente per implementare una certa funzione e cambiare messaggio di commit al commit che cambia solo la versione del progetto, lasciando questo come commit finale. In tale caso, possiamo istruire git rebase ad applicare i seguenti comandi: pick b2b95ad wip - unfinished squash edb709f profile mostly works, missing phone number squash af75ac9 update indentation for checks from linter squash 5dd0865 make variable names more clear edit 35dec0e increase version Per poter unire dei commit insieme è necessario prendere il più vecchio (pick) e istruire git rebase a “schiacciare insieme” (squash) quello immediatamente successivo. Le prime 4 righe riportate qui sopra istruiscono, quindi, git rebase a unire quattro in un solo commit. L’ultima riga dice di mantenere il contenuto del commit così com'è, ma di cambiarne il messaggio (edit). Una cosa importante da notare: per poter unire insieme tutti i commit che andavano a fare modifiche, abbiamo cambiato l’ordine con cui i commit iniziali comparivano nella history del progetto (il commit 5dd0865 e 35dec0e sono stati invertiti nell’ordine di applicazione dei comandi). Non sempre è possibile invertire l’ordine di applicazione dei commit, ma in questo nostro caso ipotetico era possibile farlo perché i due commit di partenza apportavano modifiche a file diversi. Torneremo su questa tematica più avanti. Salvando e chiudendo il file, git rebase procederà ad applicare i comandi ricevuti fino a che non sarà necessario l’intervento dell’utente. La prima richiesta sarà quella di fornire un nuovo messaggio di commit per i commit “uniti”. # This is a combination of 4 commits. # This is the 1st commit message: wip - unfinished # This is the commit message #2: profile mostly works, missing phone number # This is the commit message #3: update indentation for checks from linter # This is the commit message #4: make variable names more clear # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. Vengono riportati, per convenienza, i messaggi dei commit che si stanno unendo, ma è possibile cancellare tutto e scriverne uno nuovo da zero. Una volta scritto il messaggio desiderato, è sufficiente salvare ed uscire dall’editor per continuare. La seconda richiesta di intervento arriverà nel gestire l’ultimo commit che faceva parte del rebase. In questo caso, avendo scelto edit, git rebase non vi chiederà semplicemente di inserire il nuovo messaggio di commit, ma si farà temporaneamente da parte lasciando spazio alla esecuzione di altri comandi. $ git rebase --interactive HEAD~5 Stopped at 35dec0e... increase version You can amend the commit now, with git commit --amend Once you're satisfied with your changes, run git rebase --continue $ In questo caso, le istruzioni fornite dicono esattamente cosa fare: eseguire un git commit --amend che ci permette di cambiare il messaggio del commit, seguito poi da git rebase --continue per procedere nell’esecuzione del rebase. Se tutto sarà andato a buon fine, git rebase vi premierà con un incoraggiante messaggio rebase successful. Se qualcosa dovesse andare storto, nel momento in cui il processo si interrompe per avvisare dell’impossibilità di procedere, potrebbero essere riportare a schermo istruzioni utili a risolvere il problema. In questi frangenti, è comunque possibile eseguire un git rebase --abort per annullare l’esecuzione del rebase. Nel momento in cui l’esecuzione del rebase va a buon fine, i commit modificati sono persi, sostituiti dai nuovi nella history. Per cominciare a prendere confidenza con git rebase e la sua modalità interactive, il consiglio è quello di provare a creare piccoli commit “indipendenti”, sui quali poi fare qualche rebase per unirli, cambiarne ordine, e simili.  git rebase è uno dei comandi più “potenti” di Git, poiché permette di modificare la history del repository. Padroneggiarne al meglio l’ utilizzo potrebbe non essere immediato, ma a rendere le cose più semplici o più difficili concorre anche l’esatto contenuto delle cronologia dei commit da cambiare/riorganizzare/rifare/…  È sempre importante fare attenzione a quali modifiche andranno a far parte di un commit (e in quale ordine), perché anche da questo dipende la possibilità futura di riorganizzare la history delle proprie modifiche prima di renderle pubbliche.

9

Scorciatoie per comandi frequenti in Git

9.1

Scorciatoie per comandi frequenti in Git

I comandi Git che abbiamo presentato finora costituiscono già un insieme non banale di azioni che è possibile fare lavorando con un repository Git.  In questa sezione, sposteremo l’attenzione su quelle funzioni di Git per rendere più semplice l’interazione quotidiana con i suoi numerosi possibili comandi. In particolare, abbiamo visto visto che alcuni comandi possono essere formati da molte opzioni (come per esempio le varie possibili opzioni del comando git log). Molto spesso, nell’utilizzo quotidiano di Git, gli stessi comandi vengono ripetuti più e più volte, e spesso con le stesse opzioni. Dover ripetere ogni volta la stessa lunga sequenza di opzioni può diventare particolarmente fastidioso, per questo Git mette a disposizione la possibilità di creare degli alias. Proviamo subito un esempio: $ git st git: 'st' is not a git command. See 'git --help'. $ git config --global alias.st status $ git st On branch main Your branch is ahead of 'origin/main' by 1 commit. (use "git push" to publish your local commits) Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: server.php Cosa abbiamo fatto? Tramite git config abbiamo registrato un nuovo alias globale (cioè disponibile in tutti i repository locali). Tale alias si presenta come un nuovo comando, ma nella pratica ha lo stesso effetto di un git status. Gli alias di Git hanno due caratteristiche importanti: possono includere oltre al comando anche le rispettive azioni permettono comunque di aggiungere altre opzioni ammesse dal comando $ git st --short M server.php $ git config --global alias.sts status --short --branch $ git sts # main...origin/main [ahead 1] M server.php ?? removeme.txt Esistono alcune collezioni di alias, ma è anche utile creare i propri alias che soddisfano le proprie necessità di tutti i giorni. In fondo gli alias servono per evitare di digitare troppi caratteri ogni volta che si esegue uno specifico e frequente comando Git. Un buon set di partenza di scorciatoie, incluse o suggerite in quasi tutte le raccolte o gli articoli online, è il seguente: git config --global alias.co checkout git config --global alias.br branch git config --global alias.ci commit git config --global alias.st status git config --global alias.df diff git config --global alias.diff --cached git config --global alias.lg log -p git config --global alias.undo=reset --soft HEAD

10

Introduzione ai repository "remote" in Git

10.1

Repository condiviso in Git

Nella sezione introduttiva abbiamo detto che uno dei vantaggi di Git è quello di proporre un modello di collaborazione distribuito. Abbiamo anche già spiegato come “recuperare” un progetto preesistente ospitato su un server remoto tramite il comando git clone. Entriamo più nel dettaglio nell’uso di un repository remoto da parte di più collaboratori, partendo dal modello più semplice, quello del “repository condiviso”. Repository condiviso   La modalità di collaborazione più semplice realizzabile tramite Git è quella che è indicata come “modello shared repository”. Nella pratica, poiché un repository Git contiene l’intera cronologia del progetto, in questa modalità esiste un repository condiviso collocato su un computer remoto raggiungibile da tutti i collaboratori che viene usato per recuperare o inviare nuovi commit. I singoli web developers collaboratori recuperano sui propri computer una copia locale del repository remoto condiviso tramite il comando git clone. I commit aggiunti dai singoli collaboratori sulle proprie copie locali possono essere inviati alla copia remota condivisa tramite il comando git push. I commit aggiunti da altri collaboratori alla copia remota condivisa possono essere recuperati e riportati nella propria copia locale tramite i comandi git fetch e git pull. In questo modo il repository remoto condiviso agisce da “master copy” (la versione originale di un contenuto, per esempio un album musicale o un film da cui vengono realizzate copie, NdR) della history del repository, ovvero è la history originale e ufficiale del repository a cui tutti possono attingere.   History originale   Ovviamente, nelle operazioni di invio e recupero di nuovi commit, entra in gioco l’altra caratteristica fondamentale di Git, cioè la salvaguardia delle modifiche delle history. Nel caso rappresentato dell’immagine qui sopra due collaboratori hanno aggiunto entrambi un commit (rispettivamente D ed E) alla stessa sequenza di commit (A-B-C) recuperata da un repository remoto. In tale situazione, la history dei due repository locali sono “localmente” corrette. Entrambi sanno di avere un commit in più rispetto al repository remote ed entrambi possono potenzialmente inviare il proprio commit in più al repository remoto. Nel momento in cui uno dei due collaboratori invia il suo commit al repository remoto originale, la situazione cambia.   You shall not push   Una volta che il commit del collaboratore col cilindro (E) è entrato a far parte del repository remoto originale, non sarà più possibile all’altro collaboratore inviare il proprio commit (D) al repository remoto. Per Git infatti un commit non è composto solo dalle modifiche apportate ai file che fanno parte del progetto, ma è uno snapshot all’interno di una sequenza di snapshot. Il collaboratore senza cilindro ha quindi due opzioni: recupera dal repository remoto la history aggiornata (A-B-C-E), aggiunge a questa il suo commit e poi invia al repository remoto forza l’invio della sua history (A-B-C-D) al repository remoto, sovrascrivendo e quindi cancellando ogni traccia del commit E dal repository remoto La seconda opzione è, ovviamente, quella da non fare in situazioni normali. Git permette la sovrascrittura della history, ma va fatta solo quanto veramente e cioè per correggere qualcosa, non per vanificare il lavoro di altri. Quanto alla prima opzione, esistono diverse situazioni possibili nel momento in cui si recupera la history remota aggiornata e si riporta sulla propria copia locale che vedremo nelle successive sezioni. Una annotazione finale: ovviamente il repository remoto condiviso è usato anche per tenere sincronizzati branch e tag  
10.2

Il comando Git remote in Git

Quelle descritte nei capitoli precedenti, ed altre modalità di collaborazione sono possibili grazie ai tracked repository (o remote repository) che è possibile collegare a ogni repository Git tramite il comando git remote. Il comando git remote in git, nella realtà, non si occupa di effettuare l’effettivo acceso o sync a tali repository remoti, ma gestisce quelli che potremmo definire dei segnalibri alle URL di tali repository. $ git remote -v origin https://github.com/developer/project-fork.git (fetch) origin https://github.com/developer/project-fork.git (push) upstream https://github.com/company/project.git (fetch) upstream https://github.com/company/project.git (push) Tali segnalibri possono avere qualsiasi nome, ma per convenzione remote indicato con il nome origin è quello “originale”. Da notare che quando si crea un nuovo repository locale tramite il comando git clone, viene automaticamente creato il remote origin che punta al repository da cui si è effettuato il clone. Ci sono diversi sotto-comandi che è possibile usare per gestire i tracked repository: $ git remote origin $ git remote add john http://dev.example.com/john.git $ git remote -v origin http://dev.example.com/john.git (fetch) origin http://dev.example.com/john.git (push) john http://dev.example.com/john.git (fetch) john http://dev.example.com/john.git (push) $ git remote rename john same-origin $ git remote -v origin http://dev.example.com/john.git (fetch) origin http://dev.example.com/john.git (push) same-origin http://dev.example.com/john.git (fetch) same-origin http://dev.example.com/john.git (push) $ git remote rm same-origin
10.3

I principali repository remote di Git: Github, Gitlab e Bitbucket

Dopo aver esplorato i modelli di collaborazione e di sviluppo condiviso in Git, vediamo quali sono i suoi principali repository online.   GitHub cos’è? Per essere molto chiari su cosa sia esattamente GitHub, si tratta di un servizio di condivisione di file o codice per collaborare con diverse persone.  GitHub è un software molto utilizzato per il controllo delle versioni. È utile quando più di una persona lavora a un progetto. Ad esempio, un team di web developers vuole costruire un sito web e ognuno deve aggiornare i propri codici contemporaneamente mentre lavora al progetto. In questo caso, Github li aiuta a creare un repository centralizzato dove tutti possono caricare, modificare e gestire i file di codice.   Perché Github è così popolare? GitHub ha diversi vantaggi, ma spesso molti hanno il dubbio che non si possa usare dropbox o qualsiasi altro sistema basato sul cloud. Per rispondere a questa domanda, riprendiamo lo stesso esempio. Supponiamo che più di due web developers stiano lavorando allo stesso file e vogliano aggiornarlo contemporaneamente. Purtroppo, la persona che salva il file per prima avrà la precedenza sugli altri. In Github, invece, non è così. Github documenta le modifiche e le riflette in modo organizzato per evitare il caos tra i file caricati. Pertanto, utilizzando il repository centralizzato di GitHub, si evita tutta la confusione e lavorare sullo stesso codice diventa molto semplice.  GitHub è un repository centrale e Git è uno strumento che permette di creare un repository locale. Di solito le persone si confondono tra git e GitHub ma, in realtà, sono due cose molto diverse. Git è uno strumento di controllo delle versioni che consente di eseguire tutti i tipi di operazioni per recuperare i dati dal server centrale o per inviarli ad esso, mentre GitHub è una piattaforma di hosting per la collaborazione nel controllo delle versioni. GitHub è una società che consente di ospitare un repository centrale in un server remoto.   Proviamo a pensare ai modi in cui GitHub semplifica Git:  GitHub fornisce una bella interfaccia visiva che aiuta a tenere traccia o a gestire i progetti a controllo di versione a livello locale. Una volta registrati su GitHub, è possibile connettersi con i social network e costruire un profilo solido. Ma come funziona Github? Scopriamolo insieme.   GitHub come funziona? Vediamo, adesso, come poter utilizzare Github e cosa possiamo fare con questa piattaforma.   Come creare un repository GitHub? Un repository è uno spazio di archiviazione in cui risiede il tuo progetto. Può essere locale, in una cartella del computer, oppure può essere uno spazio di archiviazione su GitHub o su un altro host online. In un repository si possono conservare file di codice, file di testo, immagini o qualsiasi altro tipo di file. È necessario un repository GitHub quando si sono apportate delle modifiche e sono pronte per essere caricate. Questo repository GitHub funge da repository remoto. ìSe ti stai chiedendo come creare un repository su Github, ti basterà seguire questi semplici passaggi:    Vai al link: https://github.com/ . Compila il modulo di iscrizione e fare clic su "Iscriviti a Github". Clicca su "Avvia un nuovo progetto". Ora, avrai sicuramente notato che come impostazione predefinita un repository GitHub è pubblico, il che significa che chiunque può visualizzare i contenuti di questo repository, mentre in un repository privato è possibile scegliere chi può visualizzare i contenuti. Inoltre, il repository privato è una versione a pagamento. Non per ultimo, se si fa riferimento alla schermata precedente, si inizializza il repository con un file README.  Questo file contiene la descrizione del file e, una volta selezionata questa casella, sarà il primo file del repository. Ora il nostro repository centrale è stato creato con successo! Una volta fatto questo, si è pronti a eseguire commit, pull, push e tutte le altre operazioni. Ma andiamo per gradi e proviamo a capire come funzionano i branch in GitHub.   Creare branch ed eseguire operazioni in GitHub I branch aiutano a lavorare su diverse versioni di un repository contemporaneamente. Supponiamo che tu voglia aggiungere una nuova funzionalità (che è in fase di sviluppo) e che allo stesso tempo, però, tu abbia timore di apportare modifiche al progetto principale. È qui che i branch di git vengono in soccorso.  I branch permettono di spostarsi avanti e indietro tra i diversi stati/versioni di un progetto. Nello scenario precedente, è possibile creare un nuovo ramo e testare la nuova funzionalità senza influenzare il branch principale.  Una volta terminato, si possono unire le modifiche dal nuovo branch al branch principale. In questo caso il branch principale è il branch main, che è presente nel repository per impostazione predefinita. C'è un branch main che ha bisogno di un nuovo branch per i test. In questo branch vengono eseguite due serie di modifiche e, una volta completate, vengono unite di nuovo al branch main. Ecco come funziona la ramificazione! Scopriamo, ora, come creare un branch in GitHub. Per creare un branch in GitHub, dovrai seguire i seguenti passaggi:   Fai clic sul menu a tendina "Branch: main". Non appena si fa clic sul branch, è possibile trovare un branch esistente o crearne uno nuovo. Nel nostro caso, stiamo creando un nuovo branch con il nome "readme-changes". Una volta creato un nuovo branch, nel repository sono presenti due branches: main e readme-changes. Il nuovo branch è solo una copia del branch main. Eseguiamo, quindi, alcune modifiche nel nostro nuovo branch e rendiamolo diverso dal branch main. Utilizzando l’editor di testo integrato in Github modifichiamo il file readme del branch readme-changes e andiamo a eseguire un commit.   Questa operazione consente di salvare le modifiche apportate al file. Quando si esegue il commit di un file, si dovrebbe sempre fornire il messaggio, proprio per tenere a mente le modifiche apportate dall'utente. Nonostante questo messaggio non sia obbligatorio, è sempre consigliato inserirlo, in modo da poter differenziare le varie versioni o commit effettuati finora nel repository. Questi messaggi di commit mantengono la cronologia delle modifiche che, a loro volta, aiuta gli altri collaboratori a comprendere meglio il file. Ora facciamo il nostro primo commit, seguendo i passaggi seguenti:   Fai clic sul file "readme- changes" che abbiamo appena creato. Fai clic sull'icona "edit" o matita nell'angolo destro del file. Una volta cliccato, si aprirà un editor in cui è possibile digitare le modifiche o qualsiasi altra cosa.   Scrivi un messaggio di commit che identifichi le vostre modifiche. Alla fine, fai clic su "commit changes".  Abbiamo effettuato con successo il nostro primo commit. Ora questo file "readme-changes" è diverso dal branch main. Vediamo, ora, come aprire una richiesta di pull.   Creare una pull request su github Il comando Pull è il comando più importante di GitHub.  Indica le modifiche apportate al file e chiede agli altri collaboratori di visualizzarlo e di unirlo al branch main. Una volta eseguito il commit, chiunque può prelevare il file e avviare una discussione su di esso. Una volta terminato, è possibile unire il file. Il comando pull confronta le modifiche apportate al file e, se ci sono conflitti, è possibile risolverli manualmente. Vediamo ora le diverse fasi della richiesta di pull in GitHub. Fai clic sulla scheda "Richieste di pull". Fai clic su "Nuova richiesta di pull". Una volta cliccato sulla richiesta di pull, seleziona il branch e fare clic sul file "readme" per visualizzare le modifiche tra i due file presenti nel repository. Fai clic su "Crea richiesta di pull". Inserisci un titolo e una descrizione delle modifiche e fai clic su "Create pull request".   Quindi, andiamo avanti e vediamo come unire la richiesta di pull.   Merging pull request Ecco l'ultimo comando che unisce le modifiche al ramo master principale. Abbiamo visto le modifiche in colore rosa e verde, ora uniamo il file "readme" con il branch main. Segui i passaggi seguenti per unire la richiesta di pull. Fai clic su "Merge pull request" per unire le modifiche al branch main. Fai clic su "Conferma merge". È possibile eliminare il branch una volta che tutte le modifiche sono state incorporate e se non ci sono conflitti. Passiamo, quindi, all'ultimo argomento di questa guida, ovvero la clonazione di un repository GitHub.   Clone di un repository GitHub Prima di parlare della clonazione di un repository GitHub, cerchiamo di capire perché è necessario clonare un repository. La risposta è semplice!  Se si vuole utilizzare del codice presente in un repository pubblico, si può copiare direttamente il contenuto clonandolo o scaricandolo. Per clonare un repository pubblico ti basterà entrare nel repository e cliccare sul tasto verde in alto a destra su “code”: potrai clonare attraverso il comando da console git clone oppure scaricare direttamente il repository sotto forma di zip.   Alternative a GitHub Finora abbiamo parlato di Github come possibile piattaforma per repository online ed, effettivamente, ad oggi è tra le più utilizzate dagli sviluppatori; tuttavia, esistono delle valide alternative a Github.  Vediamole insieme!   GitLab GitLab è un repository Git basato sul web che offre repository aperti e privati gratuiti, funzionalità di issue-following e wiki. È una piattaforma DevOps completa che consente ai professionisti di svolgere tutte le attività di un progetto, dalla pianificazione e gestione del codice sorgente al monitoraggio e alla sicurezza. Inoltre, consente ai team di collaborare e di creare software migliori.    GitLab aiuta i team a ridurre i cicli di vita dei prodotti e ad aumentare la produttività, creando così valore per i clienti. L'applicazione non richiede agli utenti di gestire le autorizzazioni per ogni strumento. Se le autorizzazioni vengono impostate una sola volta, tutti i membri dell'organizzazione hanno accesso a ogni componente.   Bitbucket BitBucket è un servizio basato sul cloud che aiuta gli sviluppatori a memorizzare e gestire il proprio codice, nonché a tracciare e controllare le modifiche apportate al codice. BitBucket offre un servizio di hosting di repository Git basato sul cloud. L'interfaccia è abbastanza semplice da consentire anche ai codificatori alle prime armi di trarre vantaggio da Git. In genere, per utilizzare Git da solo, sono necessarie conoscenze tecniche più approfondite e l'uso della riga di comando. Inoltre, BitBucket offre una serie di servizi, come la possibilità per i team di collaborare e creare progetti, testare e distribuire il codice.  
10.4

Il modello Fork & pull

Il modello repository (remoto) condiviso è il modo più semplice per collaborare allo sviluppo di un progetto tramite Git. Esiste un singolo repository remoto usato da tutti i collaboratori per inviare e ricevere commit. Vediamo un altro modello, promosso dal servizio GitHub, in cui esistono più repository remoti con diversi permessi per i collaboratori   Fork & Pull   In questo modello sono presenti due repository remoti: upstream è il repository originale che funge da “master copy” per tutti i collaboratori; da questo repository remoto i collaboratori possono recuperare i nuovi commit (fetch), ma non possono inviare direttamente nuovi commit origin è un repository remoto di proprietà del singolo collaboratore, che è stato clonato dal repository upstream; il collaboratore può inviare (push) e recuperare (fetch) commit da questo repository Il repository locale sa quindi che esistono due diversi repository remoti a cui è collegato. Tali repository remoti, indicati come tracked repository, possono essere ispezionati e modificati tramite il comando git remote. Il modello fork & pull è pensato per quelle situazioni di collaborazione in cui non è possibile o non è desiderabile dare accesso in scrittura al repository remoto principale a ogni sviluppatore, ma è comunque utile la presenza di un repository remoto in cui il collaboratore può salvare il suo lavoro ed eventualmente condividerlo con altri. Il repository remoto personale viene chiamato “fork” e il maintainer del progetto originale potrà recuperare dal fork le eventuali modifiche per incorporarle nel progetto originale stesso tramite il comando git pull, indicando il repository fork come remote da cui recuperare le modifiche. Allo stesso modo il proprietario del repository fork potrà recuperare tramite git pull i nuovi commit del progetto originali e incorporarli nel suo fork.  

11

Sincronizzare modifiche locali e remote in Git

11.1

Il comando Git fetch in Git

Abbiamo visto come Git permetta a ogni repository di tenere traccia dei repository (remoti) a cui è connesso tramite il comando git remote. La sincronizzazione delle modifiche tra diversi repository è effettuata da altri comandi, ognuno dei quali ha la sua specifica responsabilità. Il comando Git fetch in Git Lo scopo di git fetch è quello di scaricare commit, branch e tag da un repository remoto nel repository locale. I nuovi contenuti recuperati tramite git fetch vanno esplicitamente applicati alla propria working copy, rendendo, quindi, l’esecuzione di tale comando un'operazione sicura per recuperare nuovi commit, senza necessariamente doverli applicare al proprio lavoro in corso. Supponiamo di aver clonato ieri un repository remoto in cui era presente solo il branch main e che tale branch contenesse tre commit. La mia copia locale quindi sa di essere “connessa” al repository remoto indicato come origin e la mia working copy ha estratto l’ultimo commit dell’unico branch presente, collegando il branch locale main al corrispettivo branch con lo stesso nome su origin ❯ git branch -vv * main c7ed69f [origin/main] implemented first feature Un altro collaboratore ha aggiunto alcuni commit su main e creato un nuovo branch. $ git fetch c7ed69f..45e66a4 main -> origin/main * [new branch] some-feature -> origin/some-feature $ git status On branch main Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded. (use "git pull" to update your local branch) I nuovi commit e il nuovo branch sono ora disponibili anche nel repository locale, ma non sono stati applicati. Questo perché sebbene nel repository locale siano ora presenti tutti i commit, sia locali che remoti, Git mantiene separati i commit dei branch locali rispetto a quelli dei branch remoti   Commit locali e remoti Sarà possibile vedere quali commit sono stati aggiunti a origin/master tramite git log e applicare i nuovi commit sul proprio branch locale tramite git merge. $ git log --oneline main..origin/main 22596179 (origin/main, origin/HEAD) new commit two f8986356 new commit one $ git merge origin/main Updating c7ed69f..45e66a4 Fast-forward Post merge Vedremo più avanti cosa fare nel caso in cui siano stati aggiunti commit alla propria versione locale.
11.2

Il comando Git pull in Git

Abbiamo visto, all’inizio di questa sezione, come recuperare nuovi contenuti da un repository remoto tramite il comando git fetch. Vediamo ora più nel dettaglio come riportare tali modifiche nella propria working copy tramite il comando git pull. L’unione delle modifiche dal repository remote al repository locale è un’attività comune nei flussi di lavoro di collaborazione basati su Git. Il comando git pull in Git viene spesso utilizzato per recuperare e scaricare contenuti da un repository remoto e aggiornare immediatamente la working copy del repository locale in modo che corrisponda a tale contenuto. Il comando git pull in Git è, in realtà, una combinazione di altri due comandi.  Nella prima fase dell’operazione git pull eseguirà un git fetch limitato al solo branch a cui punta la propria working copy. Una volta scaricato il contenuto, git pull entrerà in un flusso di riconciliazione dei commit che potrà seguire due modalità dagli effetti ben diversi. Proviamo a capirlo meglio con un esempio. Supponiamo di aver clonato un repository remoto quando l’ultimo commit sul suo branch main era quello indicato come “B”. Abbiamo fatto delle modifiche e abbiamo salvato in locale i commit “C”, “D” ed “E”. Nel frattempo, qualcun altro, ha creato a inviato al repository remoto altri commit, “F”, “G” e “H”   Stato iniziale esempio merge In questa situazione, considerando che il repository remote è quello che ospita la storia “ufficiale” del progetto, dovremo recuperare i nuovi commit remoti e integrarli con quelli locali. Dovremo, però, anche indicare la strategia preferita con cui farlo. Git, infatti, offre due modi ben distinti con cui effettuare pull/merge di branch divergenti, con rebase o con merge.   NOTA: nelle recenti versioni di Git l’esecuzione di git pull su branch divergenti non viene completata a meno che non si sia indicata la strategia desiderata. È possibile farlo tramite gli argomenti del comando, ma è anche possibile indicare la strategia di default tramite git config.   Pull con rebase   Stato iniziale esempio merge Il pull con rebase è la modalità che, in un certo senso, rispetta come “ufficiale” il contenuto del repository remoto e considera i commit nel repository locale come commit da applicare sulla history aggiornata del repository. Eseguendo git pull --rebase succede, in pratica, quanto segue: vengono messi da parte i commit locali (C-D-E) aggiunti rispetto all’ultimo origin/main recuperato viene eseguito il fetch che recupera i nuovi commit dal branch main del repository remoto origin i nuovi commit remoti vengono applica alla “copia” locale origin/main e viene aggiornato il branch locale main e la working copy i commit locali C-D-E vengono “riapplicati”, creando quindi i nuovi commit C’-D’-E’, ciascuno dei quali contiene le stesse differenze dei commit C-D-E Ricordiamo, infatti, che un commit è uno snapshot completo dello stato di un repository, quindi avendo cambiato la storia precedente al commit C, il commit C’ conterrà le stesse “differenze” rispetto allo snapshot precedente, ma sarà, nella pratica, un nuovo commit. È possibile scegliere questa strategia come default tramite il comando git config pull.rebase true   Pull con merge   Stato iniziale esempio merge L’altra modalità è quella di pull con merge che, nella situazione descritta, dà la “precedenza” al repository locale rispetto a quello remoto, o quanto meno nella situazione descritta potrebbe creare una history apparentemente poco coerente. Eseguendo git pull --no-rebase succede, infatti, quanto segue: viene eseguito il fetch che recupera i nuovi commit dal branch main del repository remoto origin vengono presi i commit F-G-H recuperati dal repository remoto e viene creato uno speciale commit di “merge” (I) che applica l’intero diff al branch locale viene chiesto all’utente di salvare tale commit. Alla fine del pull con merge, sebbene siamo in una situazione che dal punto di vista di Git non è più “divergente” (sarà, infatti, possibile fare git pull per inviare al repository remoto), la history del progetto risulta meno comprensibile e lineare. Se, infatti, visualizziamo la history del progetto con git log --graph  --oneline dopo aver effettuato il push, otterremo qualcosa del genere: $ git log --graph --oneline * ece860f (HEAD -> main, origin/main, origin/HEAD) I - Merge branch 'main' of https://server.com/repository.git |\ | * 09817f7 H - remote commit three | * 1e0e1d2 G - remote commit two | * b7d7502 F - remote commit one * | 4d49b1c E - local commit 3 * | 6453336 D - local commit 2 * | 24540dd C - local commit 1 |/ * 9cc4eee B - another commit * f46646d A - initial commit Notare che, dal punto di vista prettamente cronologico del repository remoto, è stata invertita la cronologia, poiché nella history i commit F-G-H erano, in pratica, già presenti prima che fossero inviati i commit C-D-E, che, invece, ora sono direttamente successivi ai commit A-B. Sebbene, quindi, il “merge” sia una attività a volte necessaria nel gestire la sincronizzazione di lavori su Git, nel caso in cui si collabori ad un branch è consigliabile optare per la modalità con rebase. È possibile scegliere questa strategia come default con il comando git config pull.rebase false
11.3

Il comando Git push in Git

Il comando git push in Git serve a fare l’upload delle modifiche locali su un repository remoto. In questo modo, i commit locali sono resi disponibili agli altri collaboratori del progetto, che potranno recuperarli tramite un fetch e incorporarli nei rispettivi repository locali. Nella modalità standard, git push invia solo le nuove modifiche, poiché Git sa quali commit sono già presenti nel repository remoto. $ git status On branch main Your branch is ahead of 'origin/main' by 2 commit. (use "git push" to publish your local commits) $ git push Enumerating objects: 7, done. Counting objects: 100% (7/7), done. Delta compression using up to 16 threads Compressing objects: 100% (4/4), done. Writing objects: 100% (4/4), 612 bytes | 612.00 KiB/s, done. Total 4 (delta 3), reused 0 (delta 0), pack-reused 0 remote: Resolving deltas: 100% (3/3), completed with 3 local objects. To https://server.com/project.git e953fe1..cf9735f main -> main Prima e dopo il push Eseguendo git push senza indicare argomenti, verranno inviate al repository remoto collegato al branch attualmente in uso nella propria working copy locale solo i nuovi commit. È possibile, comunque, indicare vari argomenti al comando per scegliere cosa esattamente inviare al repository remoto. $ git tag v1.2.3 $ git push --tags Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 To https://server.com/projecto.git * [new tag] v1.2.3 -> v1.2.3 Il push di tag verso un repository remoto deve essere esplicitamente indicato attraverso l’argomento --tags, che invia tutti i nuovi tag presenti sul repository locale al repository remoto. $ git status On branch main Your branch is up to date with 'origin/main'. $ git branch new-feature $ git branch * main new-feature $ git push --all Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 To https://server.com/projecto.git * [new branch] new-feature -> new-feature Con l’argomento --all invece viene fatto l’upload di tutti i branch presenti nel repository locale verso il repository remoto. $ git remote -vv backup [email protected]:elle.uca/aulab-demo.git (fetch) backup [email protected]:elle.uca/aulab-demo.git (push) origin https://github.com/elleuca/aulab-demo.git (fetch) origin https://github.com/elleuca/aulab-demo.git (push) $ git status On branch main Your branch is up to date with 'origin/main'. $ git branch -vv * main 35aad79 [origin/main] Initial commit $ git push backup Enumerating objects: 3, done. Counting objects: 100% (3/3), done. Writing objects: 100% (3/3), 642 bytes | 642.00 KiB/s, done. Total 3 (delta 0), reused 3 (delta 0), pack-reused 0 To gitlab.com:elle.uca/aulab-demo.git * [new branch] main -> main È, ovviamente, possibile indicare, nel caso in cui si abbiano più repository remote e più branch, il branch specifico da inviare e il remote a cui inviarlo eseguendo il comando nella forma git push <remote> <branch>. Per completezza e per non essere colti impreparati nel caso di necessità, vediamo come agire nel caso in cui sia necessario cambiare la history dopo che abbiamo già effettuato il push sul repository remoto.  Ad esempio, supponiamo di essere nella situazione in cui vogliamo effettuare un commit che modifica un file preesistente e ne aggiunge un altro, necessario affinché la modifica apportata al nostro progetto abbia senso. Per errore, però abbiamo incluso nel commit solo il file con le modifiche. $ git status On branch main Your branch is up to date with 'origin/main'. Changes to be committed: modified: page.html Untracked files: public/new-logo.png $ git commit -m "update logo" && git push [main d5c0b92] update logo Poiché, in realtà, il commit non rappresenta l’effettiva modifica voluta, potrebbe essere utile effettuare l’amend del commit invece di aggiungere un secondo commit.  In questo modo, però, la history del repository locale cambia rispetto a quella repository remoto (con l’amend, infatti, in pratica sostituiamo uno snapshot con un altro snapshot). Per allineare repository locale e repository remoto possiamo effettuare il push con l’argomento --force. $ git add public/new-logo.png $ git commit --amend --no-edit [main 5fb65cb] update logo $ git status On branch main Your branch and 'origin/main' have diverged, and have 1 and 1 different commits each, respectively. (use "git pull" to merge the remote branch into yours) $ git push --force Enumerating objects: 8, done. Counting objects: 100% (8/8), done. Delta compression using up to 16 threads Compressing objects: 100% (5/5), done. Writing objects: 100% (5/5), 39.00 KiB | 19.50 MiB/s, done. Total 5 (delta 2), reused 0 (delta 0), pack-reused 0 remote: Resolving deltas: 100% (2/2), completed with 2 local objects. To https://server.com/projecto.git + 7b82870...5fb65cb main -> main (forced update) È necessario essere estremamente attenti nell’uso dell’argomento --force del comando git push poiché, in questo caso, Git da per assunto che sia precisa intenzione dell’utente sovrascrivere interamente la history del branch remoto, sostituendola con quella del branch locale. Non è da escludere, infatti, che tra il nostro primo push e il secondo qualche altro collaboratore del progetto non abbia a sua volta effettuato altri push. Per minimizzare perdite indesiderate, è sempre utile: effettuare il fetch e pull delle modifiche remote sul proprio branch locale prima di fare un push con force utilizzare --force-with-lease invece di --force: in questo modo viene usata una modalità speciale del push con force che, nella pratica, avvisa nel caso si stia per rimuovere dal repository remoto commit altrui (che potranno quindi essere riportati nella copia locale prima di riprovare)

12

Utilizzo dei branch in Git

12.1

Il comando Git branch in Git

Git memorizza un branch come riferimento a un commit. In questo senso, un branch rappresenta la punta di una serie di commit, non è un contenitore per i commit. La cronologia di un branch viene estrapolata attraverso le relazioni tra commit, che ricordiamo essere l’unione di uno snapshot del contenuto del repository in un certo momento e il riferimento al commit precedente. Nella pratica di tutti i giorni, i branch sono utilizzati per individuare una linea di sviluppo indipendente, che si tratti della correzione di un bug o dell'aggiunta di una nuova funzionalità. Nel momento in cui viene creato un nuovo branch, Git, in pratica, ci mette a disposizione una nuovo set di working area, staging area e cronologia. Grazie alla modalità di implementazione dei branch e alle diverse modalità con cui è possibile “spostare” commit e riscrivere la cronologia dei commit offerte da Git, è possibile avere diverse modalità o strategie di utilizzo dei branch, sia per lo sviluppo sul repository locale, sia su quelli remoti condivisi. Il comando Git branch in Git Il comando git branch in Git consente di elencare, creare, rinominare ed eliminare branch. Non consente di passare da un branch all’altro o di rimettere insieme le cronologie di diversi branch. Per questo motivo, git branch è strettamente integrato con i comandi git checkout e git merge. $ git branch * main $ git branch experiment $ git branch experiment * main Senza argomenti, il comando git branch elenca i branch locali del repository. Il branch attualmente in uso è contrassegnato da un asterisco. Indicando come opzione il nome di un branch, viene creato un nuovo branch (ma non viene fatto alcuno “switch”   Creare un branch Alla creazione di un branch in Git, viene soltanto creato un nuovo puntatore alla HEAD del branch attualmente in uso. Non viene fatta alcuna altra modifica al repository. Nell’uso del comando git branch possono risultare utili se seguenti opzioni: git branch -d <branch> - elimina il branch indicato, impedendo però l’eliminazione se sono presenti commit non mergiati git branch -D <branch> - elimina il branch indicato senza verificare la presenza di commit non mergiati git branch -m <branch> - rinomina il branch corrente git branch -a - elenca i branch remoti
12.2

Il comando Git checkout in Git

Git non offre funzioni di “undo” simili a quello di programmi consumer, ma offre comunque diversi comandi che permettono di tornare sui propri passi e gestire la sequenza di commit nella history come se fosse una macchina del tempo. Il comando Git checkout in Git Sappiamo che Git memorizza una serie di snapshot di un progetto chiamati commit, ogni commit ha un suo identificativo e punta al commit precedente, in modo da ricostruire l’intera cronologia. Sappiamo anche che Git permette di avere una o più “linee temporali”, chiamate branch, che si diramano eventualmente a partire da un determinato commit comune. Prendiamo il caso di un repository con un singolo branch, chiamato main, su cui abbiamo eseguito alcuni commit. [17452bb93]<---[88adbcdf6]<---[fe78029f1]<---[6d9c22a3b]::{main} | | HEAD In questa situazione Git mantiene un riferimento simbolico chiamato HEAD che dice cosa sta mostrando la nostra working copy. Tipicamente, HEAD punta a un branch (ovvero alla sua “tip”). Il comando git checkout in Git permette di decidere cosa estrarre nella working copy, sia esso un branch, un tag, o un altro riferimento valido, come per esempio un commit. In questo modo è possibile recuperare nella propria working copy uno specifico snapshot del progetto, tornando quindi “indietro nel tempo” al momento in cui quello snapshot è stato salvato. Nel momento in cui si effettua il git checkout di uno specifico commit o tag, Git mostra un avviso che potrebbe inizialmente spaventare: $ git checkout 88adbcdf6 Note: switching to '88adbcdf6'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c <new-branch-name> Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to false HEAD is now at 88adbcdf6 Change behavior In pratica, rispetto al grafico precedente, la situazione è cambiata così: [17452bb93]<---[88adbcdf6]<---[fe78029f1]<---[6d9c22a3b]::{main} | | HEAD Poiché la HEAD punta direttamente a un commit, Git avvisa che la working copy è in uno stato detached. In pratica, quindi, ogni eventuale commit successivo non verrà aggiunto al branch main. Il messaggio indica come, eventualmente, creare un nuovo branch o tornare al branch di partenza tramite il comando git switch. È comunque possibile utilizzare nuovamente il comando git checkout per tornare al branch iniziale: $ git checkout main Previous HEAD position was 88adbcdf6 Change behavior Switched to branch 'master' Your branch is up to date with 'origin/master'. Nel caso in cui avessimo creato dei commit mentre ci si trovava nello stato detached e fossimo poi tornati al branch main, avremmo [800e2aa6]<---[fae892ce] / [17452bb93]<---[88adbcdf6]<---[fe78029f1]<---[6d9c22a3b]::{main} | | HEAD In questa situazione, non è stato creato alcun riferimento al commit fae892ce (cioè non è stato creato un nuovo branch) e Git provvederà a rimuovere tale commit (e per estensione anche il commit 800e2aa6) tramite suoi processi di routine. Riassumendo, possiamo quindi dire che git checkout: serve principalmente per indicare cosa vogliamo “caricare” nella nostra working copy tipicamente, viene usato per indicare il branch che vogliamo “caricare” nella working copy (in questo caso i commit successivi sono agganciati al branch attuale): git checkout <NOME_BRANCH> può essere utilizzato per indicare uno specifico commit, tramite il suo id o un tag, da “caricare” nella working copy (in questo caso si entra nella modalità sganciata da ogni branch): git checkout <COMMIT_O_TAG> Vedremo, in un capitolo successivo, come lavorare creando branch per separare gli sviluppi futuri, in particolare come “spostare” commit da un branch a un altro.
12.3

Il comando Git merge in Git

Il comando git merge in Git serve a combinare più sequenze di commit in una cronologia unificata. Nei casi d’uso più frequenti, git merge viene utilizzato per combinare due branch. Abitualmente, specie per chi utilizza piattaforme come GitHub o GitLab per come server per lo sviluppo condiviso, le operazioni di merge vengono effettuate direttamente dalla piattaforma sul repository remoto nel momento in cui le modifiche a una pull/merge request vengono approvate e incluse nel branch principale. È comunque utile conoscere i meccanismi base del merge, visto che si tratta di un processo essenziale in Git. In particolare è importante considerare che: in Git l’operazione di merge combina sequenze di commit in una singola sequenza di commit unificata esistono due modi in cui viene effettuato il merge: Fast Forward e Three Way Git è in grado di completare automaticamente il merge se non ci sono conflitti tra le sequenze di commit Prima di procedere a un merge, è opportuno eseguire alcuni step preparatori per essere sicuri che l’operazione proceda senza problemi, specie se si sta per eseguire localmente il merge di branch remoti: individuare i due branch che si vogliono mergiare, quello che riceverà modifiche (per esempio. main) e quello che si vuole unire (per esempio new-feature) fare fetch e pull sui tali branch delle rispettive modifiche remote fare checkout nella working copy locale del branch che riceverà le modifiche Si potrà, quindi, far partire il merge tramite il comando git merge <branch-to-be-merged>. L’effettiva modalità e risultato del merge dipende dallo rispettivo stato dei due branch. Fast Forward Merge in Git git checkout -b new-feature main git add <file> git commit -m "Start a feature" git add <file> git commit -m "Finish a feature" git checkout main git merge new-feature   Fast Forward Merge Un merge fast-forward in Git può verificarsi quando è presente un percorso lineare dall’ HEAD del ramo da unire all’ HEAD del ramo di destinazione. In questo caso, Git non effettua un vero merge, ma semplicemente sposta la HEAD del ramo di destinazione alla HEAD del ramo da unire. Le due history sono state effettivamente combinate, ma la history del nostro branch new-feature era, per così dire, il normale avanzare della history del branch main. Three Way Merge in Git git branch new-feature git add <file> git commit -m "Fix a bug" git checkout new-feature git add <file> git commit -m "Start a feature" git add <file> git commit -m "Finish a feature" git checkout main git merge new-feature Three Way Merge Nel caso in cui i branch siano divergenti, Git deve combinare le due history attraverso un three-way merge. In questa modalità viene usato un tipo di commit dedicato, il merge commit per unire insieme le due history. I commit di merge sono unici rispetto agli altri commit, poiché hanno due commit genitore. Nel creare un commit di merge, Git tenterà di unire automaticamente le cronologie separate. Se Git riscontra che la stessa parte di file è stata cambiata in entrambe le cronologia, non sarà in grado di combinarle automaticamente. Questo scenario è un conflitto di controllo della versione e Git richiederà l’intervento dell’utente per continuare. Il nome “three way” indica che a Git servono tre commit per capire come realizzare il merge: i due commit HEAD dei due branch e il rispettivo genitore comune.

13

Risoluzione dei conflitti di merge in Git

13.1

Risolvere un merge conflict in Git

Abbiamo accennato, in precedenza, alla possibilità che durante l’esecuzione di un git pull, git merge o git rebase in Git si possano verificare dei conflitti che impediscono a Git di portare a termine automaticamente l’operazione. Un conflitto nell’accezione dei version control system è una situazione per la quale il sistema sta provando a integrare due diverse history che hanno entrambe apportato modifiche alle stesse righe di un file. Il processo di merge può andare in uno stato di conflitto in due diversi momenti: all’avvio del merge - avviene quando ci sono modifiche nella working copy o nella staging area; tali modifiche “pending” non possono essere gestite da Git e sarà compito dello sviluppatore “pulire” il proprio spazio di lavoro con git stash, git checkout, git commit o git reset durante il merge - in tal caso il conflitto è effettivamente tra il branch in uso e quello che si sta mergiando, conflitto che riguarda modifiche apportate nei commit dei due branch. NOTA: come visto nella precedente sezione, nel merge portiamo le modifiche presenti in un branch dentro al branch attualmente presente nella working copy. Nel seguito di questa sezione indicheremo tali branch rispettivamente come “incoming” e “current”. Proviamo a fare qualche “esercizio” di merge conflict in Git. Proviamo a realizzare il caso in cui vogliamo riportare sul branch main delle modifiche fatte da noi su un branch, nella situazione in cui un altro sviluppatore web ha aggiunto un commit sul branch main successivamente alla creazione del nostro branch di sviluppo. Merge conflict Creiamo un repository a cui aggiungiamo un file con un commit (con l’opzione --author per simulare meglio chi ha committato cosa). $ mkdir git-merge-test $ cd git-merge-test $ git init . $ echo "initial content to edit later" > merge.txt $ git add merge.txt $ git commit -m "initial content" --author="Someone <[email protected]>" [main (root-commit) 0a99708] initial content Author: Someone <[email protected]> 1 file changed, 1 insertion(+) create mode 100644 merge.txt Creiamo un nuovo branch, apportiamo modifiche al file e salviamo tali modifiche in un commit. $ git checkout -b merge-me Switched to a new branch 'merge-me' $ echo "new content to merge later" > merge.txt $ git commit -am "edited the content for conflict" --author "Me <[email protected]>" [merge-me 15416df] edited the content for conflict Author: Me <[email protected]> 1 file changed, 1 insertion(+), 1 deletion(-) Torniamo sul branch main e aggiungiamo una nuova riga di testo al file, sempre poi salvando in un commit. $ git checkout main Switched to branch 'main' $ echo "content added to previous" >> merge.txt $ git commit -am "appended content to merge.txt" --author="Frank <[email protected]>" [main 90640eb] appended content to merge.txt Author: Frank <[email protected]> 1 file changed, 1 insertion(+) Proviamo, quindi, a fare merge delle modifiche nel branch merge-me (incoming) nel branch main (current) $ git merge merge-me Auto-merging merge.txt CONFLICT (content): Merge conflict in merge.txt Automatic merge failed; fix conflicts and then commit the result. $ git status On branch main You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: merge.txt no changes added to commit (use "git add" and/or "git commit -a") Provando ad eseguire git merge viene segnalata la presenza di un conflitto. Il processo di merge si interrompe, lasciando la propria copia in uno stato di attesa di risoluzione. Risolvere un merge conflict in Git Con il comando git status possiamo evidenziare la presenza di un conflitto durante l’esecuzione di un merge. Vediamo come scoprire maggiori dettagli su cosa esattamente è cambiato tra le due versioni della history e come intervenire. Nell’esempio di conflitto “forzato” sappiamo che il problema è nel file merge.txt, che ha ricevuto modifiche nelle stesse righe in entrambi i branch. Nel momento in cui il merge si è interrotto, nel file in conflitto ci sono le modifiche provenienti da entrambe le versioni del file: $ cat merge.txt <<<<<<< HEAD initial content to edit later content added to previous ======= new content to merge later >>>>>>> merge-me Di particolare interesse sono i marker di conflitto inseriti da Git che ci permettono di sapere che: la righe tra <<<<<<< e ======= contengono le righe in conflitto nel file come erano nel commit del branch “current” le righe tra ======= e >>>>>>> contengono le righe in conflitto nel file come erano nel commit del branch “incoming”. Ci sono tre modi per risolvere i conflitti in un file e procedere con il merge, che ovviamente possono e debbono essere scelti caso per caso: accettare in toto le modifiche che erano già nel branch “current” (nel nostro caso main) con git checkout --ours merge.txt accettare in toto le modifiche che vengono dal branch “incoming” (nel nostro caso new_branch_to_merge_later) con git checkout --their merge.txt modificare il file in modo che contenga un contenuto corretto e in linea con entrambe le modifiche, dichiarare che è stato risolto il conflitto con git add merge.txt e proseguire con il merge tramite un commit. Se volessimo procedere con questa ultima opzione, dovremmo modificare il file fino a raggiungere un contenuto adeguato e poi: $ cat merge.txt new content to merge later content added to previous $ git status On branch main You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: merge.txt no changes added to commit (use "git add" and/or "git commit -a") $ git add merge.txt $ git status On branch main All conflicts fixed but you are still merging. (use "git commit" to conclude merge) Changes to be committed: modified: merge.txt Una volta effettuato il commit, la history del nostro repository sarà la seguente: $ git log --pretty=format:'%h %s (%an)' --date=short --graph * c9802c8 merged content due conflict (Me) |\ | * 15416df edited the content for conflict (Me) * | 90640eb appended content to merge.txt (Frank) |/ * 0a99708 initial content (Someone) Notare che in questo caso, il nostro commit iniziale sul branch è rimasto intatto (ce lo dice il fatto che abbia ancora lo stesso hash, 15416df) e le modifiche necessarie a risolvere il conflitto sono state incluse nel commit di merge. Possiamo, infatti, confrontare la differenza tra gli ultimi due commit sul branch in uso con $ git diff HEAD^1 diff --git a/merge.txt b/merge.txt index c560b6f..732761b 100644 --- a/merge.txt +++ b/merge.txt @@ -1,2 +1,2 @@ -initial content to edit later +new content to merge later content added to previous
13.2

Capire meglio il contenuto dei commit durante un conflitto di merge in Git

Durante la risoluzione di un conflitto di merge in Git è particolarmente rilevante capire cosa è successo nelle due diverse versioni e se e come i due diversi contenuti possono essere riconciliati. Le due versioni potrebbero, infatti, contenere codice che si comporta in maniera molto diversa ed è importante capire cosa si sta approvando prima di procedere. Un primo suggerimento è quello di configurare il proprio Git per usare lo stile “diff3” per i conflitti di merge. Se torniamo all’esempio precedente, eseguendo git config merge.conflictstyle diff3 prima di lanciare git merge, i marker aggiunti da Git per evidenziare i contenuti in conflitto cambiano: $ git config merge.conflictstyle diff3 $ git merge new_branch_to_merge_later $ cat merge.txt <<<<<<< HEAD initial content to edit later content added to previous ||||||| 0a99708 initial content to edit later ======= new content to merge later >>>>>>> merge-me Rispetto alla versione precedente abbiamo: tra ||||||| e ======= il contenuto nel commit genitore comune ai due commit in conflitto tra <<<<<<< e ||||||| la versione nel branch “current” tra ======= e >>>>>>> la versione nel branch “incoming” Sempre nell’ottica del capire su cosa si sta mettendo mano, potrebbe risultare anche utile controllare i messaggi di commit dei rispettivi branch tramite git log --merge, magari usando l’opzione -p che mostra anche i diff dei due commit $ git log --merge -p merge.txt commit 90640eb2ecab9ec63fcad24817a314df344e024c (HEAD -> main) Author: Frank <[email protected]> Date: Sat Jan 14 23:33:22 2023 +0100 appended content to merge.txt diff --git a/merge.txt b/merge.txt index 3480007..c560b6f 100644 --- a/merge.txt +++ b/merge.txt @@ -1 +1,2 @@ initial content to edit later +content added to previous commit 15416df402028b78858105c84d49a86fee59e2e3 (merge-me) Author: Me <[email protected]> Date: Sat Jan 14 23:31:09 2023 +0100 edited the content for conflict diff --git a/merge.txt b/merge.txt index 3480007..202af08 100644 --- a/merge.txt +++ b/merge.txt @@ -1 +1 @@ -initial content to edit later +new content to merge later Da non dimenticare, poi, che molti IDE, editor di testo per web developers e applicazioni GUI per Git offrono modalità visive più accattivanti per capire le esatte differenze tra due commit in conflitto. Modificare file in conflitto con VSCode Aprendo un file che è in conflitto, l’editor VSCode riconosce ed evidenzia i marker di conflitto e offre varie opzioni per risolvere il conflitto. Modificare file in conflitto con VSCode È anche disponibile una speciale vista che permette di avere davanti sia le due versioni “originali”, sia la versione che si sta riscrivendo. NOTA: è interessante notare come mentre Git usa ours e their , alcune UI di Git hanno optato per l’uso rispettivo di “current” e “incoming”, che abbiamo preso in prestito per questa guida. Non dimenticare che l’azione di merge di due branch ha un “verso”. Da questo punto di vista la nomenclatura current/incoming risulta più esplicita nel caso in cui, per esempio, si sta facendo un merge su main di un branch su cui ci sono commit “nostri” e il conflitto è con commit fatti da altri sviluppatori: in tal caso infatti per Git sarebbero ours i commit di altri già su main e theirs i propri commit sul branch. Una volta risolto il conflitto, il risultato sarà una nuova versione del file e, in particolare, una nuova versione di quel gruppo di righe e, alla fine del merge, si dovrà controllare che il codice sia ancora corretto e funzionante. È sempre importante capire, quindi, cosa si sta accettando durante la risoluzione di un conflitto di merge, ma soprattutto è anche importante sapere quando è opportuno interrompere il merge e tornare sui propri passi, magari consultando il collega o i colleghi per capire insieme quale versione delle modifiche sarà alla fine quella che andrà inclusa nel repository

14

Git workflow e strategie di branching in Git

14.1

Workflow Git centralizzato

Git è il sistema di version control più usato al mondo, anche grazie alla sua estrema flessibilità nell’adattarsi al flusso di lavoro di ciascuno e del team. Nel tempo, sono emerse buone pratiche e raccomandazioni sull’uso efficiente di Git, al fine di poter lavorare in modo coerente e produttivo. Tali pratiche prendo il nome di “workflow”, proprio perché permettono ai team di trovare il modo più adatto per far fluire il lavoro dal server remoto che ospita il repository alla macchina dello sviluppatore web, e viceversa. Non ultimo, è opportuno anche considerare come un git workflow possa avere impatti o essere impattato da tool per la continuous integration e dalle modalità di deploy di un progetto (un sito web ha esigenze diverse rispetto a un’app per smartphone!!).   Workflow Git centralizzato Un workflow Git centralizzato consente a tutti i membri del team di apportare modifiche direttamente al branch principale del repository remoto. Questa strategia funziona bene per team piccoli, laddove i vari membri del team riescono a comunicare in modo efficiente per evitare quanto più possibile di intervenire sulla stessa porzione di codice. Vantaggi: strategia semplice adatta anche a chi è alle prime armi con Git si è obbligati a mantenere la propria working copy pulita e aggiornata Svantaggi: funziona adeguatamente in funzione del progetto e della dimensione del team necessario sincronizzarsi per stabilire momento in cui effettuare un rilascio modifiche potrebbero rimanere sui repository locali finché non si è “sbloccati”, oppure, al contrario, potrebbero essere state caricate sul repository remoto funzionalità non ancora pronte
14.2

Workflow Git feature branching

Vengono usati i branch per isolare lo sviluppo di una funzione o fix rispetto al branch principale del repository. Solo una volta completato lo sviluppo la feature viene riportata sul branch principale. I feature branch in Git sono uploadati sull’unico repository remoto e più sviluppatori possono contribuire all’implementazione della feature. Il “merge” della feature nel branch principale viene abitualmente realizzato tramite gli strumenti messi a disposizione dal servizio di hosting del repository. A tal proposito, vale la pena puntualizzare che servizi come GitHub, GitLab o Bitbucket (di cui ti abbiamo già parlato nella sezione su i principali repository online di Git) offrono agli utenti, oltre all’effettivo repository Git, anche una serie di strumenti aggiuntivi come, ad esempio, la gestione delle cosiddette pull/merge request, che permettono di operare, per l’appunto, il merge di un feature branch nel ramo principale direttamente sul repository remoto. Vantaggi: il branch principale di sviluppo può rimanere pulito finché la feature non è completa le feature possono essere condivise anche prima di essere completate più sviluppatori possono lavorare in contemporanea a una feature Svantaggi: più tempo richiede lo sviluppo della feature, più ci sono possibilità che il feature branch abbia conflitti rispetto al branch principale poiché i branch sono usati solo per “aggiungere” feature, non è possibile gestire un progetto che possa richiede rilasci di più versioni non compatibili
14.3

Workflow Git trunk-based

Il Workflow Git trunk-based è, per certi versi, l’unione dei due precedenti e cerca di unire diverse esigenze e riflessioni legate allo sviluppo software moderno. Il workflow trunk-based in Git segue l’indicazione di facilitare lo sviluppo concorrente su un singolo branch principale, offrendo diversi suggerimenti su come ottenere questo risultato. Tali suggerimenti sono anche non direttamente correlati all’uso del version control system, ma si estendono anche alle modalità con cui implementare funzioni nel proprio codice. A titolo d’esempio, nel workflow trunk-based è preferibile fare push di una nuova funzione non ancora completa direttamente sul branch principale, avendo cura, però, di disattivarla tramite degli opportuni feature flag. Non viene, però, impedita la creazione di branch che devono però essere usati solo se effettivamente necessario: per esempio, i feature branch possono essere utili se sul progetto lavorano molti sviluppatori, ma devono essere “chiusi” in un paio di giorni, i branch di release possono essere creati solo quando è effettivamente rilasciato un fix a una versione precedentemente rilasciata. Per maggiori informazioni sulla filosofia trunk-based, che ricordiamo va oltre quello che è il semplice version control di un progetto, basta un click sul link! Vantaggi: update frequente del branch principale e delle working copy, minimizza complessità di risoluzione conflitti branch principale sempre pulito e naturalmente portato verso la continuous delivery Svantaggi: non è limitato a sole guideline su come usare i branch, ma richiede conoscenze su molte altre tecniche (feature flag, continuous delivery, …) il team deve conoscere molto bene il progetto e avere forte ownership
14.4

Approccio “forking” in Git

I workflow visti finora erano accomunati dal fatto di avere comunque un singolo repository remoto di riferimento, che poteva eventualmente usare i branch per isolare gli sviluppi. Nell’approccio “forking” invece, viene creato un repository separato da quello principale, creando, quindi, una nuova e separata struttura di collaborazione. Questo tipo di approccio è spesso presente nei progetti open source in cui è necessaria una sorta di “protezione” sul repository principale, per cui solo alcuni sviluppatori (maintainer) possono effettivamente accedere a tale repository per creare branch e integrare codice. Altri sviluppatori possono autonomamente creare i propri fork, fare le proprie modifiche e poi, eventualmente, proporle per l’inclusione nel repository originale. La gestione effettiva del rapporto tra repository originale (upstream) e repository fork è fatta tramite i “remote” di Git, sulla cui base servizi come GitHub e GitLab hanno, effettivamente, implementato le rispettive funzionalità di fork e gli strumenti per aprire richieste di merge verso il repository upstream. Vantaggi: semplifica gestione dei permessi su team grandi o comunque con collaboratori “spot” posso creare, condividere e mantenere un mio fork (personale o aziendale) con modifiche che magari non è necessario integrare upstream Svantaggi: richiede buona conoscenza dell’uso di remote multipli
14.5

Gitflow in Git

Un'ultima doverosa sezione sul Git workflow noto come “GitFlow”. Come abbiamo detto all’inizio di questa sezione e di questa guida, Git in sé non è prescrittivo sull’uso di determinate funzioni, ma lascia allo sviluppatore web e al team trovare il flusso più adatto alle sue esigenze. GitFlow è stato, forse, il primo workflow proposto alla community degli sviluppatori che ha avuto un effetto dirompente, perché ha mostrato come sfruttare un tool già usato per altre esigenze - Git, per l’appunto - per coordinare e ottimizzare anche le attività legate al rilascio. Tutto ciò avveniva nel 2010; nel frattempo, sono sorti e si sono affermati altri tool e altre tipologie di software per le quali il modello GitFlow è ben poco pratico. GitFlow identifica e suggerisce l’uso di diversi branch e tipi di branch nel proprio progetto: un branch principale che memorizza la history dei rilasci ufficiali (main) un branch di integrazione delle feature (develop) dei feature branch, che nascono e fanno merge sul branch di integrazione, riconoscibili perché il nome inizia per feature dei branch di release, che nascono dal branch di integrazione, sui quali eventualmente possono essere fatti dei fix e che una volta effettuata la release vengono mergiati sia sul branch principale che sul branch di integrazione; il nome di questi branch inizia per release e contiene la versione della versione che si sta rilasciando dei branch di hotfix, che nascono dal branch principale, contengono singoli fix importanti per versioni già rilasciate, e che vengono poi mergiati sia sul branch principale che su quello di integrazione; il nome di questi branch inizia per hotfix   GitFlow Workflow   GitFlow è un flusso di lavoro ottimo per quei progetti software basati su rilasci di versioni e offre un canale dedicato per il rilascio di hotfix. D’altro canto, richiede molto impegno nella corretta gestione del name e utilizzo dei branch. Non a caso, dall’idea originale sono, poi, nate alcune estensioni per Git che offrono comandi dedicati per attuare i vari step di questo flusso. D’altro canto, c’è anche da considerare che GitFlow, per certi versi, si limita a fare in modo che la history e i commit del proprio repository siano “in linea” con i rilasci e le varie versioni. Non entra, quindi, nel merito nelle modalità con cui poi, rilasciare e distribuire un hotfix, ma permette solo di implementare e individuare in modo preciso il commit che corrisponde a una determinata versione. Consigliamo, comunque, di valutare bene tutte le implicazioni nell’uso di GitFlow prima di sceglierlo come modello risolutivo. Rimandiamo quindi al post originale dell’autore, Vincent Driessen, per maggiori approfondimenti e per la sua nota di review sull’uso di GitFlow a 10 anni dalla sua pubblicazione.

15

Convenzioni su commit message e tag in Git

15.1

Messaggi di commit in Git

Sebbene nel tempo si siano identificati con “git workflow” una serie di indicazioni relative all’uso di branch e strategie per il merge, non bisogna dimenticare che contribuiscono a un uso produttivo di Git anche strategie e convenzioni legati all’uso dei messaggi di commit e all’uso dei tag per il versionamento. Anche su tale aspetto Git non è prescrittivo, lasciando ai singoli scegliere il modo più adeguato. Messaggi di commit in Git Per quanto riguarda i messaggi di commit, è importante ricordare che Git permette di inserire un messaggio di commit formato di più righe, usando poi la prima riga del messaggio come “subject” (indicativo da questo punto è la differenza tra git log e git log --oneline). Un messaggio di commit per l’aggiunta di questa sezione potrebbe essere simile al seguente: Add commit message section in workflow chapter More specific info about how to write a commit message and some guidelines about why it is important to keep them consistent. We opted to suggest the following: - use short first line as summary (less than 72 chars, 50 is best) - wrap body lines to 72 chars (useful to read log on terminal) - use imperative form for first summary line: "Fix bug", not "Fixed bug" or "Fixes bug" - do the best to format body part to make it readable - use body part to describe how and why for the commit, as well as references to other commits, issues, ... - think if, in 6 months, you'll be able to understand the change just reading the commit message ;-) Complete and close issue #12 In breve: la prima riga del messaggio è la più importante le altre righe possono essere usate per chiarire riferimenti, motivazioni e limitazioni legate alle modifiche salvate nel commit, che magari non era necessario o possibile aggiungere direttamente nei file di progetto modificati è utile uniformarsi a uno stile comune e coerente per sapere cosa e come trovare informazioni nei messaggi di commit Una convenzione sui messaggi di commit abbastanza condivisa nel mondo dello sviluppo è quella che prende il nome di Conventional Commit (o Semantic Commit). Questa convenzione indica alcune semplici regole per creare una history dei commit esplicita (che tipo di modifica ho fatto con quel commit?) e interpretabile da script/automatizzazioni. Un messaggio di commit in linea con la specifica Conventional Commit è strutturato nella seguente forma: <type>[optional scope]: <description> [optional body] [optional footer(s)] e segue le seguenti regole: il <type> deve essere fix per quei commit che correggono un bug il <type> deve essere feat se il commit introduce una nuova funzione <type> diversi fix e feat sono consentiti, suggerendone alcuni (build, chore, ci, docs, style, refactor, perf, test), ma lasciando anche al team di definirne eventualmente di propri se il commit introduce breaking change, questi vanno segnalati iniziando il footer con BREAKING CHANGE: oppure inserendo ! dopo il type/scope nella prima riga. Se volessimo riscrivere il messaggio di commit d’esempio precedente con queste guideline, potremmo, per esempio, avere qualcosa di simile al seguente: feat(workflows): add commit messages section More specific info about how to write a commit message and some guidelines about why it is important to keep them consistent. - basic sane guidelines for commit messages - reference to conventional commit guidelines Refs: #2472 Reviewed-by: myself La scelta di scope e delle alte parti opzionali della convezioni sono ovviamente demandate al team, che può regolarsi secondo le proprie necessità. Per maggiori informazioni rimandiamo al sito ConventionalCommits.
15.2

Tagging & Versioning in Git

Sempre considerando che Git in sé non è prescrittivo nell’uso dei tag - sono etichette che puntano a un singolo commit - per una gestione efficiente di un progetto è opportuno definire alcune guideline relative all’uso dei tag in Git. Possiamo dire che i tag di Git sono abitualmente usati per indicare una determinata versione o rilascio del software. Le modalità specifiche possono dipendere da progetto a progetto. Per esempio, progetti che prevedono che la versione corrente sia salvata in un certo file possono avere un commit in cui viene salvata la versione e aggiungere a tale commit un tag con la stessa versione. Altri progetti possono taggare un commit qualsiasi come versione (ricordiamo sempre che un commit è uno snapshot). In modo simile a quanto visto poco sopra per i conventional commit, anche per il versionamento di un progetto software esistono diverse convenzioni, una di quelle più note è il semantic versioning o semver. Nel semantic versioning la versione è rappresentata da tre numeri, separati da un punto, ciascuno dei quali rappresenta come il nuovo rilascio o versione è cambiato rispetto al precedente, in particolare: il primo numero la major version, da incrementare quando il cambio non è retrocompatibile il secondo numero è la minor version, da incrementare quando si aggiungono funzioni ma è ancora retrocompatibile il terzo numero è la patch version, da incrementare quando si correggono bug in modo retrocompatibile Con questa convenzione possiamo quindi capire che: 0.2.0 - è un progetto nelle fasi iniziali di sviluppo (la versione major è 0), non è detto che cambierà in modo compatibile 1.0.0 - è la prima versione “ufficiale” 1.0.3 - è il terzo rilascio di fix della versione 1.0, non ci sono nuove funzioni rispetto alla 1.0, ma solo correzione di errori 1.1.0 - è il primo rilascio con nuove funzioni della versione 1, ma ancora compatibile con la versione 1 2.0.0 - è il primo rilascio non più compatibile con la versione 1 2.1.0-alpha.1 - è un pre-rilascio della futura versione 2.1.0 Maggiori informazioni sul semantinc versioning possono essere trovate su Semver. Dal punto di vista dei tag Git, nel caso in cui si seguisse il semantic versioning, il suggerimento è quello di usare come tag il numero di versione con davanti la lettera v. I tag in questo caso sarebbero quindi v0.2.0, v1.0.3, v1.1.0, v2.1.0-alpha.1 e così via. Ovviamente, i tag Git sono utili a contrassegnare quale commit (e quindi quale snapshot della codebase) corrisponde a una certa versione o rilascio, ma per poter eventualmente mantenere due “linee di sviluppo” (per esempio, lavorare alla versione 2 mentre si continua a fornire supporto alla versione 1) sarà necessario trovare altre modalità e workflow basate su altre funzioni di Git (per esempio, i branch)

16

Merging vs rebasing in Git

16.1

L’opzione merge in Git

Dopo aver introdotto i comandi git rebase in Git e git merge in Git e dopo aver introdotto possibili strategie di workflow, vediamo più nel dettaglio come e perché scegliere l’uno o l’altro comando in base al progetto, all'esigenza e alla finalità che si vuole raggiungere. Molto spesso, infatti, l’attività di rebase è etichettata come difficile e pericolosa per chi si avvicina a Git, ma, come abbiamo visto parlando dei vari workflow, è anche importante saper mantenere la history della propria working copy e del branch principale di sviluppo il più possibile pulita, al fine di comprendere poi meglio l’evoluzione del progetto. La prima cosa importante per poter confrontare merge e rebase è che i due comandi (o approcci) servono a risolvere lo stesso problema: integrare modifiche presenti su un branch all’interno di un altro branch.   Merge vs Rebase   L’opzione merge in Git Scegliendo l’opzione merge in Git per riportare e modifiche di main su feature, creiamo un merge commit sul branch feature, che “incorpora” le modifiche presenti sul branch principale e lega insieme i due branch. git merge feature main   Opzione Merge   L’operazione di merge è “non distruttiva”. Modifiche e commit preesistenti al merge non vengono alterati in alcun modo. D’altro canto, il branch feature conterrà un merge commit “estraneo” per ogni volta che sarà necessario incorporare modifiche dal branch principale. Se il branch feature vive molto a lungo e ci sono modifiche frequenti al branch main, sarà difficile comprendere l’esatta cronologia delle modifiche. Inoltre, se consideriamo uno sviluppo a “feature branch”, sarà sempre necessario pensare a cosa accadrà quando riporteremo il branch di feature sul ramo principale.  
16.2

L’opzione rebase in Git

Scegliendo l’opzione rebase in Git per riportare e modifiche di main su feature, l’intero branch feature viene “spostato”, ricreando i singoli commit che ne facevano parte. git checkout feature git rebase main Opzione Rebase Il vantaggio dell’opzione rebase in Git è proprio la possibilità di ottenere una history di progetto più chiara. Non esiste più il commit di merge e la storia del progetto risulta più ordinata (dal punto di vista di cosa è entrato a far parte del branch principale). Ci sono, però, due compromessi da tenere in considerazione: con il rebase non è più possibile sapere quando le modifiche del branch principale sono state riportate nel branch feature se non si segue la regola d’oro del rebase, bisogna essere pronti a gestire problemi di collaborazione tra web developers La regola d’oro del rebase in Git Mai fare rebase su un branch pubblico Per capire meglio il perché di questa regola d’oro, supponiamo che per incorporare le modifiche presenti nei due branch avessimo optato per fare rebase del branch main sul branch feature. Avremmo ottenuto la seguente situazione:   Opzione Rebase   Il rebase sposta tutti i commit precedentemente in main sulla punta di feature, creando nuovi commit. Ma ciò accade solo sul proprio repository locale. Dal momento che il rebase si traduce in commit nuovi di zecca, Git penserà che la cronologia del proprio ramo main si sia discostata da quello repository remoto e non sarà possibile fare push, se non usando l’opzione --force. Scegliendo di fare push con force, potremmo effettivamente riallineare il nostro repository locale e quello remoto, ma tutti gli altri collaboratori avranno ancora il branch main originale e non sarà loro possibile aggiornare in modo semplice i propri repository. Ovviamente, come tutte le regole d’oro, ci sono situazioni in cui è necessario doverla infrangere. Tutto dipende ovviamente dal progetto, dal team e dalla necessità. È possibile, infatti, accordarsi sul riscrivere la history di un feature branch pubblicato sul repository remoto prima di farne il merge il branch principale (per esempio perché il merge presenta dei conflitti e si vuole risolvere tali conflitti cambiando direttamente i commit nel branch che vanno in conflitto) e verificare se la risoluzione, effettivamente, lascia tutto funzionante. L’importante è sapere cosa si sta facendo e su chi, eventualmente, la cosa ha impatto.

Sei indeciso sul percorso? 💭

Parliamone! Scrivici su Whatsapp e risponderemo a tutte le tue domande per capire quale dei nostri corsi è il più adatto alle tue esigenze.

Oppure chiamaci al 800 128 626