Lezione 9 di 35

Git 09

🔀 Git & GitHub · Modulo 2 · Lezione 09

Merge: fast-forward e three-way 🔀

Ricombinare i branch · Fast-forward vs three-way · Merge commit · git merge in pratica

⏱️ Durata stimata: 75–90 min 📊 Livello: Intermedio (Modulo 2) 🔧 Strumenti: Terminale · Editor di testo 📋 Prerequisiti: Lezioni 01–08 (sai creare e navigare tra branch)

🔀 Creare branch è solo metà del lavoro. L’altra metà è rimetterli insieme. Il merge è l’operazione che prende il lavoro fatto su un branch e lo integra in un altro. Git è straordinariamente bravo a farlo automaticamente — nella maggior parte dei casi, un merge richiede un solo comando e zero intervento manuale. In questa lezione impari i due tipi di merge e quando Git usa ciascuno.

🎯 Obiettivi della lezione

  • Capire cosa significa “fare merge” di un branch
  • Distinguere tra merge fast-forward e merge three-way (ricorsivo)
  • Eseguire un merge con git merge
  • Leggere un merge commit nel log
  • Capire quando Git sceglie fast-forward e quando three-way
  • Forzare un merge commit con --no-ff
  • Pulire il branch dopo il merge

📖 1. Teoria

1.1 — Cos’è un merge

Un merge è l’operazione che integra le modifiche di un branch in un altro. Il workflow tipico è: (1) crei un branch, (2) ci lavori sopra, (3) quando il lavoro è finito, fai merge del branch dentro main. Dopo il merge, main contiene tutto il lavoro che hai fatto sul branch.

Il comando è semplice: ti sposti sul branch che deve ricevere le modifiche (di solito main), poi esegui git merge nome-branch:

# 1. Spostati sul branch di destinazione
$ git switch main

# 2. Integra il branch sorgente
$ git merge feature/navbar

📌 Regola di direzione: il merge porta le modifiche nel branch su cui sei. Se sei su main e fai git merge feature/navbar, le modifiche di navbar entrano in main. Il branch navbar non viene toccato — dopo il merge esiste ancora e punta allo stesso commit di prima.

1.2 — Fast-forward merge: il caso semplice

Il fast-forward succede quando il branch di destinazione (main) non ha ricevuto nuovi commit da quando il branch sorgente è stato creato. In altre parole, main è rimasto fermo — tutto il lavoro nuovo è sul branch.

“`
Prima del merge: main è fermo, feature ha 2 commit in più
C1
→
C2
← main
→
C3
→
C4
← feature
Main è un antenato diretto di feature — Git può semplicemente “spostare main in avanti”.
Dopo git merge feature: fast-forward — main si sposta
C1
→
C2
→
C3
→
C4
← main (spostato!)
← feature
Nessun nuovo commit creato. Main è semplicemente avanzato. La storia resta lineare.
“`
$ git switch main
$ git merge feature/navbar
Updating a6b7c8d..b1a2c3d
Fast-forward
navbar.html | 1 +
style.css | 1 +
2 files changed, 2 insertions(+)
Git dice “Fast-forward” — ha semplicemente spostato il puntatore di main in avanti. Nessun merge commit è stato creato. La storia resta una linea dritta.
Quando succede un fast-forward?
Il fast-forward è possibile solo quando il branch di destinazione (main) non ha ricevuto nuovi commit dal momento in cui il branch sorgente è stato creato. In altre parole, main è un antenato diretto del branch — c’è un percorso lineare da main al branch. Git non deve “combinare” nulla — basta spostare il puntatore.

1.3 — Three-way merge: il caso con divergenza

Il three-way merge succede quando entrambi i branch hanno ricevuto commit dopo la separazione. Main non è più fermo — qualcuno ci ha lavorato sopra. Git deve combinare le modifiche di entrambi i branch.

