HOWTO: Il modello di branching di Git

12 minute read

“Questo post è la traduzione del post originale ‘A successful Git branching model’ di Vincent Driessen.” Se trovi errori o incomprensioni nella traduzione ti prego di farmelo presente. Lo correggero prima possibile.

In questo post ti presento il modello di sviluppo che ho introdotto per tutti i miei progetti (sia sul lavoro e privato) circa un anno fa, e che ha rivelato un grande successo. Sono sempre stato motivato a scrivere su Git, ma non ho mai trovato il tempo per farlo bene, fino ad ora. Non voglio parlare di uno qualsiasi dei dettagli dei progetti ma solo sulla strategia di branching e della gestione delle release.

Ci concentreremo su Git come strumento di versione per tutti i nostri sorgenti

Perché Git?

Per una discussione approfondita sui pro ed i contro di Git rispetto ai sistemi centralizzati di revisione del codice sorgente, vedi il web. Ci sono un sacco di flame là. Come sviluppatore, preferisco Git rispetto a tutti gli altri strumenti in giro oggi. Git ha veramente cambiato il modo di pensare degli sviluppatori riguardo il merging ed il branching. Dal classico mondo CVS/Subversion da cui provengo, il merging/branching è sempre stato considerato un po’ inquietante (“Attenzione ai conflitti del merge, ti mordono!”), come qualcosa da fare una volta ogni tanto.

Ma con Git, queste azioni sono estremamente economiche e semplici, e sono considerate una delle parti fondamentali del flusso di lavoro giornaliero, davvero. Per esempio, nei libri di CVS/Subversion, branching e merging sono discussi solo negli ultimi capitoli (per utenti avanzati), mentre in ogni libro di Git già rientra nel capitolo 3 (basi).

Come conseguenza della sua semplicita e natura ripetitiva, branching e merging non sono più qualcosa di cui aver paura. Strumenti di controllo di versione sono tenuti a contribuire al branching e merging più di ogni altra cosa.

Basta sugli strumenti, entriamo nel merito sul modello di sviluppo. Il modello che ho intenzione di presentare qui è sostanzialmente non più di un insieme di procedure che ogni membro del team deve seguire al fine di giungere ad un processo di sviluppo software gestito.

Decentralizzato ma centralizzato

La configurazione repository che usiamo e che funziona bene con questo modello di branching, è quella con un repo centrale “vero”. Si noti che questo repo è considerato solo per essere quello centrale (dato che Git è un DVCS, non c’è nessuna cosa considerata come un repo centrale a livello tecnico). Si farà riferimento a questo repository come origin, dal momento che questo nome è familiare a tutti gli utenti Git.

Ogni sviluppatore compie i propri push e pull in origin. Ma oltre le relazioni centralizzata push-pull, ogni sviluppatore può effettuare i pull di modifiche da altri peers e formare sottosquadre. Ad esempio, questo potrebbe essere utile per lavorare insieme a due o più sviluppatori su una grande novità, prima di effettuare il push prematuro dei lavori in corso in origin. Nella figura sopra, ci sono sottosquadre di Alice e Bob, Alice e Davide, e Clair e David.
Tecnicamente, questo non significa niente più che Alice ha definito un Git remoto, di nome bob, che punta al repository di Bob, e viceversa.

I branch principali

Nel core, il modello di sviluppo è fortemente ispirato ai modelli già esistenti precedentemente. Il repository centrale ha due branch principali, con durata infinita:

  • master
  • develop

Il branch master in origin dovrebbe essere familiare ad ogni utente Git. Parallelamente al branch master, ne esiste un altro chiamato develop.

Consideriamo origin/master come branch principale dove il codice sorgente di HEAD rispecchia sempre lo stato di produzione.

Consideriamo origin/develop come branch principale dove il codice sorgente di HEAD rispecchia sempre uno stato con gli ultimi cambiamenti di sviluppo consegnati per la prossima release. Qualcuno vorrebbe chiamare questo branch “integration branch” (branch di integrazione). Questo è da dove ogni automatica build giornaliera viene compilata.

