No. 224

Mi ricordo piuttosto bene il 10 Gennaio 2010: quello fu il giorno in cui perdemmo la cronologia completa di un progetto. Come version control system stavamo usando Subversion, che teneva la cronologia del progetto in un repository centrale su un server e noi facevamo regolarmente il backup di questo server, o perlomeno pensavamo fosse così. Poi, il server si ruppe e il backup non era andato a buon fine. Il nostro progetto non era completamente perso, ma tutte le versioni storiche erano andate.

Migrammo a Git poco dopo la rottura del server. Avevo sempre visto il version control come una tortura: era troppo complesso e non abbastanza utile perché potessi vederne il valore, ma lo usavo comunque per dovere. Però, dopo aver passato un po' di tempo sul nuovo sistema, cominciai a capire quanto potesse essere utile Git. Da allora, mi ha salvato la pelle in molte situazioni.

Nel corso di questo articolo, vi mostrerò come Git possa aiutarvi ad evitare gli errori e come rimediarvi se sono già successi.

Ogni collega del team è un backup

Dal momento che Git è un version control system distribuito, ogni membro del vostro team che ha clonato un progetto (o che ha fatto “check out” se venite da Subversion) ha automaticamente un backup sul proprio disco. Questo backup contiene la versione più recente del progetto così come la sua storia completa.

Questo significa che se la macchina locale di uno sviluppatore o addirittura il nostro server centrale dovessero mai rompersi ancora (e il backup non funzionare per una ragione qualsiasi), saremmo di nuovo “up and running” nel giro di pochi minuti: ogni repository locale dal disco di un collega del team è tutto quello di cui abbiamo bisogno per una sostituzione completamente funzionante.

I branch mantengono separate quel che deve rimanere separato

Non sono subito impazzito di gioia quando i miei colleghi più tecnici mi avevano parlato di quanto fosse “cool” il branching in Git. Primo, devo ammettere che non avevo davvero compreso i vantaggi del branching e, secondo, venendo da Subversion, mi ricordavo fin troppo bene quanto la procedura fosse complessa e quanto facile commettere errori. Dati questi brutti ricordi, l'idea di lavorare con i branch mi metteva ansia e quindi avevo provato ad evitarli quando era possibile.

Mi ci è voluto un po' di tempo per comprendere che il branching e il merging funzionano in maniera completamente differente in Git rispetto alla maggior parte degli altri sistemi, specialmente per la loro facilità di utilizzo! Quindi, se avete imparato il concetto di branching da un altro version control system (come Subversion), vi consiglio di dimenticarvi questa vostra conoscenza pregressa e di iniziare da zero. Cominciamo innanzitutto dal capire perché i branch sono così importanti.

Perché i branch sono essenziali

Quando in passato non usavo i branch, lavorare su una nuova feature era un disastro. Praticamente, potevo scegliere tra due workflow, entrambe pessimi:

(a) Sapevo già che fare dei piccoli commit granulari con solo pochi cambiamenti era una buona abitudine di version control. Tuttavia, se lo facevo mentre stavo sviluppando una nuova feature, ogni commit mischiava la mia feature completa a metà con la code base principale fino a che non avevo finito. Per i miei colleghi, non era piacevole trovare dei bug nel progetto introdotti dalla mia feature non ancora completata.

(b) Per evitare di mischiare il mio “work in progress” con altri argomenti (sia dei miei colleghi sia miei), lavoravo su una feature in un mio spazio separato. Creavo una copia della cartella del progetto con cui avrei potuto lavorare tranquillamente e facevo poi il commit una volta completata la mia feature. Ma fare il commit dei miei cambi solo alla fine produceva un unico commit gigante, riempito di tutti i cambiamenti. Né io né i miei colleghi potevamo capire cosa fosse successo esattamente in questo commit quando ci saremmo tornati in un secondo momento.

Pian piano, capii che dovevo familiarizzare con i branch se volevo migliorare le mie capacità di programmatore.

Lavorare nei contesti

Ogni progetto ha più contesti in cui avviene il lavoro: ogni feature, bug fix, esperimento o alternativa del prodotto è in effetti un contesto a sé stante. Può essere visto come un suo “topic” chiaramente separato dagli altri topic.

Se non separate questi topic tra loro con il branching, inevitabilmente, aumenterete il rischio di avere problemi. Mischiare diversi topic nello stesso contesto:

  • rende difficile mantenere una visione globale e con molti topic, diventa quasi impossibile;
  • rende difficile annullare qualcosa si è scoperto contenere un bug, perché è già mischiata a molte altre cose;
  • non incoraggia a sperimentare e a provare cose nuove, perché è difficile togliere il codice sperimentare dal repository una volta che è mischiato con il codice stabile.

L'utilizzo dei branch mi ha dato la sicurezza che non avrei distrutto nulla. Nel caso le cose prendano una brutta piega, potrei sempre tornare indietro, annullare e ricominciare, oppure cambiare il contesto.

Branching: le basi

Il branching in Git coinvolge solo qualche comando. Per cominciare, osserviamo il workflow di base.

