Tools

Git Flow: come funziona aka Back to the future

Lettura 11 minuti

Aggiornato il 2 maggio 2019

I sistemi di controllo di versione consentono di tenere traccia delle modifiche fatte nel tempo e di collaborare più efficacemente all’interno di team composti da molte persone in particolare se, come in Bitbull, lavorano da remoto.

Non ci addentreremo nella spiegazione di che cos’è un sistema di controllo di versione, ma bensì vedremo come gestire al meglio il flusso di sviluppo con Git, il software di controllo versione distribuito.

Normalmente un repository è suddiviso in due branch, master e develop, dove master è il principale branch in cui risiede il codice della versione del progetto in produzione mentre develop è la versione con gli ultimi sviluppi che stanno per essere pubblicati ed è il branch normalmente attivo in ambiente di test (in seguito staging). Ogni modifica/aggiunta si fa su un feature branch generato da develop. Mentre gli hotfix, si fanno direttamente su master (e poi si allinea develop).

Vedremo come fare questo usando Git Flow che non è altro che una procedura per renderlo automatico ma con alcune cose in più e delle regole per farlo correttamente.

Prima di utilizzare git flow, si creavano dei branch da develop chiamati ad esempio “fix123” e la struttura visiva del repo risultava così:

- develop
- master
- fix123

In Bitbull, i feature branch eravamo soliti chiamarli già così: “feature/fix123” per cui la struttura visiva (e qui possono capire di più quelli che usano SourceTree) risultava nel modo seguente:

- develop
 |_ feature
   |_fix123
 - master

Dopo aver imparato git flow ho capito perché.

Quindi veniamo a git flow.

Premessa

Ogni progetto/repo pubblico o privato dovrebbe avere un tag per ogni versione che viene rilasciata nella forma x.y.z in modo tale da poter far riferimento a questo numero di versione ed essere in grado di “viaggiare nel tempo” tra una versione e l’altra a seconda di cosa ognuna contiene piuttosto che doversi ricordare un commit specifico. E per sapere cosa ogni versione contiene ce lo dice il release note o changelog (discusso in seguito).

Il numero di versione si costruisce usando il versionamento semantico:

MAJOR.MINOR.PATCH

dove

MAJOR serve ad indicare una versione non retrocompatibile con la precedente (una rottura con il passato, break)
MINOR è invece una modifica retrocompatibile (quindi una feature o più)
PATCH correzione di bug retrocompatibili (quindi un fix)

Per aiutarci potremo tenere a mente la terna:

break – feature – fix

Si intendono feature tutti quei task che prevedono aggiunta di funzionalità, normale svolgimento dei compiti, ecc; come hotfix, fix di bug urgenti che devono andare subito in produzione. Un fix può essere anche semplicemente l’aggiornamento di documentazione, commenti o riformattazione di codice, persino un refactoring che, per definizione, non altera il comportamento quindi non aggiunge alcuna nuova feature.

Capito questo è anche facile comprendere come per le feature serva una release mentre per gli hotfix no perché appunto vengono rilasciati direttamente in produzione senza passare dal via.

Come si fa

Per prima cosa va preparato il repo all’uso di git Flow.

Per chi usa SourceTree basta schiacciare un pulsante, per tutti gli altri: http://danielkummer.github.io/git-flow-cheatsheet/index.it_IT.html.
Su SourceTree cliccate sul pulsante Git Flow

che equivale a:

$ git flow init

Questo comando stabilisce le nomenclature da usare in git flow che di solito sono così:

Production branch -> master
Development branch -> develop
Prefissi per:
Feature branch -> feature/
Release branch -> release/
Hotfix branch ->hotfix
Tag version: qui si può lasciar vuoto oppure a qualcuno piace mettere “v” così che il tag di ogni versione avrà quest’aspetto: “v1.0.0”

Queste configurazioni le ritrovate nel file config situato nella cartella .git del progetto:

[gitflow "branch"]
  master = master
  develop = develop
[gitflow "prefix"]
  feature = feature/
  release = release/
  hotfix = hotfix/
  versiontag = v
  support = support/

