Con tutto il lavoro necessario in un progetto di sviluppo, Git è una manna dal cielo. Tuttavia, con così tanti ballerini sulla pista da ballo, inevitabilmente uno o due finiranno per pestarsi i piedi. Per il vostro progetto, questo significa che due sviluppatori potrebbero lavorare sulla stessa suite di codice ed effettuare il commit nello stesso momento. In queste situazioni, è necessario adottare alcune strategie di fusione Git per risolvere il conflitto.
Sebbene a volte effetuare un Git merge può essere un’operazione lineare, ci sono molte altre occasioni in cui è necessario un approccio avanzato. In questo caso, dovrete utilizzare strategie come merge ricorsivo, merge a tre vie e molte altre ancora. Potreste anche aver bisogno di annullare una fusione Git a un certo punto.
In questo tutorial verranno illustrate alcune tecniche di fusione Git complesse da aggiungere agli strumenti a vostra disposizione. Cominciamo subito!
Introduzione alle strategie di fusione di Git
Il concetto di base di un merge è semplice: unire due branch per trasformare più commit in uno solo. Tuttavia, ci sono diverse tecniche che si possono utilizzare per assicurarsi di eseguire il commit e il merge del codice giusto.
Vediamo alcune strategie importanti da capire. Sono in ordine sparso e, a un certo punto della vostra carriera di sviluppatori, potrebbero servirvi tutte. Inoltre, dovrete avere una solida conoscenza dei concetti di base di Git, come puntatori, branch e commit.
La differenza tra merge a due e a tre vie
È utile capire la differenza tra un merge a due vie e il suo compagno a tre vie. La maggior parte delle strategie di fusione che tratteremo in seguito riguardano situazioni a tre vie. In effetti, è più semplice parlare di una fusione a tre vie. Consideriamo il seguente esempio:
- Avete un branch main con diversi commit e un branch di feature che ha anch’esso dei commit.
- Tuttavia, se il branch main esegue ulteriori commit, entrambi i branch divergeranno.
- In parole povere, sia il branch main che quello feature hanno dei commit che l’altro non ha. Se li uniste utilizzando un approccio bidirezionale, perdereste un commit (probabilmente su main).
- Invece, Git creerà un nuovo commit di fusione sia dal branch main che da quello feature.
In poche parole, Git prenderà in considerazione tre diverse istantanee per unire le modifiche: la testa del branch main, la testa del branch feature e l’antenato comune. Questo sarà il commit finale comune sia al branch main che a quello feature.
In pratica, non dovrete preoccuparvi di sapere se una determinata strategia di fusione è bidirezionale o tridirezionale. Spesso è necessario utilizzare una strategia a prescindere. In ogni caso, è utile sapere come Git “pensa” quando si tratta di effettuare il merge di branch e repo.
Fusione rapida
La prima strategia potrebbe non richiedere alcuna azione per essere eseguita. Un merge rapido sposta il puntatore all’ultimo commit su main, senza creare un commit aggiuntivo (che potrebbe creare confusione).
La tecnica inizia con un branch main che può avere o meno dei commit. In questo caso, aprite un nuovo branch, lavorate sul codice ed eseguite i commit. A questo punto, dovete anche unire le modifiche al branch main. Un merge rapido ha un solo requisito da soddisfare:
- Dovete assicurarvi che non avvengano altri commit su main mentre lavorate sul nuovo branch.
Questo non sarà sempre possibile, soprattutto se lavorate in un team numeroso. Tuttavia, se scegliete di unire i vostri commit con un branch main corrente e senza commit propri, effettuerete una fusione rapida. Potete farlo in un paio di modi diversi:
git merge <branch>
git merge --ff-only
Spesso non è necessario specificare che si vuole eseguire una fusione rapida. Questo tipo di fusione avviene in progetti singoli o con piccoli team. In un ambiente frenetico è una fusione che avviene raramente. Per questo motivo, gli altri tipi di merge saranno più comuni.
Merge ricorsivo
La fusione ricorsiva è spesso l’opzione predefinita, poiché si presenta in situazioni più comuni rispetto ad altri tipi di fusione. Un merge ricorsivo consiste nell’effettuare commit su un branch, ma anche ulteriori commit sul branch main.
Quando sarà il momento di fare il merge, Git ripercorrerà il branch per eseguire il commit definitivo. Ciò significa che un commit di fusione avrà due branch “genitori” una volta completato.
Come nel caso di un merge rapido, normalmente non è necessario specificare un merge ricorsivo. Tuttavia, potete assicurarvi che Git non scelga qualcosa di simile a un merge rapido utilizzando i seguenti comandi e flag:
git merge --no-ff
git merge -s recursive <branch1> <branch2>
La seconda riga utilizza l’opzione di strategia -s
e la denominazione esplicita per effettuare una fusione. A differenza di una fusione rapida, una fusione ricorsiva crea un commit di fusione dedicato. Per le fusioni a due vie, la strategia ricorsiva è solida e funziona bene.
Ours e Theirs
Una situazione comune durante lo sviluppo è quella in cui create una nuova funzionalità all’interno del progetto che alla fine non otterrà il via libera. In molti casi, avrete molto codice di cui effettuare il merge che sarà anche co-dipendente. Una fusione “ours” è il modo migliore per risolvere questi conflitti.
Questo tipo di fusione può gestire tutti i branch di cui avete bisogno e ignora tutte le modifiche apportate agli altri branch. È l’ideale se volete fare piazza pulita di vecchie funzionalità o di sviluppi indesiderati. Ecco il comando di cui avete bisogno:
git merge -s ours <branch1> <branch2>
Un merge ours significa essenzialmente che il branch corrente contiene il codice de jure. Questo si collega alle fusioni “theirs”, che trattano l’altro branch come corretto. Tuttavia, è necessario passare un’altra opzione di strategia:
git merge -X theirs <branch2>
L’uso delle fusioni “ours” e “theirs” può creare confusione, ma in genere è preferibile attenersi ai casi d’uso tipici (mantenere tutto ciò che si trova nel branch corrente e scartare il resto).
Octopus
La gestione di più teste, cioè la fusione di più branch in un altro, può essere uno scenario complicato per un merge git. Potreste quasi dire che avete bisogno di più di due mani per risolvere i conflitti. Questa è la situazione perfetta per un merge octopus.
Le fusioni octopus sono l’opposto delle fusioni ours e theis. Il caso d’uso tipico è quello in cui volete includere più commit per funzioni simili e fonderli in uno solo. Ecco come si passa:
git merge -s octopus <branch1> <branch2>
Tuttavia, Git rifiuterà una fusione octopus se dovrete effettuare una risoluzione manuale. Per quanto riguarda le risoluzioni automatiche, la fusione octopus sarà l’opzione predefinita se dovrete unire più rami in uno solo.
Resolve
Uno dei metodi più sicuri per unire i commit, il resolve merge è l’ideale se avete una situazione che comporta fusioni incrociate. È anche un metodo di risoluzione veloce da implementare. Potete usarlo anche per le cronologie di fusione più complesse, ma solo per quelle con due teste.
git merge -s resolve <branch1> <branch2>
Poiché un resolve merge utilizza un algoritmo a tre vie per lavorare sia con il branch corrente che con quello da cui state attingendo, potrebbe non essere flessibile come altri metodi di fusione. Tuttavia, per il lavoro che vi serve, il resolve merge è quasi perfetto.
Subtree
Questo metodo di fusione ricorsiva potrebbe rischiare di confondervi. Cercheremo di spiegarlo con un esempio chiaro:
- Per prima cosa, considerate due alberi diversi – X e Y. Spesso si tratta di due repo.
- Il vostro scopo è unire entrambi gli alberi in uno solo.
- Se l’albero Y corrisponde a uno dei sottoalberi di X, l’albero Y viene modificato per adattarsi alla struttura di X.
Questo significa che la fusione subtree è fantastica se volete unire più repo in un unico articolo definitivo. Inoltre, apporta le modifiche necessarie all’albero “antenato” comune di entrambi i branch.
git merge -s subtree <branch1> <branch2>
In breve, un subtree merge è quello che cercate se dovete unire due repository. In effetti, potreste avere difficoltà a capire quale sia la strategia di fusione più adatta a voi. Più avanti parleremo di alcuni strumenti che potrebbero aiutarvi.
Prima però, parliamo di alcuni conflitti di fusione avanzati che dovete sapere come risolvere.
Come gestire i conflitti di fusione Git più complessi
Unire i branch in Git significa gestire i conflitti e risolverli. Maggiore è la dimensione del team e del progetto, maggiore è la possibilità di riscontrare conflitti. Alcuni di questi possono essere complessi e difficili da risolvere.
Dato che i conflitti possono consumare tempo, denaro e risorse, dovete trovare il modo di stroncarli sul nascere. Nella maggior parte dei casi, due sviluppatori lavorano alla stessa suite di codice ed entrambi decidono di effettuare il commit.
Questo potrebbe significare che potreste non essere in grado di avviare il merge a causa di modifiche in sospeso o che si verifichi un errore durante il merge che richiede un intervento manuale. Una volta che la vostra directory di lavoro è “pulita”, potrete iniziare. Spesso Git vi segnala la presenza di un conflitto quando iniziate la fusione:
Tuttavia, per maggiori informazioni, potete eseguire un git status
e vedere i dettagli:
Da qui potete iniziare a lavorare sui vari file che causano il conflitto. Alcuni degli strumenti e delle tecniche di cui parleremo in seguito vi saranno utili.
Interrompere e resettare i merge
A volte è necessario interrompere del tutto un merge e ripartire da zero. Infatti, entrambi i comandi che abbiamo citato si adattano a situazioni in cui non sapete ancora cosa fare con un conflitto.
Potete scegliere di interrompere o resettare una fusione in corso con i seguenti comandi:
git merge --abort
git reset
I due comandi sono simili, ma si usano in circostanze diverse. Ad esempio, interrompere una fusione significa riportare il branch allo stato precedente alla fusione. In alcuni casi, questo non funziona. Se la vostra directory di lavoro contiene modifiche di cui non è stato eseguito il commit o lo stash, non potrete eseguire l’interruzione.
Tuttavia, resettare un merge significa riportare i file a uno stato “good” conosciuto. Quest’ultimo è un aspetto da considerare se Git non riesce ad avviare la fusione. Notate che questo comando cancellerà tutte le modifiche di cui non sarà stato eseguito il commit, il che significa che si tratta più che altro di un’azione consapevole che richiede molta attenzione.
Eseguire il checkout dei conflitti
La maggior parte dei conflitti di fusione è semplice da accertare e risolvere. Tuttavia, in alcuni casi, potrebbe essere necessario scavare più a fondo per capire perché si verifica un conflitto e come iniziare a risolverlo.
Per ottenere un contesto più ampio dopo un git merge
si può utilizzare un checkout:
git checkout --conflict=diff3 <filename>
Questa funzione prende la tipica navigazione che fornisce un checkout e crea un confronto tra i due file che mostra il conflitto di fusione:
In senso tecnico, questa operazione controlla nuovamente il file e sostituisce i marcatori di conflitto. Questa operazione può essere eseguita più volte nel corso della risoluzione. In questo caso, se inserite l’argomento diff3
, vi fornirà la versione di base e le alternative nelle versioni “ours” e “theirs”.
Notate che l’argomento predefinito è merge
, che non dovrete specificare a meno che non modifichiate lo stile del conflitto di fusione rispetto a quello predefinito.
Ignorare lo spazio negativo
Lo spazio negativo e il suo utilizzo sono un punto di discussione comune. Alcuni linguaggi di programmazione utilizzano diversi tipi di spaziatura e anche i singoli sviluppatori utilizzano formattazioni diverse.
“Spazi vs tabulazioni” è una battaglia a cui non prenderemo parte. Tuttavia, se si verificano situazioni in cui la formattazione cambia da uno all’altro a seconda del file e della pratica di codifica, potreste imbattervi in questo problema di Git merge.
Saprete che il merge fallisce perché ci saranno linee rimosse e aggiunte osservando il conflitto:
Questo perché Git osserva le linee e considera lo spazio negativo come una modifica.
Tuttavia, è possibile aggiungere argomenti specifici al comando git merge
per ignorare gli spazi negativi nei file interessati:
git merge -Xignore-all-space
git merge -Xignore-space-change
Anche se questi due argomenti sembrano simili, hanno una differenza unica. Se scegliete di ignorare tutti gli spazi negativi, Git lo farà. Si tratta di un approccio ampio, ma al contrario, -Xignore-space-change
considera equivalenti solo le sequenze di uno o più caratteri negativi. Per questo motivo, ignorerà gli spazi singoli alla fine delle righe.
Per maggiore sicurezza, potete anche rivedere la fusione utilizzando il comando --no-commit
, per verificare che gli spazi negativi siano stati ignorati e contati nel modo giusto.
Eseguire il merge dei log
La creazione di log è fondamentale per quasi tutti i software che trasmettono dati. Nel caso di Git, potete utilizzare i log per conoscere più dettagliatamente un conflitto di fusione. Potete accedere a queste informazioni utilizzando git log
:
Si tratta essenzialmente di un file di testo che scarica ogni azione all’interno di un repo. Tuttavia, potete aggiungere altri argomenti per affinare la visualizzazione e vedere solo i commit che desiderate:
git log --oneline --left-right <branch1>...<branch2>
Utilizzate un “punto triplo” per fornire un elenco di commit coinvolti in due branch durante una fusione. Filtrate tutti i commit che i due branch condividono, lasciando una selezione di commit da analizzare ulteriormente.
Potete anche utilizzare git log --oneline --left-right --merge
per mostrare solo i commit di entrambi i branch che “toccano” un file in conflitto. L’opzione -p
mostrerà le modifiche esatte di uno specifico “diff”, ma tenete presente che questo vale solo per i commit di cui non è stato eseguito il merge. Esiste una soluzione a questo problema, di cui parliamo qui di seguito.
Usare il formato Diff combinato per indagare su un conflitto di fusione in Git
Potete approfondire la ricerca ottenuta con git log
per indagare sui conflitti di fusione. In circostanze tipiche, Git eseguirà il merge del codice e renderà visibile tutto ciò che riesce. In questo modo rimarranno solo le linee in conflitto, che potrete vedere utilizzando il comando git diff
:
Questo formato “diff combinato” aggiunge due colonne di informazioni supplementari. La prima dice se una riga è diversa tra il vostro branch (“ours”) e la copia di lavoro; la seconda dà le stesse informazioni per il branch “theirs”.
Per quanto riguarda i segni, il segno più indica se una riga è stata aggiunta alla copia di lavoro ma non in quel lato specifico della fusione, mentre il segno meno indica se la riga è stata rimossa.
È possibile vedere questo formato di diff combinato anche all’interno del log di Git utilizzando un paio di comandi:
git show
git log --cc -p
Il primo è un comando che si usa su un commit di fusione per vedere la sua cronologia. Il secondo comando utilizza la funzionalità di -p
per mostrare le modifiche a un commit non unito insieme al formato diff combinato.
Come annullare una fusione Git
Può capitare di commettere degli errori e di effettuare delle fusioni su cui è necessario tornare indietro. In alcuni casi, potete semplicemente modificare il commit più recente usando git commit --amend
. Questo aprirà l’editor per permettervi di modificare l’ultimo messaggio di commit.
Sebbene sia possibile annullare conflitti di fusione più complessi e le modifiche che ne derivano, può essere difficile perché i commit sono spesso permanenti.
Per questo motivo, è necessario seguire molti passaggi:
- Innanzitutto, dovete esaminare i commit e trovare i riferimenti alle fusioni di cui avete bisogno.
- Poi, controllare i branch per esaminare le cronologie dei commit.
- Una volta a conoscenza dei branch e dei commit di cui avete bisogno, ci sono dei comandi Git specifici in base all’azione che desiderate.
Vediamo questi comandi in modo più dettagliato e iniziamo con il processo di revisione. Da lì, vi mostreremo un modo rapido per annullare un merge in Git, per poi esaminare comandi specifici per casi d’uso più avanzati.
Revisione dei commit
Il comando git log --oneline
è ottimo se volete vedere gli ID di revisione e i messaggi di commit relativi al branch corrente:
Il comando git log --branches=*
mostrerà le stesse informazioni ma per tutti i branch. In ogni caso, potete utilizzare gli ID di riferimento insieme a git checkout
per creare uno stato “distaccato HEAD
“. Ciò significa che non lavorerete su nessun branch da un punto di vista tecnico e che, una volta tornati a un branch stabilito, sarete “orfano” delle modifiche.
In questo modo, potete utilizzare il checkout quasi come una sandbox priva di rischi. Tuttavia, se volete conservare le modifiche, potete fare il checkout del branch e dargli un nuovo nome usando git checkout -b <branch-name>
. Questo è un modo valido per annullare un merge in Git, ma ci sono modi più complessi per farlo per casi d’uso avanzati.
Usare git reset
Molti conflitti di fusione potrebbero verificarsi su un repo locale. In questi casi, git reset
è il comando che vi serve. Tuttavia, questo comando ha più parametri e argomenti da analizzare. Ecco come utilizzare il comando nella pratica:
git reset --hard <reference>
La prima parte di questo comando – git reset --hard
– prevede tre fasi:
- Sposta il branch di riferimento nella posizione precedente al commit di fusione.
- L’hard reset fa sì che l'”indice” (cioè l’istantanea del prossimo commit proposto) assomigli al branch di riferimento.
- Fa in modo che la directory di lavoro assomigli all’indice.
Una volta invocato questo comando, la cronologia dei commit rimuove i commit successivi e ripristina la cronologia all’ID di riferimento. È un modo pulito per annullare una fusione Git, ma non è adatto a tutti i casi.
Ad esempio, si verificherà un errore se si tenta di eseguire il push di un commit locale resettato su un repo remoto che contiene quel commit. In questo caso, c’è un altro comando che si può utilizzare.
Usare git revert
Anche se git reset
e git revert
sembrano simili, ci sono alcune importanti differenze. Negli esempi fatti finora, il processo di annullamento comporta lo spostamento dei puntatori di riferimento e dell’HEAD a un commit specifico. È come mischiare le carte da gioco per creare un nuovo ordine.
Al contrario, git revert
crea un nuovo commit basato sulle modifiche di backtracking, quindi aggiorna i puntatori di riferimento e rende il branch la nuova “punta”. È anche il motivo per cui dovreste usare questo comando per i conflitti di fusione dei repo remoti.
Potete usare git revert <reference>
per annullare una fusione Git. È necessario specificare sempre un riferimento di commit, altrimenti il comando non verrà eseguito. Potete anche passare HEAD
al comando per tornare all’ultimo commit.
Tuttavia, potete dare a Git una maggiore chiarezza su ciò che volete fare:
git revert -m 1 <reference>
Quando invocate il merge, il nuovo commit avrà due “genitori”. Uno si riferisce al riferimento che avete specificato e l’altro è la punta del branch che volete unire. In questo caso, -m 1
dice a Git di mantenere il primo genitore, cioè il riferimento specificato, come “linea principale”.
L’opzione predefinita per git revert
è -e
o --edit
. Questo aprirà l’editor per modificare il messaggio di commit prima del revert. Tuttavia, potete anche passare --no-edit
, che non aprirà l’editor. Potete anche passare -n
o --no-commit
.
Questo indica a git revert
di non creare un nuovo commit, ma di “invertire” le modifiche e aggiungerle all’indice di staging e alla directory di lavoro.
La differenza tra la fusione e il rebase in Git
Invece di usare il comando git merge
, si può anche usare git rebase
. Anche questo è un modo per integrare le modifiche in un’unica directory, ma con una differenza:
- Un merge a tre vie è l’impostazione predefinita quando si utilizza
git merge
. Unisce le istantanee di due branch correnti e le fonde con un antenato comune di entrambi per creare un nuovo commit. - Il rebasing consiste nel prendere una modifica patchata da un branch divergente e applicarla a un altro, senza bisogno dell’antenato. Questo significa che non ci sarà un nuovo commit.
Per utilizzare questo comando, fate il checkout al branch in cui desiderate effettuare il rebase. Da lì, potete usare il seguente comando:
git rebase -i <reference>
In molte situazioni, il vostro riferimento sarà il branch main. L’opzione -i
avvia il “rebase interattivo”. Questo vi dà la possibilità di modificare i commit man mano che si spostano. Potete usarla per ripulire la cronologia dei commit, che è uno dei grandi vantaggi dell’uso di git rebase
.
Eseguendo il comando, nell’editor verrà visualizzato un elenco di potenziali commit da spostare. In questo modo potete modificare completamente l’aspetto della cronologia dei commit. Potete anche unire i commit se cambiate il comando pick
in fixup
. Una volta salvate le modifiche, Git eseguirà il rebase.
In generale, si usa Git merge per molti conflitti. Tuttavia, il rebase ha anche molti vantaggi. Ad esempio, mentre l’operazione di fusione è semplice da usare e permette di preservare il contesto che circonda la vostra cronologia di fusione, il rebase può essere più pulito in quanto permette di semplificare la cronologia dei commit in un unico documento.
Ciononostante, dovete fare più attenzione con il rebasing, perché le possibilità di commettere un errore sono tante. Inoltre, non dovreste utilizzare questa tecnica sui branch pubblici, poiché il rebasing interesserà solo il vostro repo. Per risolvere i problemi che ne derivano, dovrete fare ancora più fusioni e vedrete più commit.
Strumenti che aiutano a gestire meglio una fusione Git
Data la complessità dei conflitti di fusione di Git, potreste volere un aiuto. Ci sono molti strumenti disponibili per aiutarvi a realizzare una fusione efficace e utilizzando Intellij IDEA, avrete un metodo integrato grazie al menu Branches:
Anche VSCode include una funzionalità simile all’interno della sua interfaccia utente (UI). I vecchi utenti di Atom scopriranno che Microsoft ha portato avanti la sua fantastica integrazione con Git, con la possibilità di connettersi a GitHub senza ulteriori estensioni o componenti aggiuntivi.
Inoltre, potrete disporre di ulteriori opzioni grazie alla palette dei comandi. Questo vale anche per gli editor che si basano sul framework open-source di VSCode, come Onivim2:
Il vantaggio, come nel caso di tutti gli strumenti di questo elenco, è che non avete bisogno della riga di comando per effettuare le fusioni. Di solito è necessario selezionare un branch di origine e uno di destinazione da un menu a tendina e poi lasciare che l’editor esegua la fusione. Anche in questo caso, non è necessario un approccio passivo. Potrete rivedere le modifiche dopo, quindi effettuare il commit che vi serve.
Un editor che offre un’interfaccia grafica separata (GUI) per lavorare con Git è Sublime Text. Se utilizzate questo editor, Sublime Merge potrebbe essere l’aggiunta ideale al vostro flusso di lavoro:
Indipendentemente dall’editor di codice, spesso avrete la possibilità di lavorare con Git senza utilizzare la riga di comando. Questo avviene anche con Vim e Neovim, grazie al plugin Git Fugitive di Tim Pope, fantastico e semplice da usare.
Tuttavia, esistono alcuni strumenti di fusione di terze parti che si concentrano esclusivamente su questo compito.
Applicazioni dedicate a Git merge
Ad esempio, Mergify è un metodo di livello aziendale per eseguire la fusione del codice che si integra nella pipeline e nel flusso di lavoro di continuous integration/continuous delivery (CI/CD):
Alcune funzioni aiutano ad automatizzare l’aggiornamento delle richieste di pull prima della fusione, a riordinarle in base alla priorità e a raggrupparle in batch. Per una soluzione open source, Meld potrebbe essere utile:
La sua versione stabile supporta Windows e Linux e funziona sotto licenza GPL. Offre le funzionalità di base per confrontare i branch, modificare le fusioni e altro ancora. Potete anche ottenere confronti a due o tre vie e il supporto per altri sistemi di controllo di versione come Subversion.
Riepilogo
Git è uno strumento essenziale per collaborare e gestire le modifiche al codice in modo efficiente. Tuttavia, se più sviluppatori lavorano sullo stesso codice, possono sorgere dei conflitti. Le strategie di fusione di Git vi aiuteranno a risolvere questi conflitti, e ci sono molti modi con cui potrete farlo. Per strategie di fusione Git più complesse, dovrete ricorrere a tattiche avanzate.
Potrebbe bastare ignorare gli spazi negativi o spulciare i log di ricerca. Tuttavia, non è sempre necessario utilizzare la riga di comando. Ci sono molte applicazioni che vi possono aiutare, e spesso anche il vostro editor di codice utilizza un’interfaccia integrata.
Se volete assicurarvi di avere un hosting per applicazioni di alta qualità, ci pensiamo noi. I nostri servizi di Hosting di Applicazioni basati sul cloud assicurano che la vostra applicazione full-stack sia pronta per andare online in pochissimo tempo.
Quale di queste strategie di Git merge vi aiuterà a uscire da una situazione difficile? Fatecelo sapere nella sezione commenti qui sotto!
Lascia un commento