Per creare un nuovo branch basandosi sullo stato corrente, tutto quello che dovete fare è scegliere un nome ed eseguire un unico comando da riga di comando. Supponiamo di voler cominciare a lavorare su una nuova versione della nostra form di contatto e quindi creiamo un branch chiamato “contact-form”:

$ git branch contact-form

Usando il comando git branch senza specificare un nome farà elencare tutti i branch che abbiamo al momento (e la flag “-v” ci fornisce pochi dati in più rispetto al solito):

$ git branch -v
Schermata di Git che mostra i branch attuali di contact-form.

Potreste ora aver notato il piccolo asterisco sul branch di nome “master”: esso indica il branch attualmente attivo. Quindi, prima di cominciare a lavorare sulla nostra contact form, dobbiamo rendere quest'ultimo il contesto attivo:

$ git checkout contact-form

Adesso Git ha reso questo branch il contesto di lavoro attuale. (Nel gergo di Git, questo viene chiamato “HEAD branch”). Tutti i cambiamenti e qualunque commit faremo d'ora in poi influenzerà solo quest'unico contesto, gli altri contesti non verranno toccati. Se vogliamo cambiare il contesto ad un altro branch, dobbiamo semplicemente usare di nuovo il comando git checkout.

Nel caso vogliamo integrare i cambiamenti da un branch in un altro, possiamo farne il “merge” nel contesto di lavoro attuale. Immaginate che abbiamo lavorato sulla nostra feature “contact-form” per un po' e adesso vogliamo integrare questi cambiamenti nel nostro branch “master”. Tutto quello che dobbiamo fare è tornare a questo branch e chiamare git merge:

$ git checkout master
$ git merge contact-form

Usare i branch

Vi consiglio caldamente di usare i branch il più possibile nel vostro workflow quotidiano. I branch sono uno dei concetti chiave su cui è costruito Git. Sono estremamente economici e facili da creare, nonché semplici da gestire e ci sono moltissime risorse se siete pronti per imparare di più sul loro utilizzo.

Annullare azioni

C'è una cosa che ho imparato come programmatore nel corso degli anni: gli errori si fanno, indipendentemente da quanta esperienza si abbia. Non si possono evitare ma si possono tenere a portata di mano degli strumenti che ci aiutino a recuperare.

Una delle feature migliori di Git è che si può fare l'undo di quasi tutto. Questo mi ha dato la fiducia necessaria per provare cose senza paura, perché finora non sono riuscito a distruggere davvero qualcosa oltre il recovery.

Correggere l'ultimo commit

Anche se fate i vostri commit molto attentamente, è fin troppo facile dimenticarsi di aggiungere un cambio o scrivere male un messaggio. Con la flag —amend del comando git commit, Git ci permette di cambiare l'ultimissimo commit ed è una fix molto semplice da eseguire. Per esempio, se vi siete dimenticati di aggiungere una certa modifica e avete anche fatto un errore di battitura nell'oggetto del commit, potete facilmente correggerli:

$ git add some/changed/files
$ git commit --amend -m "The message, this time without typos"

C'è solo una cosa che dovreste tenere a mente: non dovreste mai fare l'amend di un commit che è già stato inviato a un repository remoto. Rispettando questa regola, l'opzione "amend" è un grande piccolo aiutante per sistemare l'ultimo commit.

(Per ulteriori dettagli sull'opzione amend, vi raccomando l'eccellente guida di Nick Quaranto).

Fare l'undo dei cambiamenti locali

I cambiamenti di cui non si è ancora fatto il commit sono detti "locali". Tutte le modifiche che sono attualmente presenti nella vostra directory di lavoro sono cambiamenti “local” non "committed".

Scartare questi cambiamenti può anche aver senso quando il vostro lavoro attuale è… beh… peggiore di quello che avevate prima. Con Git, potete facilmente fare l'undo dei cambiamenti locali e cominciare da capo con l'ultima versione del vostro progetto di cui avete fatto il commit.

Se volete ripristinare un singolo file, potete usare il comando git checkout:

$ git checkout -- file/to/restore

Non confondete questo utilizzo del comando checkout con il cambio dei branch (vedi sopra). Se lo usate con due trattini (separati con uno spazio!) e il percorso del file, eliminerà i cambiamenti di cui non si è fatto il commit in un dato file.

Tuttavia, in una giornata cattiva, potreste addirittura voler eliminare tutti i cambiamenti locali e ripristinare l'intero progetto:

$ git reset --hard HEAD

Questo rimpiazzerà tutti i files nella vostra directory di lavoro con l'ultima revisione di cui avete fatto il commit. Proprio come l'utilizzo del comando checkout sopra, questo eliminerà i cambiamenti locali.

State attenti con queste operazioni: dal momento che non avete fatto il check in un repository dei cambiamenti locali, non c'è modo di riaverli una volta eliminati!

Fare l'undo dei cambiamenti di cui si è fatto il commit

Ovviamente, l'annullamento delle cose non è limitato solo ai cambiamenti locali. Potete anche fare l'undo di alcuni commit quando è necessario, per esempio, se avete introdotto un bug.