Quando il codice sorgente nel branch develop raggiunge un punto stabile ed e pronto per essere rilasciato, tutti i cambiamenti dovrebbero essere portati nel master in qualche modo e poi “taggati” con il numero di release. Come fare questo verrà discusso più avanti.

Quindi, ogni volta che i cambiamenti sono portati nel master, per definizione questa è la nuova release di produzione. Tendiamo ad essere molto severi in questo, in modo che, teoricamente, si potrebbe usare uno hook script di Git per fare il build e il roll-out del nostro software nei server di produzione ogni volta che c’è un commit nel master.

I branch di supporto

Accanto ai branch principali master e develop, il nostro modello di sviluppo usa una varieta di branch di supporto per aiutare lo sviluppo parallelo tra i membri del team, facilitare il tracking delle funzionalità, preparare le release di produzione e contribuire a risolvere i problemi di produzione in maniera rapida. A differenza dei branch principali, questi hanno sempre una durata limitata, in quanto saranno rimossi alla fine della loro vita.

I diversi branch che possiamo utilizzare sono i seguenti:

  • Feature branches: branch di nuove features
  • Release braches: branch per i rilasci
  • Hotfix branches: branch per la risoluzione di problemi

Ognuno di questi branch ha degli scopi precisi e sono limitati a delle regole severe, ovvero da dove dovrebbero essere generati e dove devono essere rimergiati.. Li analizzeremo tra poco.

In nessun caso sono branch “speciali” dal punto di vista tecnico. I tipi di branch sono suddivisi in categorie dipendentemente da come li usiamo.

Feature branches

Si dovrebbero formare da: develop Devono essere rimergiati in: develop I nomi che solitamente si usano: tutti tranne master, develop, release-*, hotfix-*

Feature branches (a volte chiamati topic branches) sono usati per sviluppare nuove features per release future più o meno lontane. Quando si parte allo sviluppo di una nuova feature, la release di destinazione in cui la funzionalità dovra essere incorporata potrebbe essere sconosciuta fino a quel momento. L’essenza del feature branch e che esiste finché la funzionalità è in sviluppo, fin quando eventualmente sarà rimergiata in develop (per aggiungerla definitivamente alla prossima release) oppure scartata (esperimento fallito).

Feature branch esiste tipicamente solo nel repository dello sviluppatore, non in origin.

Creazione di un feature branch

Quando si inizia a lavorare su una nuova feature, si dirama dal branch develop

$ git checkout -b myfeature develop  
Switched to a new branch "myfeature"

Incorporare una feature terminata nel develop

Le funzionalità terminate dovrebbero essere fuse nel branch develop in modo da aggiungerle nella prossima release:

    
$ git checkout develop  
Switched to branch 'develop'  

$ git merge --no-ff myfeature  
Updating ea1b82a..05e9557  
(Summary of changes)  

$ git branch -d myfeature  
Deleted branch myfeature (was 05e9557).  

$ git push origin develop

Il flag \--no-ff causa il merge per creare sempre un nuovo oggetto commit, anche se il merge potrebbe essere eseguito con un fast-forward. Ciò evita la perdita di informazioni circa l’esistenza storica del feature branch e raggruppa tutti i commit che hanno aggiunto questa feature. Confronta:

In quest’ultimo caso, è impossibile vedere dalla storia di git che l’insieme di commit hanno implementato una nuova funzionalità - si dovrebbero leggere manualmente dai messaggi di log. Il ripristino di una intera funzionalità (cioe un gruppo di commit) fa venire il mal di testa in questo ultimo caso, mentre è presto fatto se è stato usato il flag \--no-ff.

Si, verranno creati un po’ più oggetti commit (vuoti), ma il guadagno è molto più grande del costo.

Sfortunatamente, non ho trovato un modo per avere il \--no-ff come comportamento di default per il git merge, ma in realta dovrebbe esserlo.

Release branches

