Programma
"Commit early, commit often" è un mantra diffuso nello sviluppo software quando si usa Git. Farlo garantisce che ogni modifica sia ben documentata, migliora la collaborazione e rende più facile seguire l'evoluzione del progetto. Tuttavia, può anche portare a un eccesso di commit.
Qui entra in gioco l'importanza dello squash dei commit. Fare squash significa combinare più commit in un unico commit coeso.
Per esempio, supponiamo di lavorare su una feature che implementa un form di login e di creare i seguenti quattro commit:

Una volta completata la feature, per il progetto nel suo complesso questi commit sono troppo dettagliati. In futuro non ci serve sapere che abbiamo incontrato un bug risolto durante lo sviluppo. Per mantenere una cronologia pulita nel branch principale, facciamo lo squash di questi commit in un unico commit:

Come fare squash dei commit in Git: interactive rebase
Il metodo più comune per fare squash dei commit è usare un interactive rebase. Lo avviamo con il comando:
git rebase -i HEAD~<number_of_commits>
Sostituisci <number_of_commits> con il numero di commit che vogliamo fare squash.
Nel nostro caso abbiamo quattro commit, quindi il comando è:
git rebase -i HEAD~4
Eseguire questo comando aprirà un editor della riga di comando interattivo:

La sezione superiore mostra i commit, mentre in basso trovi commenti su come fare lo squash dei commit.
Vediamo quattro commit. Per ciascuno dobbiamo decidere quale comando eseguire. Ci interessano i comandi pick (p) e squash (s). Per fare lo squash di questi quattro commit in un unico commit, possiamo pickare il primo e squasciare i restanti tre.
Applichiamo i comandi modificando il testo che precede ciascun commit, cambiando in particolare pick in s o squash per il secondo, il terzo e il quarto commit. Per fare queste modifiche, dobbiamo entrare in modalità "INSERT" nell'editor a riga di comando premendo il tasto i sulla tastiera:

Dopo aver premuto i, in basso apparirà il testo -- INSERT --, a indicare che siamo entrati in modalità insert. Ora possiamo muovere il cursore con le frecce, cancellare caratteri e digitare come in un normale editor di testo:

Quando siamo soddisfatti delle modifiche, dobbiamo uscire dalla modalità insert premendo il tasto Esc sulla tastiera. Il passo successivo è salvare le modifiche e uscire dall'editor. Per farlo, premiamo prima il tasto : per comunicare all'editor che intendiamo eseguire un comando:

In basso nell'editor ora vediamo un due punti : che ci invita a inserire un comando. Per salvare le modifiche usiamo il comando w, abbreviazione di "write". Per chiudere l'editor usa q, abbreviazione di "quit". Questi comandi si possono combinare e digitare insieme come wq:

Per eseguire il comando, premiamo il tasto Enter. Questa azione chiuderà l'editor corrente e ne aprirà uno nuovo, permettendoci di inserire il messaggio del commit appena squasciato. L'editor mostrerà un messaggio predefinito composto dai messaggi dei quattro commit che stiamo squasciando:

Ti consiglio di modificare il messaggio in modo che rifletta accuratamente le modifiche introdotte da questi commit combinati: dopotutto, lo scopo dello squash è mantenere una cronologia pulita e facilmente leggibile.
Per interagire con l'editor e modificare il messaggio, premiamo di nuovo i per entrare in modalità di modifica e apportiamo le modifiche desiderate.

In questo caso, sostituiamo il messaggio del commit con "Implement login form." Per uscire dalla modalità di modifica, premiamo Esc. Quindi salviamo le modifiche premendo :, inserendo il comando wq e premendo Enter.
Come visualizzare la cronologia dei commit
In generale, ricordare l'intera cronologia dei commit può essere difficile. Per visualizzare la cronologia possiamo usare il comando git log. Nell'esempio citato, prima di eseguire lo squash, l'esecuzione di git log mostrerebbe:

Per scorrere l'elenco dei commit, usa le frecce su e giù. Per uscire, premi q.
Possiamo usare git log per confermare il successo dello squash. Eseguendolo dopo lo squash verrà mostrato un solo commit con il nuovo messaggio:

Push del commit squasciato
Il comando sopra agirà sul repository locale. Per aggiornare il repository remoto, dobbiamo pushare le modifiche. Tuttavia, poiché abbiamo cambiato la cronologia dei commit, dobbiamo forzare il push usando l'opzione --force:
git push --force origin feature/login-form
Il force push sovrascriverà la cronologia dei commit sul branch remoto e potrebbe creare problemi a chi sta lavorando su quel branch. È buona prassi avvisare il team prima di farlo
Un modo più sicuro di forzare il push, che riduce il rischio di interrompere il lavoro dei collaboratori, è usare l'opzione --force-with-lease:
git push --force-with-lease origin feature/login-form
Questa opzione assicura che forziamo il push solo se il branch remoto non è stato aggiornato dall'ultimo fetch o pull.
Fare squash di commit specifici
Immagina di avere cinque commit:

Supponiamo di voler mantenere i commit 1, 2 e 5 e fare squash dei commit 3 e 4.