“`
Prima del merge: entrambi i branch hanno commit nuovi
C1
→
C2
base comune
↗
C3
main
↘
C4
→
C5
feature
Main e feature hanno diverguto — ciascuno ha commit che l’altro non ha.
Dopo git merge feature: Git crea un merge commit
C1
→
C2
↗
C3
↘
↘
C4→C5
↗
M
merge commit
← main
Git ha creato un merge commit (M) con due genitori: C3 (da main) e C5 (da feature). Contiene il risultato combinato di entrambi i branch.
“`
$ git switch main
$ git merge feature/contatti
Merge made by the ‘ort’ strategy.
contatti.html | 8 ++++++++
1 file changed, 8 insertions(+)
create mode 100644 contatti.html
Git dice “Merge made by the ‘ort’ strategy” — ha dovuto creare un merge commit perché i branch avevano diverguto. L’editor si è aperto per farti scrivere il messaggio del merge commit (il default Merge branch 'feature/contatti' va benissimo).

1.4 — Confronto: fast-forward vs three-way

Fast-forward Three-way
Quando Main non ha nuovi commit dopo la separazione Entrambi i branch hanno nuovi commit
Cosa fa Git Sposta il puntatore di main in avanti Crea un nuovo commit (merge commit) con 2 genitori
Merge commit? No — storia lineare Sì — storia ramificata
Output nel terminale Fast-forward Merge made by the ‘ort’ strategy
Apre l’editor? No Sì (per il messaggio del merge commit)
Conflitti possibili? Mai Possibili (li gestirai nella Lezione 10)

1.5 — Il merge commit: un commit speciale

Un merge commit è un commit con due genitori invece di uno solo. Ogni commit normale ha un solo genitore (il commit precedente). Un merge commit ha due genitori: uno per ciascun branch che è stato combinato.

$ git log –oneline –graph -n 5
* f8a9b0c (HEAD -> main) Merge branch ‘feature/contatti’
|\
| * d7e8f9a (feature/contatti) Aggiunto form di contatto
| * b5c6d7e Creata pagina contatti base
* | a1b2c3d Corretto typo nella home
|/
* e4f5a6b Aggiunto foglio di stile
Il merge commit f8a9b0c ha la forma |\ nel grafo: due linee che si ricongiungono. Puoi vedere i due rami — main (sinistra) e feature/contatti (destra) — che convergono nel merge commit.
# Ispezionare il merge commit
$ git show –stat f8a9b0c
commit f8a9b0c…
Merge: a1b2c3d d7e8f9a ← due genitori! (main e feature)
Author: Mario Rossi <mario.rossi@email.it>
Date: …

Merge branch ‘feature/contatti’

contatti.html | 8 ++++++++
1 file changed, 8 insertions(+)

1.6 — Forzare un merge commit: –no-ff

A volte vuoi un merge commit anche quando Git potrebbe fare fast-forward. Perché? Perché il merge commit crea un “nodo” visibile nella storia che documenta quando e perché un branch è stato integrato. Con il fast-forward, quella informazione si perde — la storia sembra lineare come se il branch non fosse mai esistito.

Fast-forward (default)
$ git merge feature/nav
Storia lineare, pulita. Non si vede che esisteva un branch. Pro: storia semplice. Contro: perdi il contesto del branch.
No fast-forward (forzato)
$ git merge –no-ff feature/nav
Crea sempre un merge commit, anche se potrebbe fare fast-forward. Pro: vedi nel log che esisteva un branch. Contro: storia più complessa.
Confronto visuale nel grafo
Fast-forward
* C4 (main)
* C3
* C2
* C1
Lineare — nessuna traccia del branch
–no-ff
* M (main, merge commit)
|\
| * C4 (feature)
| * C3
|/
* C2
* C1
Si vede che C3-C4 erano su un branch

📌 Quale scegliere? Per progetti scolastici e personali, il fast-forward (default) va benissimo. Per team professionali, molti preferiscono --no-ff perché il merge commit documenta chiaramente che una feature è stata sviluppata su un branch separato. GitHub stesso usa sempre merge commit (non fast-forward) quando integri una Pull Request.

1.7 — Dopo il merge: pulizia