Si dovrebbero formare da: develop
Devono essere rimergiati in: develop e master
I nomi che solitamente si usano: release-

I branch di release supportano la preparazione per una nuova release di produzione. Essi consentono gli aggiustamenti e le rifiniture dell’ultimo minuto (n.d.r. mettere i puntini sulle i). Inoltre, essi consentono le correzioni di bug minori e la preparazione di meta-dati per un rilascio (numero di versione, build date, ecc.). Facendo tutto questo lavoro su un release branch, il branch develop è autorizzato a ricevere funzionalità per la prossima grande release.

Il momento chiave per creare un branch di release branch da develop è quando develop rispecchia (quasi) lo stato desiderato della nuova release. Almeno tutte le caratteristiche previste per la release-da-buildare devono essere mergiate in develop a questo punto nella timeline. Tutte le caratteristiche previste per le versioni future, non possono/devono aspettare fino a dopo che il release branch è stato formato.

Ed è proprio all’inizio di un release branch che alla prossima release viene assegnato un numero di versione - nessuno precedentemente. Fino a quel momento, il branch develop rispecchiava i cambiamentei per la “prossima release”, ma non è chiaro se questa “prossima release” diventera eventualmente un 0.3 oppure una 1.0, fino a quando release branch sarà avviato. Tale decisione e effettuata all’inizio della release branch ed il numero e assegnato secondo le regole del progetto.

Creare un release branch

I release branch sono creati dal branch develop. Per esempio, diciamo che la versione 1.1.5 e la release di produzione corrente ed abbiamo una nuova grande release che sta arrivando. Lo stato di develop è pronto per la “prossima release” ed abbiamo deciso che questa diventera la versione 1.2 (invece che la 1.1.6 oppure 2.0). Allora creiamo un branch e diamo al release branch un nome che rispecchi il nuovo numero di versione:

$ git checkout -b release-1.2 develop  
Switched to a new branch "release-1.2"  

$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.  

$ git commit -a -m "Bumped version number to 1.2"  
[release-1.2 74d9424] Bumped version number to 1.2  
1 files changed, 1 insertions(+), 1 deletions(-)

Dopo la creazione del nuovo branch e dopo aver cambiato in esso, gli diamo il numero di versione. Qui, bump-version.sh è un finto script di shell che cambia qualche file nella working copy per rispecchiare la nuova versione. (Questo può essere fatto anche manualmente - il punto è che alcuni file cambiano). Successivamente, la versione numerata è committata.

Questo nuovo branch può esistere per un po’ di tempo, finché la release non verrà lanciata definitivamente. Durante questo intervallo, i bug fix possono essere applicati a questo branch (anziche al branch develop). Aggiungere nuove grandi feature è severamente vietato. Devono essere mergiate nel develop, e quindi, aspettare la prossima grande release.

Finire un release branch

Quando lo stato del release branch è pronto per diventare una vera release, è necessario eseguire qualche azione. Primo, il release branch è mergiato nel master (ricorda che ogni commit nel master è una nuova release per definizione). Successivamente, quel commit nel master deve essere taggato per una facile consultazione futura a questa versione storica. Alla fine, i cambiamenti fatti nel release branch neccessitano di essere rimergiati nel develop, così le future release contengono questi bug fix.

I primi due step in Git:

$ git checkout master  
Switched to branch 'master'  

$ git merge --no-ff release-1.2  
Merge made by recursive.  
(Summary of changes)  

$ git tag -a 1.2

La release è creata e taggata per una futura consultazione.
Edit: Puoi usare anche il flag -s oppure -u per firmare crittograficamente i tuoi tag.

Per mantenere i cambiamenti apportati nel release branch, dobbiamo mergiarli nel develop. In Git:

$ git checkout develop
Switched to branch 'develop'  

$ git merge --no-ff release-1.2  
Merge made by recursive.  
(Summary of changes)