Praticamente, ci sono due comandi principali per fare l'undo di un commit:

(a) git reset

Illustrazione che mostra come funziona il comando `git reset`.

Il comando git reset fa davvero tornare indietro nel tempo. Gli dite a che versione volete tornare ed esso ripristina esattamente questo stato, annullando tutti i cambiamenti che sono avvenuti dopo questo punto nel corso del tempo. Semplicemente, dategli lo "hash ID" del commit a cui volete tornare:

$ git reset -- hard 2be18d9

L'opzione —hard è l'approccio più semplice e pulito, ma elimina anche tutti i cambiamenti locali che potreste ancora avere nella vostra directory di lavoro. Quindi, prima di fare così, assicuratevi che non ci sia alcun cambiamento locale che desiderate ardentemente.

(b) git revert

Illustrazione che mostra il funzionamento di `git revert`.

Il comando git revert è usato in uno scenario differente. Immaginatevi di avere un commit che non volete più, ma i commit che sono venuti dopo sono ancora importanti per voi. In questo caso, non dovrete usare il comando git reset perché annullerebbe anche tutti i commit successivi!

Tuttavia, il comando revert fa il revert solo degli effetti di un certo commit. Non rimuove alcun commit, come invece fa git reset. Al contrario, crea addirittura un nuovo commit, il quale introduce i cambiamenti che sono solo l'opposto del commit che deve essere ripristinato. Per esempio, se avete cancellato una certa riga di codice, revert creerà un nuovo commit che introduce di nuovo proprio questa riga.

Per usarlo, passategli semplicemente lo "hash ID" del commit di cui volete fare il ripristino:

$ git revert 2be18d9

Trovare i bug

Quando si tratta di scovare i bug, devo ammettere di aver passato molto tempo brancolando nel buio. Spesso sapevo che un paio di giorni prima funzionava, ma non avevo idea in che punto esattamente le cose fossero andate storte. Fu solo quando scoprii git bisect che il processo cominciò a velocizzarsi un po'. Con il comando bisect, Git fornisce uno strumento che ci aiuta a trovare il commit che ha introdotto un problema.

Immaginatevi questa situazione: sappiamo che la nostra versione attuale (taggata con “2.0”) ha dei problemi. Sappiamo anche che un paio di commit fa (la nostra versione “1.9”) tutto andava bene. Il problema deve essere successo da qualche parte fra questi due punti.

Illustrazione che mostra i commit tra le versioni funzionante e non funzionante.

Questa informazione è già sufficiente per cominciare la nostra caccia al bug con git bisect:

$ git bisect start
$ git bisect bad
$ git bisect good v1.9

Dopo aver iniziato il processo, avevamo detto a Git che il nostro commit attuale conteneva il bug e pertanto è “bad”. Poi, abbiamo anche detto a Git quale commit precedente funziona effettivamente (come parametro per git bisect good).

Quindi, Git ripristina il nostro progetto a metà tra le condizioni note di bad e good:

Illustrazione che mostra che il bisect inizia tra le versioni.

Adesso, testiamo questa versione (per esempio, facendo degli unit test, creando l'app, facendone il deploy su un sistema di testing, etc.) per trovare se questo stato funziona, o se contiene già il bug. Non appena lo scopriamo, lo diciamo di nuovo a Git, o con git bisect bad o git bisect good.

Supponiamo di aver detto che questo commit era ancora “bad”. Questo significa effettivamente che il bug deve essere stato introdotto ancora prima e Git restringerà i commit in questione:

Illustrazione che mostra il modo in cui dei bisect in più fanno diminuire ulteriormente i commit.

In questo modo, troverete rapidamente il punto esatto in cui è successo il problema. Una volta che lo saprete, dovrete chiamare git bisect reset per terminare la caccia al bug e ripristinare lo stato originale del progetto.

Uno strumento per salvarvi la pelle

Devo confessare che il mio primo incontro con Git non è stato amore a prima vista. All'inizio, sembrava proprio come tutte le altre esperienze con il version control: tedioso e inutile. Ma col tempo, la pratica è diventata intuitiva e si è guadagnato la mia fiducia.

Dopo tutto, gli errori capitano, non importa quanta esperienza abbiate o quanto duramente cerchiamo di evitarli. Quello che separa i professionisti dai principianti è la preparazione: aver configurato un sistema su cui fare affidamento in caso di problemi. Vi aiuta a stare aggiornati sulle cose, specialmente in progetti complessi. E, in ultima analisi, vi aiuta a diventare professionisti migliori.

Riferimenti

Illustrazioni: Carlo Brigatti

Share/Save/Bookmark
 

Discutiamone

Ti sembra interessante? Scrivi tu il primo commento


Cenni sull'autore

Tobias Günther

Tobias Günther è l'autore del libro disponibile gratuitamente online, Learn Version Control with Git – A step-by-step guide for the complete beginner. Fa anche parte del team che sta dietro a Tower, il popolare client Git per Mac. Trovate Tobias su Twitter come @gntr.

Questo sito per poter funzionare utilizza i cookie. Per saperne di più visita la pagina relativa all' INFORMATIVA