Dopo un merge, il branch sorgente esiste ancora — punta allo stesso commit di prima. Se il lavoro è finito, puoi (e dovresti) cancellarlo per tenere il repository pulito:

# Il merge è stato fatto. Puliamo.
$ git branch -d feature/navbar
Deleted branch feature/navbar (was b1a2c3d).

# -d funziona perché il branch è stato merged!
# Se non fosse stato merged, Git avrebbe rifiutato.

$ git branch
* main
I commit del branch non vengono cancellati — sono raggiungibili da main tramite il merge. Hai solo rimosso l’etichetta (puntatore) del branch. La storia è intatta.

1.8 — Il workflow completo: branch → lavora → merge → pulisci

Il ciclo completo di una feature
1. BRANCH
git switch -c
feature/x
→
2. LAVORA
add, commit
add, commit
…
→
3. MERGE
git switch main
git merge
feature/x
→
4. PULISCI
git branch -d
feature/x
Questo ciclo si ripete per ogni nuova feature, fix o esperimento. È il “ritmo” del lavoro con Git.

💻 2. Esempi guidati

Esempio A — Fast-forward merge

# === SETUP ===
$ mkdir lab-merge && cd lab-merge && git init
$ echo “<h1>Home</h1>” > index.html
$ git add . && git commit -m “Setup: pagina home”

# === BRANCH E LAVORO ===
$ git switch -c feature/footer
$ echo “
© 2026
” > footer.html

$ git add . && git commit -m “Aggiunto footer”
$ echo “footer { background: #333; }” > footer.css
$ git add . && git commit -m “Aggiunti stili footer”

# === MERGE (main non ha nuovi commit = fast-forward!) ===
$ git switch main
$ git merge feature/footer
Fast-forward
footer.css | 1 +
footer.html | 1 +
2 files changed, 2 insertions(+)

# === VERIFICA ===
$ git log –oneline –graph –all
* c3d4e5f (HEAD -> main, feature/footer) Aggiunti stili footer
* a1b2c3d Aggiunto footer
* e4f5a6b Setup: pagina home
Storia lineare! Main e feature/footer puntano allo stesso commit. Nessun merge commit.
# === PULIZIA ===
$ git branch -d feature/footer
Deleted branch feature/footer (was c3d4e5f).

Esempio B — Three-way merge

# Continuiamo dal repo dell’Esempio A

# === CREIAMO DIVERGENZA ===
$ git switch -c feature/contatti
$ echo “<form>Contattaci</form>” > contatti.html
$ git add . && git commit -m “Aggiunta pagina contatti”

# Torna a main e fai un commit (questo crea la divergenza!)
$ git switch main
$ echo “

Benvenuto sul sito

” >> index.html

$ git add . && git commit -m “Aggiunto paragrafo di benvenuto”

# Ora main e feature/contatti hanno entrambi commit nuovi
$ git log –oneline –graph –all
* b7c8d9e (HEAD -> main) Aggiunto paragrafo di benvenuto
| * d4e5f6a (feature/contatti) Aggiunta pagina contatti
|/
* c3d4e5f Aggiunti stili footer
* …

# === MERGE (three-way perché c’è divergenza) ===
$ git merge feature/contatti
# Si apre l’editor con il messaggio: Merge branch ‘feature/contatti’
# Salva e chiudi l’editor
Merge made by the ‘ort’ strategy.
contatti.html | 1 +
1 file changed, 1 insertion(+)

# === VERIFICA: ora c’è un merge commit! ===
$ git log –oneline –graph –all
* f8a9b0c (HEAD -> main) Merge branch ‘feature/contatti’
|
| * d4e5f6a (feature/contatti) Aggiunta pagina contatti
* | b7c8d9e Aggiunto paragrafo di benvenuto
|/
* c3d4e5f Aggiunti stili footer
Il merge commit f8a9b0c ha due genitori (le due linee che convergono). Main ora contiene sia il paragrafo di benvenuto che la pagina contatti.

Esempio C — Merge con –no-ff

# Puliamo il branch precedente
$ git branch -d feature/contatti

# Creiamo un nuovo branch (senza divergenza su main)
$ git switch -c feature/logo
$ echo “<img src=’logo.png’>” > logo.html
$ git add . && git commit -m “Aggiunto logo”

# Merge forzando il merge commit
$ git switch main
$ git merge –no-ff feature/logo
# Si apre l’editor — salva e chiudi
Merge made by the ‘ort’ strategy.

$ git log –oneline –graph -n 4
* a2b3c4d (HEAD -> main) Merge branch ‘feature/logo’
|
| * e5f6a7b (feature/logo) Aggiunto logo
|/
* f8a9b0c Merge branch ‘feature/contatti’
Anche se il fast-forward era possibile (main non aveva nuovi commit), il --no-ff ha forzato un merge commit. Nel grafo si vede chiaramente il “palloncino” del branch logo.

🏋️ 3. Esercizi

🟢 Esercizio 1 — Il tuo primo merge (Facile)

a) Crea un repo con un commit iniziale (README.md).
b) Crea un branch feature/about, crea about.html e fai 1 commit.
c) Torna a main. Fai il merge: git merge feature/about.
d) Che tipo di merge è stato? (Guarda l’output: “Fast-forward” o “Merge made by”?)
e) Il file about.html è visibile su main?
f) Cancella il branch con git branch -d feature/about.
g) Il file about.html è ancora su main? (Sì — il merge lo ha integrato.)

