git rebase: come funziona, modalità interattiva e squash dei commit
git rebase sposta i commit del branch corrente su una nuova base: Git toglie temporaneamente i vostri commit, sposta il branch in fondo a quello indicato e li riapplica come nuovi commit. Si ottiene così una cronologia lineare senza merge commit, e con git rebase -i i commit si possono unire (squash), rinominare o eliminare; in questo modo si riscrivono solo commit non ancora pubblicati per altri.
Cosa fa git rebase
git rebase «trapianta» il vostro branch in un altro punto della cronologia. Supponiamo di aver creato feature da main una settimana fa, e che da allora main sia andato avanti. Dopo il rebase il vostro branch appare come se aveste iniziato a lavorare adesso, a partire dal commit più recente di main.
git switch feature
git fetch origin
git rebase origin/main
Dietro le quinte succedono tre cose. Git trova l'antenato comune dei branch, salva in ordine le modifiche di ciascun vostro commit successivo, sposta il branch su origin/main e riapplica le modifiche salvate una dopo l'altra. Ne risultano nuovi commit con lo stesso contenuto e gli stessi messaggi, ma con hash diversi.
Prima: A---B---C---F---G main
\
D---E feature
Dopo: A---B---C---F---G main
\
D'---E' feature
Gli apici di D' ed E' non sono decorativi. Sono oggetti diversi con hash diversi e un genitore diverso. Da qui deriva la regola principale del rebase, di cui parliamo sotto.
La regola d'oro: non fate rebase di ciò che è pubblicato
Il rebase riscrive la cronologia. Finché i commit esistono solo da voi, è sicuro. Ma se i colleghi hanno già scaricato il branch, dopo il rebase loro avranno i vecchi D ed E, voi i nuovi D' ed E', e al pull successivo la cronologia si duplicherà e i conflitti andranno risolti di nuovo.
Regola pratica: il rebase si usa sui propri branch locali e sui feature branch personali in cui lavora una sola persona. I branch condivisi (main, develop, quelli di release) non si ribasano mai.
Come fare push di un branch dopo il rebase
Se il branch era già sul server, un normale git push verrà rifiutato: la cronologia locale non prosegue più quella remota. Serve un push forzato, nella variante sicura:
git push --force-with-lease
A differenza di --force, il flag --force-with-lease rifiuta di sovrascrivere il branch se sul server sono comparsi commit che non avete visto. Così non cancellerete per sbaglio il lavoro altrui.
Conflitti durante il rebase
Poiché i commit vengono applicati uno alla volta, un conflitto può presentarsi su ognuno di essi, anche più volte di fila. Git si ferma e indica quale commit non è riuscito ad applicare.
$ git rebase origin/main
Auto-merging src/api.js
CONFLICT (content): Merge conflict in src/api.js
error: could not apply 4e1f9a2... Aggiungere cache delle richieste
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
Come procedere:
- Aprite il file in conflitto, scegliete la variante corretta e rimuovete i marcatori
<<<<<<<,=======,>>>>>>>. - Segnate il file come risolto:
git add src/api.js. Non serve fare un commit. - Proseguite:
git rebase --continue. Git applicherà il commit successivo.
Gli altri comandi per gestire il processo:
| Comando | Cosa fa |
|---|---|
git rebase --continue | Proseguire dopo aver risolto il conflitto |
git rebase --skip | Saltare interamente il commit corrente (le sue modifiche andranno perse) |
git rebase --abort | Interrompere il rebase e riportare il branch allo stato iniziale |
Fate attenzione alla confusione tra lato «nostro» e «loro». Durante il rebase HEAD è la base su cui spostate il branch, mentre le modifiche «in arrivo» sono i vostri commit. Per questo git checkout --ours durante un rebase prende la versione di main, e --theirs la vostra. Nel merge è il contrario.
Conflitti ripetuti: rerere
Se fate spesso il rebase di un branch lungo e risolvete sempre lo stesso conflitto, attivate git config --global rerere.enabled true. Git ricorderà come avete risolto il conflitto e la volta successiva applicherà la soluzione da solo.
Rebase interattivo: sistemare la cronologia prima della pubblicazione
La parte più preziosa del rebase è la modalità interattiva. Permette di mettere in ordine i propri commit prima di mostrarli in review: unire «fix», «fix typo», «altro fix» in un unico commit sensato, riscrivere i messaggi, cambiare l'ordine o eliminare il superfluo.
git rebase -i HEAD~4 # ultimi 4 commit
git rebase -i origin/main # tutti i commit del branch che non sono in main
Git aprirà l'editor con l'elenco dei commit, dal più vecchio al più recente (al contrario di git log):
pick 1a2b3c4 Aggiungere form di accesso
pick 5d6e7f8 fix
pick 9a0b1c2 Validazione e-mail
pick 3d4e5f6 refuso nella validazione
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash" but keep only the previous commit's log message
# d, drop = remove commit
Cambiate la parola all'inizio della riga, salvate e chiudete l'editor: Git eseguirà il piano dall'alto verso il basso.
| Comando | Effetto |
|---|---|
pick | Lasciare il commit così com'è |
reword | Mantenere le modifiche ma riscrivere il messaggio |
edit | Fermarsi sul commit per modificarne il contenuto (git commit --amend) o dividerlo in più commit |
squash | Unire al commit precedente, combinando e modificando i messaggi |
fixup | Unire al commit precedente, scartando il messaggio di questo commit |
drop | Eliminare il commit insieme alle modifiche |
Spostare le righe cambia l'ordine dei commit; eliminare una riga equivale a drop.
Git squash: come unire più commit in uno
In Git non esiste un comando git squash separato. Lo «squash» dei commit si fa in tre modi, e il rebase interattivo è il più flessibile.
Metodo 1. Rebase interattivo
Nell'esempio sopra trasformiamo quattro commit in due ordinati:
pick 1a2b3c4 Aggiungere form di accesso
fixup 5d6e7f8 fix
pick 9a0b1c2 Validazione e-mail
fixup 3d4e5f6 refuso nella validazione
Il primo commit del gruppo deve restare pick: squash e fixup uniscono il commit a quello precedente, e il primo non ha nulla in cui confluire.
Metodo 2. Commit fixup e autosquash
Se sapete già quale commit state correggendo, create la correzione già contrassegnata:
git commit --fixup=1a2b3c4
git rebase -i --autosquash origin/main
Git posizionerà da solo il commit fixup sotto quello giusto e lo segnerà come fixup: resta solo da salvare il piano. Per attivare questo comportamento di default: git config --global rebase.autoSquash true.
Metodo 3. Comprimere tutto il branch in un commit
git reset --soft $(git merge-base HEAD origin/main)
git commit -m "Accesso con e-mail e validazione"
reset --soft riporta il puntatore del branch al punto di diramazione, ma lascia tutte le modifiche nell'index: resta da fare un solo commit. Un'altra opzione è git merge --squash durante il merge nel branch principale, oppure il pulsante «Squash and merge» nella pull request.
Varianti utili del rebase
git pull --rebase
Un normale pull unisce il branch remoto nel vostro e crea un merge commit se avete fatto commit in parallelo ai colleghi. git pull --rebase invece sposta i vostri commit locali sopra quelli remoti aggiornati. Per renderlo il comportamento predefinito: git config --global pull.rebase true. È proprio la zona sicura del rebase: i vostri commit locali non li ha ancora visti nessuno.
--onto: spostare una parte del branch
Avete creato feature-b da feature-a e ora volete che feature-b parta direttamente da main, senza i commit di feature-a:
git rebase --onto main feature-a feature-b
Si legge così: prendere i commit di feature-b che non sono in feature-a e trapiantarli su main.
--exec: verificare ogni commit
git rebase -i --exec "npm test" origin/main
Dopo ogni commit applicato vengono lanciati i test; se falliscono, il rebase si ferma sul commit problematico. Comodo quando è importante che ogni commit della cronologia compili.
--update-refs: una pila di branch dipendenti
Se mantenete una catena di branch che dipendono l'uno dall'altro, il flag --update-refs (Git 2.38+) sposta anche i branch intermedi quando fate il rebase di quello in cima.
Come annullare un rebase
Mentre il processo è in corso: git rebase --abort. Se il rebase è già terminato e il risultato non vi piace, i vecchi commit non sono spariti: li ricorda il registro dei riferimenti.
$ git reflog
e5f6a7b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e5f6a7b HEAD@{1}: rebase (pick): Validazione e-mail
...
c3d4e5f HEAD@{5}: commit: refuso nella validazione
$ git reset --hard HEAD@{5}
Trovate in git reflog la voce precedente all'inizio del rebase e riportate il branch lì. Subito dopo il rebase funziona anche la scorciatoia git reset --hard ORIG_HEAD. Il reflog è locale e per impostazione predefinita conserva le voci per circa 90 giorni, quindi non c'è motivo di farsi prendere dal panico dopo un rebase andato male.
Errori tipici
- Rebase di un branch condiviso seguito da force push: la cronologia dei colleghi va in pezzi. Rimedio: accordarsi, ripristinare il branch dal reflog di chi ha la versione corretta e vietare il force push sui branch protetti del server.
- Usare
--forceinvece di--force-with-leasee cancellare un commit che un collega aveva fatto in tempo a pushare sullo stesso branch. - Squash sulla prima riga del piano interattivo: Git risponde con l'errore «cannot 'squash' without a previous commit».
- Avviare il rebase con il working tree sporco. Git rifiuterà di partire. Fate il commit delle modifiche, mettetele in
git stasho usategit rebase --autostash. - Risolvere il conflitto e fare
git commitinvece digit rebase --continue. Si ottiene un commit in più; dopo--continuesi può rimuovere con un altro rebase interattivo.
Riepilogo
git rebase sposta i vostri commit su una nuova base e li ricrea, per cui la cronologia risulta lineare e pulita. La modalità interattiva trasforma i commit provvisori in una cronologia ordinata: squash e fixup uniscono le correzioni, reword sistema i messaggi, drop elimina il superfluo. La regola principale è una sola: fate il rebase solo di ciò che non avete ancora pubblicato per altri, e dopo il rebase del vostro branch remoto inviatelo con --force-with-lease. Il lavoro con rebase e la pulizia della cronologia dei commit sono affrontati in pratica nel corso su Git.
Domande frequenti
git rebase cambia il contenuto del codice?
In assenza di conflitti il contenuto finale dei file sarà lo stesso di un merge, ma i commit vengono ricreati con nuovi hash e nuovi genitori. Se in un conflitto avete scelto un'altra variante, i commit intermedi differiranno dagli originali.
Come fare il rebase fino al primissimo commit del repository?
Usate git rebase -i --root: nel piano entrano tutti i commit, compreso quello radice. Serve raramente, per esempio per riscrivere il messaggio del primo commit.
Il rebase conserva autore e data dei commit?
Autore e data di authoring vengono conservati, mentre committer e data del commit diventano quelli attuali. Il flag --committer-date-is-author-date mantiene la data del commit uguale a quella di authoring.
Si può fare il rebase dei merge commit?
Per impostazione predefinita il rebase linearizza la cronologia e scarta i merge commit. Per conservare la struttura dei merge usate git rebase --rebase-merges.
Come capire se è in corso un rebase?
git status mostrerà la riga interactive rebase in progress o You are currently rebasing e indicherà i comandi disponibili. Molte shell e IDE lo mostrano anche nel prompt.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Git da zero a sviluppatore sicuro».
Vai al corso