Git Rebase: come funziona e perché dovresti usarlo
Nel sistema di controllo di versione Git, esistono due modi per combinare un branch con un altro, rappresentati da comandi diversi:
-
git merge. I commit di un branch vengono trasferiti in un altro creando un commit di unione (merge commit). -
git rebase. I commit di un branch vengono trasferiti in un altro branch mantenendo l’ordine originale delle modifiche.
In parole semplici: con git merge, i commit di un branch vengono “compressi” in uno solo, mentre con git rebase rimangono intatti, ma i branch vengono comunque combinati.
Il comando git rebase consente quindi di combinare i commit di entrambi i branch formando una cronologia condivisa delle modifiche.
Questa guida illustra il comando git rebase, che serve a riposizionare (rebase) i commit (modifiche) da un branch a un altro.
Tutti gli esempi mostrati utilizzano Git versione 2.34.1, in esecuzione su un server Hostman con sistema operativo Ubuntu 22.04.
Puoi utilizzare queste guide per installare Git sul tuo computer:
Che cos’è Git Rebase Copia link
Git Rebase è un potente comando Git utilizzato principalmente per integrare le modifiche di un branch su un altro riscrivendo la cronologia dei commit. A differenza di git merge, che crea un nuovo commit di unione e preserva la cronologia di entrambi i branch, git rebase sposta o “riproduce” una serie di commit da un branch su un altro. Questo processo produce una cronologia lineare, facendo apparire come se il branch delle funzionalità fosse stato sviluppato direttamente a partire dall’ultimo commit del branch di destinazione, ad esempio main o master. In questo modo, si ottiene una cronologia più pulita e leggibile, eliminando i commit di merge non necessari e rendendo la cronologia del progetto più ordinata.
Come funziona Git Rebase Copia link
Il modo migliore per comprendere come funziona il rebase in Git è osservare un repository astratto composto da diversi branch, analizzando passo dopo passo l’operazione di rebase.
Creazione dei branch Copia link
Supponiamo di aver creato un repository con un solo branch master, contenente un singolo commit. Il branch master appare così:
master
commit_1Successivamente, a partire da master, abbiamo creato un nuovo branch chiamato hypothesis, dove abbiamo deciso di testare alcune funzionalità. In questo nuovo branch abbiamo eseguito diversi commit per migliorare il codice. Ora il branch appare così:
hypothesis
commit_4
commit_3
commit_2
commit_1Più tardi, abbiamo aggiunto un altro commit al branch master per correggere urgentemente una vulnerabilità. Ora il branch master appare così:
master
commit_5
commit_1A questo punto, il nostro repository contiene due branch:
master
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_1Il branch master è quello principale, mentre hypothesis è secondario (derivato). I commit più recenti sono elencati sopra quelli precedenti, come nell’output del comando git log.
Unione dei branch Copia link
Supponiamo di voler continuare a lavorare sulla funzionalità che avevamo spostato nel branch hypothesis. Tuttavia, questo branch non contiene la correzione critica che abbiamo effettuato in master.
Pertanto, vogliamo “sincronizzare” lo stato di hypothesis con master in modo che il commit di correzione appaia anche nel branch delle funzionalità. In altre parole, vogliamo che la struttura del repository appaia così:
master
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_5
commit_1Come puoi vedere, ora il branch hypothesis ripete esattamente la cronologia di master, anche se era stato creato prima del commit_5. In altre parole, hypothesis ora contiene la cronologia di entrambi i branch: la propria e quella di master.
Per ottenere questo risultato, dobbiamo usare git rebase.
Successivamente, le modifiche apportate in hypothesis possono essere unite a master utilizzando il classico comando git merge, che crea un commit di unione.
La struttura del repository sarà quindi la seguente:
master
commit_merge
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_5
commit_1Inoltre, eseguire git merge dopo git rebase può ridurre la probabilità di conflitti.
Pratica: git rebase Copia link
Una volta coperto l’aspetto teorico del comando git rebase, possiamo passare alla pratica testandolo in un repository reale di un progetto dimostrativo. La struttura del repository sarà la stessa dell’esempio teorico precedente.
Creazione di un repository Copia link
Per prima cosa, creiamo una directory separata per contenere il repository:
mkdir rebasePoi entriamoci:
cd rebaseOra possiamo inizializzare il repository:
git initNella console apparirà un messaggio informativo standard:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
...E nella directory corrente apparirà una cartella nascosta .git, che puoi visualizzare con il seguente comando:
ls -aIl flag -a significa all e consente di visualizzare il file system in modalità estesa. Il suo contenuto sarà:
. .. .gitPrima di eseguire i commit, dobbiamo specificare alcune informazioni di base sull’utente.
Prima, il nome:
git config --global user.name "NAME"Poi l’email:
git config --global user.email "NAME@HOST.COM"Popolamento del branch master Copia link
Utilizzando semplici file di testo, simuleremo l’aggiunta di diverse funzioni al progetto. Ogni nuova funzione sarà rappresentata come un commit separato.
Crea un file per la prima funzione improvvisata:
nano function_1Inserisci il seguente contenuto:
Function 1Ora indicizza le modifiche effettuate nel repository:
git add .Per sicurezza, controlla lo stato di indicizzazione:
git statusNella console dovrebbe apparire il seguente messaggio, che mostra le modifiche indicizzate:
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: function_1
Ora possiamo eseguire il commit:
git commit -m "commit_1"La console dovrebbe mostrare un messaggio che conferma il commit riuscito in master:
[master (root-commit) 4eb7cc3] commit_1
1 file changed, 1 insertion(+)
create mode 100644 function_1Popolamento del branch hypothesis Copia link
Ora creiamo un nuovo branch chiamato hypothesis:
git checkout -b hypothesisIl flag -b serve per passare immediatamente al nuovo branch.
La console mostrerà un messaggio di conferma:
Switched to a new branch 'hypothesis'Successivamente, dobbiamo eseguire tre commit in sequenza con tre file, in modo analogo a master:
-
commit_2with filefunction_2and content: Function 2 -
commit_3with filefunction_3and content: Function 3 -
commit_4with filefunction_4and content: Function 4 commit_2con il filefunction_2e contenuto: Function 2commit_3con il filefunction_3e contenuto: Function 3commit_4con il filefunction_4e contenuto: Function 4
Se poi controlliamo l’elenco dei commit:
git log --onelineLa console mostrerà la seguente sequenza:
d3efb82 (HEAD -> hypothesis) commit_4
c9f57b7 commit_3
c977f16 commit_2
4eb7cc3 (master) commit_1Qui, il flag --oneline viene utilizzato per visualizzare le informazioni sui commit in formato compatto.
Aggiunta di un commit al branch master Copia link
L’ultimo passaggio è aggiungere un altro commit al branch principale master. Passiamo a esso:
git checkout masterLa console confermerà il cambio:
Switched to branch 'master'Ora creiamo un altro file di funzione:
nano function_5Con il seguente contenuto:
Function 5Successivamente, indiciamo le modifiche:
git add .E creiamo il nuovo commit:
git commit -m "commit_5"Se controlliamo l’elenco dei commit:
git log --onelineIl branch master avrà ora due commit:
3df7a00 (HEAD -> master) commit_5
4eb7cc3 commit_1Unione dei branch con git rebase Copia link
Per eseguire il rebase, dobbiamo prima passare al branch hypothesis:
git checkout hypothesisE poi eseguire il rebase:
git rebase masterDopo questo, la console mostrerà un messaggio che conferma il successo dell’operazione:
Successfully rebased and updated refs/heads/hypothesis.Ora possiamo verificare l’elenco dei commit:
git log --onelineLa console mostrerà un elenco contenente i commit di entrambi i branch nell’ordine originale:
8ecfd58 (HEAD -> hypothesis) commit_4
f715aba commit_3
ee47470 commit_2
3df7a00 (master) commit_5
4eb7cc3 commit_1Ora il branch hypothesis contiene la cronologia completa dell’intero repository.
Risoluzione dei conflitti Copia link
Come con git merge, anche quando si utilizza il comando git rebase possono verificarsi conflitti che richiedono una risoluzione manuale.
Modifichiamo il repository in modo da creare artificialmente un conflitto di rebase.
Creiamo un altro file nel branch hypothesis:
nano conflictE scriviamo il seguente testo al suo interno:
There must be a conflict here!Indichiamo le modifiche:
git add .E creiamo un commit:
git commit -m "conflict_1"Ora passiamo al branch master:
git checkout masterCreiamo un file simile:
nano conflictE aggiungiamo il seguente contenuto:
There must NOT be a conflict here!Indichiamo nuovamente le modifiche:
git add .E creiamo un commit:
git commit -m "conflict_2"Riapriamo il file creato:
nano conflictE modifichiamone il contenuto con:
There definitely must NOT be a conflict here!Indichiamo di nuovo le modifiche:
git add .E creiamo un altro commit:
git commit -m "conflict_3"Ora torniamo al branch hypothesis:
git checkout hypothesisE eseguiamo nuovamente il rebase:
git rebase masterLa console mostrerà un messaggio di conflitto:
Auto-merging conflict
CONFLICT (add/add): Merge conflict in conflict
error: could not apply 6003ed7... conflict_1
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 6003ed7... conflict_1
Git suggerisce di modificare il file di conflitto, indicizzare le modifiche con git add e poi continuare il rebase utilizzando il flag --continue.
Questo è esattamente ciò che faremo:
nano conflictIl file conterrà due versioni in conflitto racchiuse tra marcatori speciali:
<<<<<<< HEAD
There definitely must NOT be a conflict here!
=======
There must be a conflict here!
>>>>>>> 6003ed7 (conflict_1)Il nostro compito è rimuovere le parti non necessarie e riempire il file con una versione finale del testo:
There must absolutely definitely unanimously NOT be any conflict here!Ora indichiamo le modifiche:
git add .E continuiamo il rebase:
git rebase --continueDopo di ciò, la console aprirà un editor di testo suggerendo di modificare il messaggio di commit originale del commit in cui si è verificato il conflitto:
conflict_1
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto bd7aefc
# Last commands done (4 commands done):
# pick 8ecfd58 commit_4
# pick 6003ed7 conflict_1
# No commands remaining.
# You are currently rebasing branch 'hypothesis' on 'bd7aefc'.
#
# Changes to be committed:
# modified: conflict
#
La console mostrerà quindi un messaggio che conferma il completamento riuscito del processo di rebase:
[detached HEAD 482db49] conflict_1
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/hypothesis.Ora, se controlli l’elenco dei commit nel branch hypothesis:
git log --onelineVedrai la sequenza originale di tutte le modifiche effettuate:
482db49 (HEAD -> hypothesis) conflict_1
bd5d036 commit_4
407e245 commit_3
948b41c commit_2
bd7aefc (master) conflict_3
d98648d conflict_2
3df7a00 commit_5
4eb7cc3 commit_1Nota che i commit conflict_2 e conflict_3, creati nel branch master, appaiono nella cronologia prima del commit conflict_1. Tuttavia, questo vale per qualsiasi commit creato nel branch master.
Rebase di un repository remoto Copia link
Oltre a lavorare con branch locali, è possibile eseguire un rebase anche quando si scaricano modifiche da un repository remoto. Per farlo, è necessario aggiungere il flag --rebase al comando pull standard:
git pull --rebase remote branchDove:
-
remoteè il repository remoto -
branchè il branch remoto
Fondamentalmente, questa configurazione di pull è equivalente al comando git rebase, con la differenza che le modifiche (commit) applicate al branch corrente provengono dal repository remoto.
Principali vantaggi di Git Rebase Copia link
- Linearità
Il comando git rebase consente di creare una cronologia lineare del branch di destinazione, composta da commit eseguiti in sequenza. Una sequenza senza ramificazioni rende la cronologia più facile da comprendere e analizzare.
- Meno conflitti
Eseguire git rebase in anticipo può ridurre significativamente la probabilità di conflitti durante l’unione dei branch con git merge. I conflitti sono più facili da risolvere nei commit sequenziali rispetto a quelli uniti in un unico commit di merge. Questo è particolarmente utile quando si inviano i branch a repository remoti.
Svantaggi di Git Rebase Copia link
- Modifica della cronologia
A differenza di merge, rebase riscrive parzialmente la cronologia del branch di destinazione, rimuovendo elementi di cronologia non necessari.
- Rischio di errori
La possibilità di ristrutturare significativamente la cronologia dei commit può portare a errori irreversibili nel repository. Ciò significa che alcuni dati potrebbero andare definitivamente persi.
Quando utilizzare Git Rebase Copia link
Git rebase è particolarmente utile quando si lavora su piccoli branch o branch di funzionalità individuali che verranno successivamente uniti in un branch principale condiviso. È anche ottimo per mantenere una cronologia pulita e lineare, il che è particolarmente vantaggioso nei progetti open source o quando è necessario mantenere una cronologia dei commit chiara per facilitare il debug e la comprensione del progetto.
Tuttavia, negli ambienti di team con più collaboratori, rebase dovrebbe essere utilizzato con cautela per evitare problemi legati alla riscrittura della cronologia pubblica. È importante comunicare con il team su quando è appropriato utilizzare rebase e assicurarsi che tutti siano consapevoli del potenziale rischio di conflitti. In molti casi, l’uso di git merge per integrare i branch può essere più sicuro e semplice, soprattutto quando si lavora su branch condivisi o quando una cronologia non lineare è accettabile.
Considerazioni importanti Copia link
- Riscrittura della cronologia
Uno degli aspetti fondamentali di git rebase è che riscrive la cronologia dei commit, il che significa che gli hash SHA-1 dei commit rebasati cambiano. Ciò può causare problemi negli ambienti collaborativi, specialmente quando si esegue un rebase su un branch che è già stato inviato a un repository remoto condiviso. La riscrittura della cronologia può portare a conflitti con altri sviluppatori che hanno basato il proprio lavoro sulla cronologia precedente. Può anche causare problemi durante il push del branch rebasato, poiché il repository remoto riconoscerà che la cronologia locale non corrisponde più a quella remota.
- Evitare di rebasare branch pubblici
Una pratica comune consigliata è quella di evitare di rebasare branch pubblici che sono già stati condivisi con altri. Poiché rebase modifica la cronologia dei commit, rebasare branch pubblici su cui si basano più sviluppatori può generare cronologie divergenti, causando confusione e conflitti di merge difficili da risolvere. In generale, git rebase è più appropriato per i branch locali o per preparare un branch di funzionalità alla fusione finale in un branch principale. I branch pubblici, soprattutto quelli su cui lavorano più persone, dovrebbero di norma essere uniti con merge invece che rebasati.
- Conflitti potenziali
Durante un rebase, se ci sono modifiche in conflitto sia nel branch delle funzionalità che in quello di destinazione, Git si fermerà e chiederà di risolvere il conflitto manualmente. Sebbene la risoluzione dei conflitti durante un rebase sia simile a quella di un merge, il rebase richiede di risolvere i conflitti per ogni commit che deve essere riapplicato, rendendo il processo potenzialmente più laborioso, soprattutto con branch di grandi dimensioni. Una volta risolti i conflitti, è possibile continuare il rebase con il comando git rebase --continue.
Conclusione Copia link
L’unione di due branch utilizzando il rebase, implementata con il comando git rebase, è fondamentalmente diversa dall’unione classica eseguita con il comando git merge.
-
git mergetrasforma i commit di un branch in un unico commit in un altro. -
git rebasesposta i commit di un branch alla fine di un altro mantenendo l’ordine originale delle modifiche.
Un effetto di rebase simile può essere ottenuto anche utilizzando il comando git pull con il flag aggiuntivo --rebase.
Da un lato, il comando git rebase consente di ottenere una cronologia dei commit più pulita e comprensibile nel branch principale, aumentando la manutenibilità del repository.
Dall’altro, git rebase riduce il livello di dettaglio delle modifiche all’interno del branch, semplificando la cronologia e rimuovendo alcune delle sue registrazioni.
Per questo motivo, il rebase è una funzionalità destinata agli utenti più esperti che comprendono a fondo come funziona Git.
Nella maggior parte dei casi, il comando git rebase viene utilizzato insieme al comando git merge, consentendo di ottenere una struttura del repository e dei branch quanto più ottimale possibile.