quindi se serve modificare si può fare da qui (per coloro che usano il terminale un elenco di comandi da usare per aggiornare le config è disponibile su https://github.com/petervanderdoes/gitflow-avh/wiki/Reference:-git-flow-config)

Fatto ciò, se siamo partiti con un solo branch, ovvero master, verrà creato develop.

Siamo ora pronti ad usare il flow. La struttura del nostro repo diventa così:

develop
master

Feature vs Hotfix

Abbiamo dei nuovi task da fare sul progetto, cambiare un colore, migliorare una pagina, aggiungere una nuova funzione ad un modulo, lavoriamo quindi ad una feature che chiameremo “TASK”:

$ git flow feature start TASK

(in SourceTree pulsante gitflow > Start a new feature)

Il nuovo branch partirà automaticamente dall’ultimo commit di develop (è comunque possibile specificare un commit o una working copy revision dal quale deve partire), ci chiederà il nome del branch in cui scriviamo “TASK” e voilà ecco la nostra alberatura:

develop
 |_feature
   |_TASK
 master

Lavoriamo pertanto su questo branch, lo facciamo vedere al cliente, gli piace, ce lo conferma e siamo pronti a portarlo in develop:

$ git flow feature finish TASK


Per i palati più fini

Se non vuoi sporcare la storia con commit non semantici come “merge branch feature/foo into develop” puoi utilizzare rebase unitamente allo squash.
Ma tieni conto che questo prevede che il branch sia fatto con una certa logica. E ancora prima prevede che si abbia una certa dimestichezza con rebase e squash e capacità nel risolvere eventuali conflitti. Se così non fosse, limitati ad un gitflow finish senza opzioni.

Se però intendi procedere con rebase e squash, prima di chiuderlo assicurati che l’ultimo branch descriva in modo semantico la feature che stai andando a chiudere (es. che non sia un messaggio generico come “fix”) come spiegato nel precedente paragrafo. Sconsiglio il rebase quando la storia di develop rispetto a quella del branch è nel frattempo andata di molto avanti, perché potresti andare incontro ad un’infinità di conflitti che git flow da solo non può risolvere e serve un intervento manuale. Inutile dire che è un oneroso (e talvolta inutile) dispendio di tempo.

Ecco il comando rivisto con le opzioni appena spiegate:

$ git flow feature finish TASK -rS

r, che sta per rebase, riporta tutti i commit su develop di modo che la storia del repo sia aggiornata e non ci siano come detto dei brutti e insignificanti “merge”) e S, che sta per squash, equivale a sintetizzare tutti i commit della feature in uno solo (ricorda: git utilizza il messaggio dell’ultimo commit fatto sul feature branch – motivo per il quale quest’ultimo dovrebbe spiegare cosa è stato fatto in questo branch).


Chiusa la feature, il branch viene riportato su develop e viene cancellato solo dalla macchina locale. Da repository in remoto va fatto a mano o come dicono qua http://stackoverflow.com/questions/11151623/does-git-flow-deletes-branch-on-remote-server da terminale con -F. Aaaah pulizia!

Facciamo finta che questo branch basti e avanzi e lo si vuole ora portare in produzione. Occorre fare una release. Supponiamo che siamo alla versione 1.0.0 e dobbiamo rilasciare questa nuova feature:

$ git flow release start 1.1.0

Ecco che creiamo la release incrementando di 1 il secondo numero e riportando a ZERO il terzo.

L’alberatura:

develop
 master
 |_release
   |_1.1.0

In questo frangente, andiamo a modificare (se non presente lo andiamo a creare) la release note che può essere sotto forma di releasenote.txt o CHANGELOG.md nella root del repo. Questo file ha più o meno queste fattezze:

# Change Log

## 1.0.0
 - Version 1

