GNU/Linux: gestire le branch in git spiegato semplice

Oggi impariamo a gestire le branch di Git. L’argomento è straordinariamente interessante, quindi rimanete fino alla fine: alla fine affronterò anche il famoso, o famigerato, detached HEAD state.

Cos’È una branch

Per capire cosa sono le branch bisogna immaginare Git come una macchina del tempo in cui convivono più linee temporali, come universi paralleli. Ogni volta che cambiamo branch, la nostra cartella di lavoro cambia completamente per adeguarsi al contenuto di quella branch. Possiamo tenere linee di sviluppo parallele, anche differenti, che però devono convergere sullo stesso progetto: le branch, così come si dividono, si possono anche riunire. Non sono progetti diversi, ma versioni diverse dello stesso progetto.

Gli stili di branching sono tanti. Uno molto semplice è usare una branch per la versione stabile e un’altra per la versione di sviluppo, riunendole periodicamente.

Ma cos’è esattamente una branch? Semplificando, è un’etichetta appiccicata a un determinato commit. Quando siete su una branch siete su un commit che porta l’etichetta con il nome di quella branch. Un repository nuovo ha una sola branch, che a seconda della versione di Git e delle impostazioni si chiama master oppure main. Per elencare le branch:

git branch

L’output è:

* master

L’asterisco indica la branch su cui ci troviamo.

Creare una branch e spostarsi

Supponiamo di voler iniziare un nuovo filone di sviluppo, per esempio un file per i colori. Sapendo che sviluppo una nuova feature, creo una branch apposta e faccio lì i miei commit, finché non sono pronti per entrare nel ramo principale:

git branch colori

Sui nomi delle branch ci sono varie filosofie; fatemi sapere nei commenti se vi interessano gli stili di utilizzo di Git. Rifacciamo git branch:

git branch

L’output è:

  colori
* master

Per spostarci da una branch all’altra c’è git checkout, che vuole il nome della branch di destinazione:

git checkout colori

Git ci informa del cambio e git status lo conferma. Ora creiamo un file, lo aggiungiamo e facciamo un commit:

echo rosso > colori.txt
git add colori.txt
git commit -m "aggiunto colori.txt"

Se torniamo a master, il file colori.txt è scomparso:

git checkout master

Non è andato perso: si trova ancora sulla branch colori, mentre su master è assente. Pensate di realizzare codice e non semplici file: posso lavorare sulla libreria colori finché non è pronta, e solo allora riunirla alla branch principale.

Rinominare una branch

Torniamo alla branch colori. La parola non è molto autoesplicativa: voglio chiamarla aggiunta-colori, con git branch e l’opzione -m:

git checkout colori
git branch -m aggiunta-colori

Non c’è limite al numero di branch. Ogni branch parte dal commit su cui mi trovo nel momento in cui la creo, se non specifico altro. Un repository può quindi ramificarsi molto in fretta, ed è per questo che si usano strutture convenzionali per creare le branch. Altrettanto importante è avere chiara la mappa del repository, e Git ci aiuta con git log e alcune opzioni:

git log --oneline --graph --decorate --all
  • –oneline dà un output più sintetico;
  • –graph disegna in ASCII l’albero del repository;
  • –decorate aggiunge le etichette (branch, HEAD) che aiutano la leggibilità;
  • –all mostra tutti i commit e non solo quelli della branch corrente.

La mappa, per ora, è semplice: c’è la branch aggiunta-colori su cui siamo (HEAD) che è avanti di un commit rispetto a master. Se lavoriamo un po’ sui colori e facciamo altri commit, la distanza fra aggiunta-colori e master aumenta.

Unire le branch: git merge

Supponiamo di aver terminato lo sviluppo dei colori e di voler riunire il codice di aggiunta-colori a master. Per fare il merge ci si colloca sulla branch di destinazione, quindi prima ci spostiamo su master, poi:

git checkout master
git merge aggiunta-colori

Git ci dà informazioni interessanti: gli id dei commit coinvolti e il fatto che è stato fatto un fast forward. Il fast forward è la strategia di merge più lineare possibile: non essendoci conflitti, Git fa semplicemente avanzare l’etichetta master fino all’etichetta aggiunta-colori. Se guardiamo la mappa, HEAD punta a master e aggiunta-colori, che ora si trovano nello stesso punto.

Finito lo sviluppo possiamo eliminare la branch aggiunta-colori. Non c’è da preoccuparsi: quello che eliminiamo è soltanto l’etichetta, il codice è già nel ramo master. Infatti colori.txt è presente con tutto il suo contenuto. Si elimina con -d:

git branch -d aggiunta-colori

L’output è:

Branch aggiunta-colori eliminato (era 7768e09).

Git ci dice anche su quale commit stava l’etichetta. Con git log vediamo che tutti i commit ci sono ancora, esattamente dove devono essere, e il repository è più pulito, senza una etichetta che non ha più senso.

Un caso più complicato, e i conflitti

