Guida

git merge: come unire i branch e risolvere i conflitti

Aggiornato: · Skillaria.Top

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

FlagCosa faQuando serve
--no-ffCrea sempre un merge commit, anche se il fast-forward è possibileVolete vedere nella cronologia quali commit provengono da una stessa feature
--ff-onlyEsegue il merge solo con fast-forward, altrimenti termina con erroreAggiornare un branch locale senza merge commit accidentali
--squashMette tutte le modifiche del branch nell'index, ma non fa il commit né crea un legame con il branchTrasformare una feature in un unico commit pulito
--no-commitEsegue il merge ma si ferma prima del commitVolete controllare il risultato o lanciare i test prima di registrarlo
-m "testo"Imposta il messaggio del merge commitMerge negli script e senza editor
--abortAnnulla il merge in corso e ripristina lo stato precedenteTroppi conflitti, meglio ricominciare
--continueCompleta il merge dopo la risoluzione dei conflittiAlternativa a git commit nell'ultimo passaggio
-X ours / -X theirsNei frammenti in conflitto prende automaticamente la nostra o la loro versioneSapete 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 main stando su main, oppure avete unito una feature in un'altra feature. Controllate il branch corrente con git branch --show-current prima del merge.
  • Merge con un branch non aggiornato. Avete unito un main locale che non veniva aggiornato da una settimana. Prima git 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 init e 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 con git diff --check e lanciate i test prima del commit.

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.

Impara l’argomento in video

Questo argomento è trattato in modo approfondito, con pratica, nel corso «Git da zero a sviluppatore sicuro».

Vai al corso