🔵 Esercizio 2 — Three-way merge (Medio)

a) Parti dal repo dell’esercizio 1. Crea un branch feature/gallery e fai 2 commit (crea gallery.html, poi aggiungi contenuto).
b) Torna a main e fai 1 commit (modifica README.md aggiungendo una riga).
c) Verifica con git log --oneline --graph --all che i branch abbiano diverguto.
d) Fai il merge: git merge feature/gallery.
e) Che tipo di merge è stato? C’è un merge commit?
f) Quanti genitori ha il merge commit? (Verificalo con git show --stat HEAD — cerca “Merge:”)
g) Cancella il branch e verifica la storia.

🟠 Esercizio 3 — ff vs –no-ff (Medio)

a) Crea un repo nuovo con un commit iniziale.
b) Crea feature/a, fai 1 commit. Torna a main. Merge con fast-forward (default).
c) Guarda il grafo con git log --oneline --graph. È lineare?
d) Crea feature/b, fai 1 commit. Torna a main. Merge con --no-ff.
e) Guarda il grafo. Si vede il “palloncino” del branch?
f) Confronta i due grafi. Quale preferisci? Spiega perché.

🟣 Esercizio 4 — Simulazione progetto completo (Difficile)

Simula un ciclo di sviluppo completo con 3 feature:

a) Crea un repo progetto-merge con commit iniziale: README.md, index.html, style.css.
b) Feature 1 (navbar): crea branch, 2 commit, torna a main, merge, cancella branch.
c) Feature 2 (gallery): crea branch e fai 1 commit. Ma prima di fare merge, torna a main e fai un commit di fix sul README. Poi merge la gallery. Che tipo di merge è stato?
d) Feature 3 (contatti): crea branch, 2 commit, torna a main, merge con --no-ff, cancella branch.
e) Guarda il grafo finale: git log --oneline --graph --all. Disegnalo su carta. Identifica: quanti merge commit ci sono? Quali sono fast-forward e quali three-way?
f) Quanti file ha main alla fine? Elencali con ls.

❓ 4. Quiz di verifica

1. Quando fai git merge feature/x, le modifiche entrano nel branch feature/x o nel branch su cui ti trovi?

Mostra risposta

Le modifiche entrano nel branch su cui ti trovi (quello indicato da HEAD). Se sei su main e fai git merge feature/x, le modifiche di feature/x vengono integrate in main. Il branch feature/x non viene toccato — punta ancora allo stesso commit di prima. Per questo il primo passo è sempre git switch main (o il branch che deve ricevere le modifiche) prima di eseguire il merge.