Con l'interactive rebase, i commit contrassegnati per lo squash verranno combinati con il commit immediatamente precedente. In questo caso, significa che vogliamo squasciare Commit4 in modo che si unisca a Commit3.
Per farlo, dobbiamo avviare un interactive rebase che includa questi due commit. In questo caso, bastano tre commit, quindi usiamo il comando:
git rebase -i HEAD~3
Poi impostiamo Commit4 su s in modo che venga squasciato con Commit3:

Dopo aver eseguito questo comando e aver elencato i commit, osserviamo che i commit 3 e 4 sono stati squasciati insieme mentre il resto è rimasto invariato.

Squash a partire da un commit specifico
Nel comando git rebase -i HEAD~3, la parte HEAD è una scorciatoia per l'ultimo commit. La sintassi ~3 viene usata per specificare un antenato di un commit. Per esempio, HEAD~1 si riferisce al padre del commit HEAD.

In un interactive rebase, i commit considerati includono tutti gli antenati fino al commit specificato nel comando. Nota che il commit specificato non è incluso:

Invece di usare HEAD, possiamo specificare direttamente un hash di commit. Per esempio, Commit2 ha hash dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf, quindi il comando:
git rebase -i dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf
avvierebbe un rebase considerando tutti i commit effettuati dopo Commit2. Quindi, se vogliamo avviare un rebase a un commit specifico e includere anche quel commit, possiamo usare il comando:
git rebase -i <commit-hash>~1
Risoluzione dei conflitti durante lo squash dei commit
Quando facciamo lo squash dei commit, combiniamo modifiche multiple in un unico commit, il che può portare a conflitti se le modifiche si sovrappongono o divergono in modo significativo. Ecco alcuni scenari comuni in cui potrebbero sorgere conflitti:
- Modifiche sovrapposte: se due o più commit oggetto di squash hanno modificato le stesse righe di un file o righe strettamente correlate, Git potrebbe non riuscire a conciliare automaticamente queste modifiche.
- Stato delle modifiche diverso: se un commit aggiunge un certo pezzo di codice e un altro commit modifica o elimina quello stesso pezzo di codice, fare lo squash di questi commit può generare conflitti da risolvere.
- Rinominare e modificare: se un commit rinomina un file e i commit successivi apportano modifiche al vecchio nome, fare lo squash di questi commit può confondere Git, causando un conflitto.
- Modifiche a file binari: i file binari non si fondono bene con strumenti di diff basati su testo. Se più commit modificano lo stesso file binario e proviamo a fare lo squash, può verificarsi un conflitto perché Git non riesce a riconciliare automaticamente tali modifiche.
- Storia complessa: se i commit hanno una storia complessa con più merge, branching o rebase nel mezzo, squasciarli può generare conflitti per via della natura non lineare delle modifiche.
Durante lo squash, Git proverà ad applicare ciascuna modifica una per volta. Se incontra conflitti, si fermerà e ci permetterà di risolverli.
I conflitti saranno contrassegnati con i marcatori <<<<<< e >>>>>>. Per gestirli, dobbiamo aprire i file e risolvere manualmente ciascun conflitto scegliendo quale parte di codice mantenere.
Dopo aver risolto i conflitti, dobbiamo mettere in stage i file risolti con il comando git add. Poi possiamo continuare il rebase con il seguente comando:
git rebase --continue
Per approfondire i conflitti in Git, dai un'occhiata a questo tutorial su come risolvere i conflitti di merge in Git.
Alternative allo squash con rebase
Il comando git merge --squash è un metodo alternativo a git rebase -i per combinare più commit in un singolo commit. Questo comando è particolarmente utile quando vogliamo unire le modifiche di un branch nel branch principale facendo lo squash di tutti i commit individuali in uno solo. Ecco una panoramica di come fare lo squash usando git merge:
- Passiamo al branch di destinazione in cui vogliamo incorporare le modifiche.
- Eseguiamo il comando
git merge --squash <branch-name>sostituendo<branch-namecon il nome del branch. - Facciamo il commit delle modifiche con
git commitper creare un unico commit che rappresenti tutte le modifiche del feature branch.
Per esempio, diciamo di voler incorporare le modifiche del branch feature/login-form in main come un singolo commit:
git checkout main
git merge --squash feature-branch
git commit -m "Implement login form"
Queste sono le limitazioni di questo approccio rispetto a git rebase -i:
- Granularità del controllo: meno controllo sui singoli commit. Con il rebase possiamo scegliere quali commit unire, mentre con il merge siamo costretti a combinare tutte le modifiche in un unico commit.
- Storia intermedia: usando il merge, la cronologia dei singoli commit del feature branch si perde nel branch principale. Questo può rendere più difficile tracciare le modifiche incrementali effettuate durante lo sviluppo della feature.
- Revisione pre-commit: poiché mette in stage tutte le modifiche come un unico insieme, non possiamo revisionare o testare ciascun commit singolarmente prima dello squash, a differenza dell'interactive rebase in cui ogni commit può essere revisionato e testato in sequenza.
Conclusione
Integrare commit frequenti e piccoli nel flusso di lavoro favorisce la collaborazione e una documentazione chiara, ma può anche appesantire la cronologia del progetto. Lo squash dei commit trova un equilibrio, preservando le tappe importanti ed eliminando il rumore delle modifiche iterative minori.
Per saperne di più su Git, ti consiglio queste risorse:


