git merge: come unire i branch e risolvere i conflitti
git merge integra la cronologia di un altro branch in quello corrente: se il branch corrente non è andato avanti, Git sposta semplicemente il puntatore (fast-forward), altrimenti crea un merge commit con due genitori. Passate al branch di destinazione ed eseguite git merge nome-branch; in caso di conflitto correggete i file, poi git add e git commit, oppure annullate tutto con git merge --abort.
Cosa fa git merge
Il comando git merge prende le modifiche dal branch indicato e le integra nel branch in cui vi trovate adesso. È importante ricordare la direzione: si fa il merge «dentro di sé». Per integrare feature/login in main, prima si passa a main e poi si avvia il merge.
git switch main
git pull # scaricare il main aggiornato dal server
git merge feature/login
Git non copia nulla riga per riga «a mano». Trova l'antenato comune dei due branch (merge base), confronta con esso entrambe le versioni e combina le modifiche. Se i due lati hanno modificato punti diversi, il risultato si ottiene automaticamente. Se hanno modificato lo stesso punto, nasce un conflitto e la decisione spetta a voi.
Due modalità: fast-forward e merge a tre vie
Fast-forward: quando il merge commit non serve
Supponiamo che abbiate creato feature da main, fatto tre commit, e che nel frattempo nessuno abbia aggiunto nulla a main. In questo caso la cronologia di main è semplicemente l'inizio della cronologia di feature. Git sposta in avanti il puntatore main fino all'ultimo commit di feature, e non viene creato alcun nuovo commit.
$ git merge feature
Updating 3f2a1c4..9b7e0d2
Fast-forward
src/auth.js | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
La cronologia resta lineare, ma si perde l'informazione «qui c'era il branch feature». A volte va bene, a volte no: ne parliamo più sotto, nella sezione su --no-ff.
Merge a tre vie e merge commit
Se entrambi i branch sono andati avanti rispetto all'antenato comune, non è più possibile spostare il puntatore. Git esegue un merge a tre vie: la base, il lato «nostro» (branch corrente) e il lato «loro» (branch da unire). Il risultato viene registrato in un merge commit separato, che ha due genitori.
$ git merge feature
Merge made by the 'ort' strategy.
src/auth.js | 42 ++++++++++++++++++++
src/router.js | 3 +-
2 files changed, 44 insertions(+), 1 deletion(-)
Git aprirà l'editor con un messaggio del tipo Merge branch 'feature'. Potete lasciarlo così o aggiungere il motivo del merge. Per vedere la «biforcazione» risultante è comodo usare:
git log --oneline --graph --all
La strategia ort è quella predefinita a partire da Git 2.34; prima era recursive. Nel lavoro quotidiano non serve cambiarla.
Flag utili di git merge
| Flag | Cosa fa | Quando serve |
|---|---|---|
--no-ff | Crea sempre un merge commit, anche se il fast-forward è possibile | Volete vedere nella cronologia quali commit provengono da una stessa feature |
--ff-only | Esegue il merge solo con fast-forward, altrimenti termina con errore | Aggiornare un branch locale senza merge commit accidentali |
--squash | Mette tutte le modifiche del branch nell'index, ma non fa il commit né crea un legame con il branch | Trasformare una feature in un unico commit pulito |
--no-commit | Esegue il merge ma si ferma prima del commit | Volete controllare il risultato o lanciare i test prima di registrarlo |
-m "testo" | Imposta il messaggio del merge commit | Merge negli script e senza editor |
--abort | Annulla il merge in corso e ripristina lo stato precedente | Troppi conflitti, meglio ricominciare |
--continue | Completa il merge dopo la risoluzione dei conflitti | Alternativa a git commit nell'ultimo passaggio |
-X ours / -X theirs | Nei frammenti in conflitto prende automaticamente la nostra o la loro versione | Sapete con certezza quale versione è corretta (con cautela!) |
--no-ff: mantenere i confini della feature
Molti team fanno il merge delle feature solo con --no-ff. Così nel grafo ogni attività appare come una «bolla» e può essere annullata con un solo git revert -m 1 <merge-commit>. Con il fast-forward questo punto non esiste e bisognerebbe annullare ogni commit separatamente.
--squash: un commit invece di dieci
git switch main
git merge --squash feature/login
git commit -m "Accesso con e-mail e password"
Dopo --squash Git non considera il branch come unito: git branch -d feature/login rifiuterà di eliminarlo con l'avviso «not fully merged». Eliminatelo con git branch -D dopo aver verificato che tutto sia al suo posto. E non continuate a lavorare nello stesso branch dopo lo squash: il merge successivo riporterebbe modifiche già integrate e quasi certamente produrrebbe conflitti.
Conflitto di merge: come risolverlo
Un conflitto è una normale situazione di lavoro, non un disastro. Significa solo una cosa: due branch hanno modificato le stesse righe (oppure uno ha eliminato un file che l'altro ha modificato) e Git non si azzarda a indovinare quale versione sia giusta.
$ git merge feature/login
Auto-merging src/config.js
CONFLICT (content): Merge conflict in src/config.js
Automatic merge failed; fix conflicts and then commit the result.
Passo 1. Vedere cosa è in conflitto
$ git status
On branch main
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 modified: src/config.js
Passo 2. Modificare il file
Nel file in conflitto Git inserisce dei marcatori:
<<<<<<< HEAD
const TIMEOUT = 5000;
=======
const TIMEOUT = 10000;
>>>>>>> feature/login
La parte superiore è il vostro branch corrente (HEAD), quella inferiore il branch da unire. Lasciate la variante corretta (o combinatele), eliminate tutte e tre le righe dei marcatori e salvate il file. Un errore frequente è dimenticare un marcatore a metà file; prima del commit cercateli: git diff --check evidenzierà i marcatori rimasti.
Passo 3. Segnare la risoluzione e completare il merge
git add src/config.js
git commit # oppure git merge --continue
Se il conflitto si risolve interamente scegliendo un lato, non serve modificare il file a mano:
git checkout --ours src/config.js # tenere la versione del branch corrente
git checkout --theirs src/config.js # prendere la versione del branch da unire
git add src/config.js
Più comodo nell'editor
VS Code, gli IDE di JetBrains e altri editor mostrano il conflitto su tre colonne e offrono pulsanti «accetta corrente / in arrivo / entrambi». Potete anche collegare uno strumento esterno con git mergetool. Un'impostazione utile è git config --global merge.conflictStyle zdiff3: nei marcatori compare anche la versione originale dell'antenato comune, e diventa chiaro chi ha cambiato cosa.
Ci avete ripensato: annullare il merge
git merge --abort
Il comando riporta la cartella di lavoro e l'index allo stato precedente a git merge. Per questo è meglio avere un working tree pulito prima del merge: fate il commit delle modifiche incomplete o mettetele da parte con git stash, altrimenti sarà difficile separarle dai risultati del merge.
Come annullare un merge già fatto
Se il merge commit non è ancora stato inviato al server, la cosa più semplice è riportare indietro il branch:
git reset --hard ORIG_HEAD
Git registra ORIG_HEAD prima delle operazioni rischiose, compreso il merge: è la posizione del branch prima del merge. L'opzione --hard scarta le modifiche non committate, quindi controllate git status.
Se il merge è già nel repository condiviso, la cronologia non si riscrive. Si crea un commit inverso:
git revert -m 1 a1b2c3d
Il flag -m 1 indica che il genitore «principale» è il primo (il branch in cui si è fatto il merge). Attenzione a un tranello: dopo un revert di questo tipo Git considera i commit della feature già integrati. Se in seguito vorrete unire di nuovo la feature corretta, dovrete prima fare il revert del commit di revert stesso.
Il merge nel lavoro di squadra
git pull è fetch più merge
Un normale git pull scarica le modifiche e fa il merge del branch remoto in quello locale. Se voi e un collega avete fatto commit in parallelo, comparirà un commit «Merge branch 'main' of ...». Per evitare questi merge tecnici, impostate git config --global pull.ff only (il pull fallisce se il fast-forward non è possibile e decidete voi) oppure usate git pull --rebase.
Pull request e il pulsante Merge
Su GitHub e GitLab il merge avviene di solito tramite pull/merge request. Il pulsante «Create a merge commit» equivale a git merge --no-ff, «Squash and merge» a un merge con squash, «Rebase and merge» a un rebase. La scelta della modalità è una convenzione del team, ma il meccanismo è lo stesso della console.
Unire main nel proprio branch
Un branch di lunga durata si allontana da main e i conflitti si accumulano. Portate regolarmente il branch principale nel vostro:
git switch feature/login
git fetch origin
git merge origin/main
Così i conflitti si risolvono a piccole dosi man mano che compaiono, e il merge finale in main fila liscio.
Errori tipici
- Merge nella direzione sbagliata. Avete eseguito
git merge mainstando sumain, oppure avete unito una feature in un'altra feature. Controllate il branch corrente congit branch --show-currentprima del merge. - Merge con un branch non aggiornato. Avete unito un
mainlocale che non veniva aggiornato da una settimana. Primagit pull, poi il merge. - «Your local changes would be overwritten by merge». Nella cartella di lavoro ci sono modifiche non committate negli stessi file. Fatene il commit o mettetele nello stash.
- «refusing to merge unrelated histories». I branch non hanno un antenato comune: per esempio, il repository locale è stato creato con
git inite quello remoto contiene già un README. Se l'unione è davvero necessaria, aggiungete--allow-unrelated-histories. - Commit con i marcatori di conflitto. La build fallisce su
<<<<<<<. Controllate congit diff --checke lanciate i test prima del commit.
Riepilogo
git merge è un modo sicuro per unire i branch: non riscrive i commit esistenti, ne aggiunge solo uno nuovo (o fa avanzare il puntatore). Fate il merge nel branch corrente, mantenete pulito il working tree, usate --no-ff se contano i confini delle feature e --squash se vi serve un solo commit. I conflitti si risolvono in tre passi: modifica del file, git add, git commit, e git merge --abort vi riporterà sempre al punto di partenza. Il merge dei branch e la risoluzione dei conflitti si esercitano a fondo nel corso su Git.
Domande frequenti
Qual è la differenza tra git merge e git pull?
git pull prima scarica le modifiche dal server (git fetch) e poi unisce il branch remoto a quello corrente con git merge o, se configurato, con rebase. git merge lavora solo con ciò che è già presente in locale.
Si possono unire più branch contemporaneamente?
Sì, git merge feature-a feature-b esegue un merge «octopus» con più genitori, ma solo se non ci sono conflitti. In pratica i branch si uniscono di solito uno alla volta.
Come vedere quali branch sono già stati uniti in quello corrente?
Il comando git branch --merged mostra i branch interamente inclusi in quello corrente, mentre git branch --no-merged quelli non ancora uniti. I branch già uniti si possono eliminare tranquillamente con git branch -d.
Come vedere in anticipo cosa porterà il merge?
Eseguite git log main..feature per vedere i commit che non sono in main, e git diff main...feature per vedere le modifiche del branch rispetto all'antenato comune.
Cos'è ORIG_HEAD dopo un merge?
È un riferimento che Git salva prima del merge: punta alla posizione del branch prima dell'operazione. Con git reset --hard ORIG_HEAD si può annullare un merge locale finché non è stato inviato al server.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Git da zero a sviluppatore sicuro».
- Branch: la realtà parallela del tuo codice
- Conflitti di unione: niente panico — analizziamo e risolviamo