(ispirato a http://keepachangelog.com/)

Nel quale vengono cioè riportate in punto elenco le modifiche fatte (si usa il markdown come markup) per cui ora qui va aggiunto un elenco di cose che questa release comprende. Siccome noi abbiamo fatto le modifiche che chiedeva il TASK scriveremo questo:

# Change Log

## 1.1.0
 - Ref. #xxxx Ho fatto il TASK

## 1.0.0
 - Version 1

Si elencano le modifiche fatte (per convenzione in Bitbull si usa aggiungere anche una referenza al task) e si fa il commit.

FATTO. La release è pronta. Prima di chiuderla è buona norma fare un giro su tutto il sito e verificare che funzioni e non si sia rotto niente. Se è tutto ok possiamo chiudere la release:

$ git flow release finish 1.1.0

Ecco come appare su SourceTree:

Si spunta sempre delete branch perché non è necessario mantenere questo branch (oltretutto teoricamente è un’operazione che fa una persona sulla macchina in locale aprendo e chiudendo quindi non viene nemmeno fatto il push) e si scrive il commento del tag (non è particolarmente rilevante, reperibile soltanto in git show <tag>, come convenzione mettiamo “close release”).

La chiusura della release fa queste operazioni:

  • fa il merge della release in master
  • crea un tag con il nome della release (1.1.0)
  • fa il merge della release in develop
  • cancella il branch della release

Facciamo push di tutto (develop e master) sul repo online:

$ git push origin develop master --tags

Se si va a guardare il repo su bitbucket (o direttamente in SourceTree) si vedrà che nel branch master c’è un bellissimo tag con il nome della nuova versione:

Ed ecco la nuova versione del progetto!

URGENT bug da fixare!!!!1!

Oh no, abbiamo appena deployato il tutto su produzione ma è uscito un bug da fissare subito. Dobbiamo perciò fare un hotfix al volo!

$ git flow hotfix start 1.1.1

Il bug da fissare riguarda questa release appena lasciata per cui si incrementa l’ultimo numero della versione (che è quello appunto destinato ai bug fix)

Come si vede anche dall’immagine, il branch dell’hotfix partirà automaticamente da master senza possibilità di scelta, qui a differenza della feature, SourceTree vi chiede non il nome ma il numero della versione. Per non sbagliarsi e sapere quale usare basta guardare qual è l’ultimo numero presente nella release note / changelog e incrementare l’ultimo numero.

L’alberatura:

develop
 master
 |_hotfix
   |_1.1.1

Eseguiamo le modifiche, ci assicuriamo che funzioni tutto e aggiorniamo la release note:

# Change Log

## 1.1.1
 - Ref. #xxxx Fixato il bug

## 1.1.0
 - Ref. #xxxx Ho fatto il TASK

## 1.0.0
 - Version 1

si esegue il commit e si chiude l’hotfix:

Non vengono chieste altre informazioni.

La chiusura dell’hotfix fa queste operazioni:

  • fa il merge dell’hotfix in master
  • crea un tag con il nome dell’hotfix (1.1.1)
  • fa il merge dell’hotfix in develop
  • cancella il branch dell’hotfix

Facciamo push di tutto (develop e master) sul repo online:

$ git push origin develop master --tags

Notare che in questo caso non serve fare una release perché la modifica viene già riportata su master e develop alla chiusura del branch.

Bello. Ora abbiamo questa nuova versione di cui si può fare facilmente pull (ad esempio per aggiornare staging dopo il deploy):

$ git checkout tags/1.1.1

La stessa cosa si poteva fare eseguendo il pull di develop, ma l’utilità dei tag si ha nei casi in cui il cliente ci dice “no aspetta non siamo pronti, ci mancano i seguenti prodotti e non possiamo ancora andare online con queste feature”. A quel punto allora si ritorna alla versione di prima come se non fosse successo niente con:

$ git checkout tags/1.0.0

Figo no??

Altri esempi

Più persone stanno lavorando sul repo oppure sei da solo, ma stai lavorando su più task che dovranno essere rilasciati nel prossimo deploy:

develop
 |_feature
   |_TASK1
   |_TASK2
   |_TASK3
 master

e supponiamo che il cliente li abbia visti tutti ed approvati si procederà ad aggiornare la release note PRIMA della chiusura della feature e lo si farà così:

# Change Log

## unreleased
 - Ref. #xxxx Fatto TASK1

## 1.1.1
 - Ref. #xxxx Fixato il bug

## 1.1.0
 - Ref. #xxxx Ho fatto il TASK

## 1.0.0
 - Version 1

utilizzando il titolino “unreleased” di modo che, quando tutte saranno chiuse e riportate su develop, si avrà un merge di questo file automatico che riporterà tutte le modifiche fatte in questo frangente:

# Change Log

## unreleased
 - Ref. #xxxx Fatto TASK1
 - Ref. #xxxx Fatto TASK2
 - Ref. #xxxx Fatto TASK3

## 1.1.1
 - Ref. #xxxx Fixato il bug

## 1.1.0
 - Ref. #xxxx Ho fatto il TASK

## 1.0.0
 - Version 1

A questo punto sarà possibile creare la nostra release con la procedura già descritta ed aggiornare il release note con il numero di versione definitivo per il deploy:

# Change Log

## 1.2.0
 - Ref. #xxxx Fatto TASK1
 - Ref. #xxxx Fatto TASK2
 - Ref. #xxxx Fatto TASK3

## 1.1.1
 - Ref. #xxxx Fixato il bug

## 1.1.0
 - Ref. #xxxx Ho fatto il TASK

## 1.0.0
 - Version 1

Notare che, essendo passati ad una nuova feature viene azzerato il numero destinato agli hotfix.

Può accadere che si stia lavorando su un feature branch complesso che potrebbe finire in successivi deploy ma non nel prossimo. È buona regola perciò in questo caso, ad ogni deploy rilasciato in produzione, fare il merge di develop nel feature branch su cui si sta lavorando di modo da risolvere eventuali conflitti direttamente in questo branch e testare che tutto funzioni anche con le nuove feature rilasciate parallelamente. Questo impedisce anche di ritrovarsi alla chiusura di questa feature parecchie release dopo e dover risolvere moltitudine di conflitti durante quest’operazione che invece dovrebbe scorrere abbastanza liscia.

Domande? Dubbi? Perplessità?

Articolo scritto da

Lorena Ramonda

☝ Ti piace quello che facciamo? Unisciti a noi!