Creiamo una nuova branch, aggiunta-pasta, con un nuovo file, e facciamo git add. Mentre lavoriamo sulla pasta ci viene segnalato un problema o una modifica da fare sul ramo principale: aggiungere altri colori. Non vogliamo sporcare master con questa modifica, quindi apriamo un’altra branch, modifica-colori, e modifichiamo colori.txt. Nel ramo modifica-colori il file della pasta non c’è, perché sta sull’altro ramo. La mappa si complica: master è fermo, e ci sono commit su due strade diverse, aggiunta-pasta e modifica-colori. Torno sulla pasta e faccio altri commit; poi la riunisco a master con un fast forward, e modifica-colori resta indietro di alcuni commit.

Un caso frequente è quello in cui il codice modificato su rami diversi va in conflitto: per esempio modifico le stesse righe del file colori.txt sia su master sia su modifica-colori. Se faccio il merge:

git merge modifica-colori

Git ci segnala chiaramente un conflitto su colori.txt: il merge automatico è fallito. Mi trovo in una situazione ibrida, e git status lo descrive con molta informazione. Posso tornare indietro con:

git merge --abort

oppure risolvere il conflitto e completare il merge. Aprendo colori.txt la prima volta potrebbe venire un colpo, ma non c’è nulla di esoterico: è quello che fa Git quando non riesce a risolvere. Mi presenta prima le righe di un ramo (la destinazione, HEAD) e poi quelle dell’altro (la provenienza), fra dei marcatori. Con i colori del mio esempio il file diventa:

<<<<<<< HEAD
viola
indaco
=======
arancione
indaco
>>>>>>> modifica-colori

Cioè: su HEAD hai viola e indaco, su modifica-colori hai arancione e indaco. Devo decidere la forma finale del file: cancello i marcatori e i dati che non servono (qui scelgo di tenere arancione e indaco) fino ad ottenere la versione finale. Poi salvo, aggiungo il file e faccio il commit, che completa il merge:

git add colori.txt
git commit

Ora l’area di lavoro è di nuovo pulita. L’albero ha una forma più strana, ma strana solo finché non si impara a leggerlo. HEAD punta a master, l’ultimo commit è il merge della branch modifica-colori, che è rimasta indietro di un commit: il commit di merge fa parte di master, non di modifica-colori. L’etichetta modifica-colori non serve più, quindi la si può eliminare. Se faccio divergere aggiunta-pasta con un nuovo commit, la struttura si ramifica ancora, ma git log –graph ci aiuta: gli asterischi indicano il commit sulla loro riga, e le barre colorate come sono collegati i vari commit.

Il detached HEAD state

Il detached HEAD state è lo spauracchio di molti utenti di Git, ma è anche uno strumento utile e potente, se si sa cosa si sta facendo. Dopo aver ripulito il repository dalle etichette inutili, si scopre che git checkout, oltre a un nome di branch, accetta come argomento un riferimento di commit. Per esempio, con l’hash di un commit:

git checkout f9874f0

Git ci dice che siamo in detached HEAD state. Per capire cosa significa bisogna capire da cosa HEAD è staccato. Con git log vediamo che HEAD si trova sul commit f9874f0, esattamente quello richiesto, mentre master è rimasta su un altro commit. HEAD non è agganciato a nessuna etichetta di branch: in questo momento non ci troviamo su nessuna branch. È una situazione comodissima per fare pasticci: possiamo guardare in giro, fare modifiche sperimentali, perfino commit, oppure scartare tutto facendo un checkout altrove, senza nessun impatto sulle branch. In pratica Git ci dice: non sei su nessuna branch, qui puoi fare quello che vuoi, poi quando torni su un’altra branch tutto sarà dimenticato. Quello che succede in detached HEAD rimane in detached HEAD.

Se però i pasticci ci piacciono e vogliamo tenerli, possiamo creare una nuova branch con:

git checkout -b nome-nuova-branch

Sperimentiamo aggiungendo un nuovo file, i tortellini. Anche git status ci ricorda che siamo in detached HEAD. Se facessi checkout altrove perderei la modifica; ma se i tortellini mi piacciono creo una branch con le modifiche:

git checkout -b pasta-ripiena

Ora HEAD non è più detached: è agganciato a pasta-ripiena. Posso fare il merge su master (dopo essermi spostato su master):

git checkout master
git merge pasta-ripiena
git branch -d pasta-ripiena

e master contiene i tortellini. Se invece di creare la branch avessi fatto un semplice checkout su master, la modifica sarebbe stata scartata e nulla sarebbe stato toccato: molto comodo per fare esperimenti.

Conclusione

In sintesi: una branch è un’etichetta su un commit; git branch elenca e crea, git checkout cambia ramo, git branch -m rinomina, git merge riunisce (con fast forward se non ci sono conflitti, con risoluzione a mano dei marcatori se ci sono), git branch -d toglie l’etichetta, git log –oneline –graph –decorate –all disegna la mappa, e il detached HEAD è un terreno di prova da cui si esce tenendo tutto con git checkout -b o buttando tutto con un checkout. Aprite la vostra Linux box e sperimentate: queste cose si imparano solo con la pratica.

Link e risorse

Sostieni il canale

PER SOSTENERE IL CANALE BASTANO UN LIKE E UN’ISCRIZIONE!
ma se vuoi offrirmi un caffè, mi aiuterà a rimanere sveglio mentre registro i video 😁☕
clicca qui per scoprire come https://buymeacoffee.com/thelinuxnerd

questi link sono affiliati Amazon, acquistando i libri che ti consiglio sostieni il canale!