Rimanendo dello stesso ambito, questa puntata esplora l’importanza di una corretta gestione dei contributi, andando a sviscerare le differenze tra merge e rebase.
In cosa consiste un merge
Nello scorso articolo è stato esplorato il concetto di branch, andando a creare quindi una variante (i fan di Loki capiranno di che cosa si sta parlando in un attimo) del ramo principale del repository, ossia main.
Rimane quindi da capire quali sono le azioni da compiere per far sì che le variazioni presenti nel branch alternativo diventino parte di quello principale. Rimane cioè da capire il concetto di merge, ossia unione.
Dato un branch, in questo caso mio-contributo, contenente una modifica che non esiste nel branch main:
/git # git checkout mio-contributo
Switched to branch 'mio-contributo'
/git # git lg
* ab47204d465f - (2024-02-16 11:45:37 +0000) (HEAD -> mio-contributo) Settimo commit <Raoul Scarazzini>
* ebca67901328 - (2024-02-16 10:58:20 +0000) (main) Sesta modifica al repository <Raoul Scarazzini>
* d2a002445d21 - (2024-02-16 10:57:55 +0000) Quinto commit <Raoul Scarazzini>
* 667916b7af10 - (2024-02-16 10:57:55 +0000) Quarto commit <Raoul Scarazzini>
* ce337855b51c - (2024-02-16 10:57:55 +0000) Terzo commit <Raoul Scarazzini>
* bee7830d28ef - (2024-02-16 10:57:55 +0000) Secondo commit <Raoul Scarazzini>
* 1cb162db064c - (2024-02-16 10:57:55 +0000) Primo commit <Raoul Scarazzini>
/git # git checkout main
Switched to branch 'main'
/git # git lg
* ebca67901328 - (2024-02-16 10:58:20 +0000) (HEAD -> main) Sesta modifica al repository <Raoul Scarazzini>
* d2a002445d21 - (2024-02-16 10:57:55 +0000) Quinto commit <Raoul Scarazzini>
* 667916b7af10 - (2024-02-16 10:57:55 +0000) Quarto commit <Raoul Scarazzini>
* ce337855b51c - (2024-02-16 10:57:55 +0000) Terzo commit <Raoul Scarazzini>
* bee7830d28ef - (2024-02-16 10:57:55 +0000) Secondo commit <Raoul Scarazzini>
* 1cb162db064c - (2024-02-16 10:57:55 +0000) Primo commit <Raoul Scarazzini>
Per far sì che questa ne diventi parte si potrà usare il comando git merge, passando come parametro il nome del branch che si vuole unire, in questo caso quindi mio-contributo:
HEAD è quindi riferimento comune per entrambi i branch. Preciso, pulito, semplicissimo… In un mondo perfetto, denso di unicorni e arcobaleni.
Gestione dei conflitti
I branch e le varianti in esso presenti fintanto che operano su file diversi possono funzionare in maniera molto lineare, ma cosa succede quando le varianti operano sugli stessi file? Come si gestiscono i conflitti?
Ripartendo dalla precedente situazione iniziale:
/git # git checkout mio-contributo
Already on 'mio-contributo'
/git # git lg
* 4fa4cd79e977 - (2024-02-16 13:41:29 +0000) (HEAD -> mio-contributo) Settimo commit <Raoul Scarazzini>
* ebca67901328 - (2024-02-16 10:58:20 +0000) (main) Sesta modifica al repository <Raoul Scarazzini>
* d2a002445d21 - (2024-02-16 10:57:55 +0000) Quinto commit <Raoul Scarazzini>
* 667916b7af10 - (2024-02-16 10:57:55 +0000) Quarto commit <Raoul Scarazzini>
* ce337855b51c - (2024-02-16 10:57:55 +0000) Terzo commit <Raoul Scarazzini>
* bee7830d28ef - (2024-02-16 10:57:55 +0000) Secondo commit <Raoul Scarazzini>
* 1cb162db064c - (2024-02-16 10:57:55 +0000) Primo commit <Raoul Scarazzini>
/git # git checkout main
Switched to branch 'main'
/git # git lg
* ebca67901328 - (2024-02-16 10:58:20 +0000) (HEAD -> main) Sesta modifica al repository <Raoul Scarazzini>
* d2a002445d21 - (2024-02-16 10:57:55 +0000) Quinto commit <Raoul Scarazzini>
* 667916b7af10 - (2024-02-16 10:57:55 +0000) Quarto commit <Raoul Scarazzini>
* ce337855b51c - (2024-02-16 10:57:55 +0000) Terzo commit <Raoul Scarazzini>
* bee7830d28ef - (2024-02-16 10:57:55 +0000) Secondo commit <Raoul Scarazzini>
* 1cb162db064c - (2024-02-16 10:57:55 +0000) Primo commit <Raoul Scarazzini>
All’interno del branch main si andrà ad aggiungere un commit che modifica Settimo.txt, lo stesso file modificato nel branch mio-contributo:
Se si provasse ad effettuare un merge del branch mio-contributo all’interno del branch main questi fallirebbe. In queste situazioni quindi è necessario sanare manualmente i conflitti, modificando la propria prospettiva. È infatti all’interno del branch variante mio-contributo che si opererà, in modo da renderlo compatibile con un merge “pulito” come quello mostrato in apertura.
Ci sono sostanzialmente due modi per raggiungere il risultato: sanare i conflitti mediante merge oppure mediante rebase.
Utilizzo di merge
L’allineamento del branch mio-contributo con main utilizzando merge rivelerà cosa c’è da risolvere:
/git # git checkout mio-contributo
Switched to branch 'mio-contributo'
/git # git merge main
Auto-merging Settimo.txt
CONFLICT (add/add): Merge conflict in Settimo.txt
Automatic merge failed; fix conflicts and then commit the result.
/git # git status
On branch mio-contributo
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 added: Settimo.txt
no changes added to commit (use "git add" and/or "git commit -a")
Il problema è chiaro: entrambi i commit hanno modificato lo stesso file, quindi va gestito il conflitto che, all’interno del file apparirà evidenziato così:
/git # cat Settimo.txt
<<<<<<< HEAD
Settimo commit
=======
Variante settimo commit
>>>>>>> main
si capisce come la porzione inclusa tra <<<<<<< HEAD e ======= sia relativa alle modifiche del branch attuale, mentre l’altra, da ======= a >>>>>>> main sia relativa a quanto vogliamo includere.
Una volta ricomposto il file secondo le aspettative, ad esempio in questo modo:
L’operazione comporterà l’apertura dell’editor vim, che consentirà di inserire un messaggio esteso per il commit di merge:
Merge branch 'main' into mio-contributo
Modifica cumulativa al file Settimo.txt.
# Conflicts:
# Settimo.txt
#
# It looks like you may be committing a merge.
# If this is not correct, please run
# git update-ref -d MERGE_HEAD
# and try again.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch mio-contributo
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
# modified: Settimo.txt
#
Salvando ed uscendo dall’editor (:wq) un nuovo commit (il merge, appunto) verrà aggiunto nella sequenza:
L’alberatura ha subito una variazione, ma ora comprende anche il commit presente nel branch main, pertanto il merge del branch mio-contributo nel branch main avverrà senza colpo ferire:
HEAD è ora allineato ad entrambi i branch, che quindi sono identici, anche se esteticamente la situazione lascia un poco a desiderare in termini di comprensione.
Utilizzo di rebase
Considerando la stessa situazione di partenza, lo stesso processo può essere eseguito utilizzando rebase:
Dal branch mio-contributo anziché merge si effettuerà un rebase di main:
/git # git rebase main
Auto-merging Settimo.txt
CONFLICT (add/add): Merge conflict in Settimo.txt
error: could not apply 1c6886c1eff5... Settimo commit
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 1c6886c1eff5... Settimo commit
/git # git status
interactive rebase in progress; onto 6474fc1a2e20
Last command done (1 command done):
pick 1c6886c1eff5 Settimo commit
No commands remaining.
You are currently rebasing branch 'mio-contributo' on '6474fc1a2e20'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both added: Settimo.txt
no changes added to commit (use "git add" and/or "git commit -a")
Anche in questo caso esiste lo stesso conflitto (non potrebbe essere altrimenti), e si può gestire in maniera identica al precedente, sistemando il contenuto del file Settimo.txt:
La differenza sostanziale è che ad essere creato sarà un nuovo commit, basato sul precedente, e non un commit di merge:
Settimo commit
Descrizione estesa del settimo commit
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto 6474fc1a2e20
# Last command done (1 command done):
# pick 9d9d72d6709f Settimo commit
# No commands remaining.
# You are currently rebasing branch 'mio-contributo' on '6474fc1a2e20'.
#
# Changes to be committed:
# modified: Settimo.txt
#
Il che comporterà, una volta completato l’editing ed usciti dal terminale vim, questa sequenza di commit:
Rispetto al risultato ottenuto in precedenza con il merge, la lista dei commit è decisamente più lineare, e sarà anche più facilmente integrabile nel branch main:
Come si può notare nella lista dei commit HEAD corrisponde tanto al branch main quanto a mio-contributo. Tutte le variazioni sono state mantenute, ma a differenza della situazione ottenuta con il merge, il rebase in questo caso garantisce molta più linearità, garantendo anche una gestione dei commit più mirata, anche in caso di annullamento delle modifiche, ad esempio mediante git revert.
Conclusioni
Quanto sia preziosa una lista di commit lineare rispetto ad una intersecata e con mille diramazioni è letteralmente incalcolabile. Il tutto concorre ancora una volta al tema principale che aleggia questa serie: l’empatia. Mettersi nei panni di chi dovrà un domani gestire il repository, spendendo qualche minuto in più per rendere la vita più facile in termini di comprensione e gestione, fa di noi delle persone migliori.
Alla prossima puntata!
Da sempre appassionato del mondo open-source e di Linux nel 2009 ho fondato il portale Mia Mamma Usa Linux! per condividere articoli, notizie ed in generale tutto quello che riguarda il mondo del pinguino, con particolare attenzione alle tematiche di interoperabilità, HA e cloud.
E, sì, mia mamma usa Linux dal 2009.