2. Qual è la condizione per un fast-forward merge?

Mostra risposta

Il fast-forward è possibile solo quando il branch di destinazione (main) non ha ricevuto nuovi commit dopo la creazione del branch sorgente. In altre parole, main è un antenato diretto del branch — c’è un percorso lineare senza biforcazioni. In questo caso, Git non ha bisogno di “combinare” nulla — basta spostare il puntatore di main in avanti fino al commit del branch sorgente. Se main ha nuovi commit (i branch hanno diverguto), il fast-forward non è possibile e Git deve fare un three-way merge.


3. Cos’è un merge commit e cosa lo rende diverso da un commit normale?

Mostra risposta

Un merge commit è un commit speciale che ha due genitori invece di uno. Ogni commit normale ha un solo genitore (il commit precedente). Un merge commit ha due genitori: uno dal branch di destinazione e uno dal branch sorgente. Nel grafo di git log --graph, appare come il punto dove due linee convergono. Il contenuto del merge commit è il risultato della combinazione delle modifiche di entrambi i branch. Puoi vedere i due genitori con git show --stat HASH (la riga “Merge: hash1 hash2”).


4. Dopo il merge, il branch sorgente esiste ancora. Perché, e cosa dovresti fare?

Mostra risposta

Il merge non cancella il branch sorgente — ne integra solo le modifiche nel branch di destinazione. Il branch sorgente continua a esistere e punta allo stesso commit di prima. Se il lavoro su quel branch è finito, dovresti cancellarlo con git branch -d nome per tenere il repository pulito. I commit non vengono cancellati perché sono raggiungibili da main attraverso il merge. Se prevedi di continuare a lavorare sullo stesso branch, puoi tenerlo — ma in pratica è meglio creare branch nuovi per ogni nuova unità di lavoro.


5. A cosa serve il flag --no-ff in git merge --no-ff?

Mostra risposta

Forza la creazione di un merge commit anche quando Git potrebbe fare fast-forward. Senza --no-ff, se i branch non hanno diverguto, Git fa fast-forward e la storia resta lineare — non c’è traccia visiva nel log che il lavoro è stato fatto su un branch separato. Con --no-ff, il merge commit crea un “nodo” nel grafo che documenta chiaramente quando e da dove le modifiche sono state integrate. Molti team lo preferiscono perché rende la storia più leggibile. GitHub usa questo comportamento quando integri una Pull Request.


6. Nell’output di git merge leggi “Fast-forward”. Cosa è successo e quanti commit sono stati creati?

Mostra risposta

Zero commit sono stati creati. Git ha semplicemente spostato il puntatore del branch di destinazione (main) in avanti fino al commit a cui puntava il branch sorgente. Non c’è stato bisogno di combinare modifiche perché main non aveva commit propri — era un antenato diretto del branch sorgente. La storia resta lineare, come se il lavoro fosse stato fatto direttamente su main. È l’operazione di merge più semplice e veloce possibile.


📚 Riepilogo della lezione

Cosa hai imparato oggi:
  • git merge nome-branch — integra le modifiche nel branch corrente
  • Prima di merge: spostati sul branch che deve ricevere (git switch main)
  • Fast-forward: main non ha nuovi commit → Git sposta il puntatore → storia lineare, nessun merge commit
  • Three-way: entrambi i branch hanno diverguto → Git crea un merge commit con 2 genitori
  • --no-ff — forza il merge commit anche quando il fast-forward è possibile
  • Dopo il merge: git branch -d nome per cancellare il branch sorgente
  • Il workflow: branch → lavora → switch main → merge → cancella branch
  • Merge commit nel grafo: |\ e |/ — due linee che convergono

Prossima lezione →

Lezione 10 — Gestire i conflitti di merge
Come nascono i conflitti, come si presentano nel file, come risolverli.
Marker <<< === >>>. Esercitazione pratica con conflitto simulato.

Corso Git & GitHub · Per studenti e insegnanti · Lezione 09 di 45