Questo passaggio può anche portare ad un conflitto di merge (probabilmente anche dal fatto che abbiamo cambiato il numero di versione). Se così fosse, correggilo e committa.
A questo punto abbiamo veramente finito ed il release branch può essere rimosso dal momento che non ne abbiamo più bisogno:

$ git branch -d release-1.2 
Deleted branch release-1.2 (was ff452fe).

Hotfix branches

Si dovrebbero formare da: master
Devono essere rimergiati in: develop e master
I nomi che solitamente si usano: hotfix-

Gli hotfix branch sono molto simili ai release branch in quanto hanno anche lo scopo di preparare una versione nuova produzione, anche se non pianificata. Essi nascono dalla necessità di agire immediatamente, a uno stato indesiderato di una versione di produzione in circolo. Quando un bug critico in una versione di produzione deve essere risolto immediatamente, un hotfix branch può essere branchato fuori dal tag corrispondente sul branch principale che contraddistingue la versione di produzione.

L’essenza è che il lavoro dei membri del gruppo (sul branch develop) può continuare, mentre un’altra persona sta preparando una soluzione rapida per la produzione.

Creare un hotfix branch

Gli Hotfix branch vengono creati dal branch master. Ad esempio, diciamo che la versione 1.2 è la release di produzione attualmente in esecuzione e sta avendo problemi a causa di un errore grave. I cambiamenti sul develop sono ancora instabili. Possiamo creare un hotfix branch ed iniziare a fixare il problema:

$ git checkout -b hotfix-1.2.1 master  
Switched to a new branch "hotfix-1.2.1"  
    
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.  
    
$ git commit -a -m "Bumped version number to 1.2.1"  
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1  
1 files changed, 1 insertions(+), 1 deletions(-)

Non dimenticate di eliminare il numero di versione dopo aver creato il branch! Successivamente, correggi il bug e committa la correzione in uno o più commit.

$ git commit -m "Fixed severe production problem" 
[hotfix-1.2.1 abbe5d6] Fixed severe production problem  
5 files changed, 32 insertions(+), 17 deletions(-)

Finire un hotfix branch

Alla fine, i bugfix devono essere rimergiati nel master, ma anche nel develop, in modo da garantire che il bugfix è incluso nella prossima release. Questo è del tutto simile a come finire i release branch.

Prima di tutto, aggiorniamo il master e tagghiamo la release:

$ git checkout master  
Switched to branch 'master'  
 
$ git merge --no-ff hotfix-1.2.1  
Merge made by recursive.  
(Summary of changes)$ git tag -a 1.2.1

Edit: Puoi usare anche il flag -s oppure -u per firmare crittograficamente i tuoi tag.

Successivamente, includiamo il bugfix in develop:

$ git checkout develop
Switched to branch 'develop'  

$ git merge --no-ff hotfix-1.2.1  
Merge made by recursive.  
(Summary of changes)

L’unica eccezione alla regola è che, quando esiste un release branch, i cambiamenti di hotfix devono essere rimergiati in quel release branch piuttosto che nel develop. Il merge dei bugfix nel release branch porterà i bugfix nel develop anche quando il release branch finirà. (Se il lavoro in develop richiede questo bugfix e non si può aspettare che il release branch sia finito, si possono mergiare tranquillamente i bugfix nel develop.)

Alla fine, rimuoviamo il branch temporaneo:

$ git branch -d hotfix-1.2.1  
Deleted branch hotfix-1.2.1 (was abbe5d6).

Conclusioni

Mentre non vi è nulla di nuovo in questo modello di ramificazione, la “grande immagine” che si trova all’inizio di questo post si è rivelata veramente utile nei nostri progetti. Essa costituisce un elegante modello mentale che è semplice da comprendere e permette ai membri del team di sviluppare una comprensione condivisa dei processi di branching e releasing.

Una versione ad alta qualità della figura è fornita qui sotto. Scaricala, stampala ed appendila al muro per una rapida consultazione.

Aggiornamento: e per chiunque lo ha richiesto: qui sotto anche il diagramma principale in formato Apple Keynote.

Sentitevi liberi di commentare! :)

Comments