Abbiamo visto come usare il comando git merge per fondere due branch. Git mette a disposizione un altro comando che si comporta in maniera simile, ovvero il comando git rebase che consente di integrare i commit di un branch nell’altro. Le funzionalità offerte da git rebase differiscono da git merge in alcuni aspetti chiave.
L’operazione di rebasing (rifondazione) consiste nello spostare una serie di commit in modo tale che il primo di questi sia preceduto da un diverso commit. Detto in altro modo, eseguendo il comando git rebase su un branch, quest’ultimo avrà una nuova base di partenza. A voler essere più precisi, i commit non vengono in realtà spostati. Se ricordate quanto avevamo già detto in una delle precedenti lezioni, ogni commit è un oggetto unico e immutabile identificato in maniera univoca da un valore di hash generato usando l’algoritmo SHA-1. Per questo motivo non è possibile modificare il genitore di un commit (ricordiamo che in ogni oggetto commit è salvato un riferimento al commit che lo precede). Vengono quindi creati dei nuovi commit sulla base delle informazioni contenute nei vecchi oggetti.
Come usare il comando git rebase
Vediamo come usare il comando git rebase attraverso un esempio. Realizziamo un nuovo esempio simile a quello visto nella lazeione precedente, ma lo modifichiamo in modo da avere due file elenco_componenti.txt, creato sul branch master, e preventivo_pc_primo_negozio.txt, creato sul branch primo_negozio. Riportiamo l’output del comando git log per illustrare la situazione corrente del repository.
# l'opzione --all permette di mostrare tutti i branch
[ test_git_rebase ] (primo_negozio) $ git log --oneline --graph --all --decorate
* 8b976d0 (master) aggiunge un hard disk a elenco_componenti.txt
| * 85d07b0 (HEAD -> primo_negozio) aggiorna preventivo_pc_primo_negozio con un nuovo prezzo del processore (120->110)
| * ee52311 aggiunge il file preventivo_pc_primo_negozio contenente il preventivo iniziale
|/
* 1140a74 inserisce in elenco_componenti.txt una prima possibile configurazione
* af44cd8 first commit
Nell’esempio mostrato, il branch corrente è primo_negozio. Vogliamo prendere i commit del branch primo_negozio, generare delle copie e posizionarle in modo che il nuovo commit, creato sulla base del contenuto del commit ee523, abbia come genitore 8b976, ovvero il commit attualmente referenziato da master.
Per far ciò, basterà eseguire il seguente comando:
[ test_git_rebase ] (primo_negozio) $ git rebase master
First, rewinding head to replay your work on top of it...
Applying: aggiunge il file preventivo_pc_primo_negozio contenente il preventivo iniziale
Applying: aggiorna preventivo_pc_primo_negozio con un nuovo prezzo del processore (120->110)
[ test_git_rebase ] (primo_negozio) $ git log --oneline --graph --all --decorate
* 74859a7 (HEAD -> primo_negozio) aggiorna preventivo_pc_primo_negozio con un nuovo prezzo del processore (120->110)
* a6a720c aggiunge il file preventivo_pc_primo_negozio contenente il preventivo iniziale
* 8b976d0 (master) aggiunge un hard disk a elenco_componenti.txt
* 1140a74 inserisce in elenco_componenti.txt una prima possibile configurazione
* af44cd8 first commit
Come possiamo notare dall’output del comando git log, sono stati creati due nuovi commit che hanno lo stesso messaggio e contenuto degli originali.
Differenza fra git rebase e git merge
La differenza principale fra il comando git rebase e git merge consiste nel fatto che il primo modifica la storia del repository creando dei commit che sono copie di altri commit e che non esistevano prima della sua esecuzione. Inoltre, la struttura del repository cambia conformazione. Ciò può condurre a conclusioni errate. Facendo infatti riferimento all’immagine riportata sopra, se una persona visualizzasse l’elenco dei commit del repository dopo che è stato eseguito il rebasing, potrebbe pensare che i commit a6a72 e 74859 sono più recenti del commit referenziato dal branch master. In realtà sono stati creati in parallelo e solo in seguito sono stati "spostati" dal rebasing. Proprio perché viene alterata la struttura del repository, è fortemente sconsigliato usare il rebasing su branch che sono condivisi con altri collaboratori onde evitare incomprensioni, perdite o duplicazioni di informazioni. Il comando git rebase rende tuttavia la struttura del repository più lineare e più semplice da leggere e visualizzare risultando così uno strumento utile e vantaggioso nel caso di branch non condivisi.
Il comando git merge, invece, mantiene inalterata la storia precedente del repository, ma può comportare una difficile visualizzazione del repository stesso, specie nei casi in cui si abbiano numerosi branch e vengano effettuate diverse operazioni di merging.
Il rebase ‘interattivo’
Concludiamo parlando del comando git rebase in modalità interattiva, ovvero attraverso l’opzione ‘-i’ o ‘–interactive’. In questo caso Git mostrerà un’interfaccia testuale che consente di selezionare ed eseguire operazioni diverse per ogni commit. Si tratta sempre di azioni che causano una modifica e riscrittura della storia del repository per cui vanno eseguite prestando attenzione e seguendo gli stessi principi illustrati nel caso del rebasing "standard".
Vediamo un esempio in cui usiamo il comando git rebase -i. La situazione di partenza è la seguente:
[ test_git_rebase] (master) $ git log --oneline --graph --all --decorate
* c52ee2c (HEAD -> master) Aggiunge il mouse all'elenco dei componenti
* 5bed09b Aggiunge la tastiera all'elenco dei componenti
* 8b976d0 aggiunge un hard disk a elenco_componenti.txt
| * 85d07b0 (primo_negozio) aggiorna preventivo_pc_primo_negozio.txt con un nuovo prezzo del processore (120->110)
| * ee52311 aggiunge il file preventivo_pc_primo_negozio.txt contenente il preventivo iniziale
|/
* 1140a74 inserisce in elenco_componenti.txt una prima possibile configurazione
* af44cd8 first commit
Eseguiamo quindi il comando git rebase -i passando come argomento il valore identificativo del commit a partire dal quale vogliamo eseguire le modifiche del repository. In questo caso useremo il commit che precede la divergenza dei due rami.
[ test_git_rebase] (master) $ git rebase -i 1140a74
Dopo aver premuto il tasto INVIO, verrà mostrata una schermata in cui possiamo decidere quali modifiche apportare ai commit che seguono 1140a74 (1140a74 è escluso) sul branch master.
Nella schermata in alto, viene mostrato un elenco. Per ciascun commit dovremo indicare l’azione che vogliamo venga eseguita da Git. Come viene evidenziato nei commenti, se eliminiamo una riga, il commit corrispondente viene eliminato per sempre. Possiamo anche riordinare i commit se crediamo sia opportuno. Il file su cui stiamo lavorando contiene quindi delle istruzioni che vengono eseguite da Git una alla volta a partire dalla prima. Nel nostro caso chiediamo di unire i due commit 5bed09d e c52ee2c in un unico nuovo commit. Notate che stiamo editando il file com VIM e, dopo aver terminato le modifiche, salviamo e chiudiamo il file con : wq (in basso a sinistra nella foto). Una volta chiuso il file, Git inizia ad effettuare le modifiche e ci verrà chiesto di intervenire solo se necessario. Infatti per il primo commit non è richiesta nessuna azione da parte nostra. Per i due commit successivi, Git aprirà nuovamente l’editor predefinito perché dobbiamo selezionare il messaggio da inserire nel nuovo commit.
Rimuoviamo il secondo messaggio e modifichiamo opportunamente il primo.
Dopo aver salvato verrà mostrato un messaggio nella shell come il seguente.
[ test_git_rebase] (master) $ git rebase -i 1140a
[detached HEAD 166d45b] Aggiunge tastiera e mouse all'elenco dei componenti
1 file changed, 2 insertions(+)
Successfully rebased and updated refs/heads/master.
A questo punto spostiamoci sul branch primo_negozio e modifichiamo il contenuto dei messaggi di entrambi i commit aggiungendo l’estensione .txt mancante a ‘preventivo_pc_primo_negozio’.
[ test_git_rebase] (primo_negozio) $ git rebase -i 1140a74
Dopo aver salvato, Git chiederà per due volte di intervenire per modificare i messaggi di entrambi i commit.
Dopo aver salvato il primo, ci verrà chiesto di modificare anche il secondo messaggio.
Alla fine verrà nuovamente mostrato un messaggio nella shell a conferma del completamento dell’operazione di rebasing.
[ test_git_rebase] (primo_negozio) $ git rebase -i 1140a
[detached HEAD 0d3307e] aggiunge il file preventivo_pc_primo_negozio.txt contenente il preventivo iniziale
1 file changed, 9 insertions(+)
create mode 100644 preventivo_pc_primo_negozio.txt
[detached HEAD a428fe2] aggiorna preventivo_pc_primo_negozio.txt con un nuovo prezzo del processore (120->110)
1 file changed, 2 insertions(+), 2 deletions(-)
Successfully rebased and updated refs/heads/primo_negozio.
Dopo aver apportato le modifiche appena illustrate, possiamo lanciare il comando git log per visualizzare la nuova fisionomia del repository.
[ test_git_rebase] (primo_negozio) $ git log --oneline --graph --all --decorate
* a428fe2 (HEAD -> primo_negozio) aggiorna preventivo_pc_primo_negozio.txt con un nuovo prezzo del processore (120->110)
* 0d3307e aggiunge il file preventivo_pc_primo_negozio.txt contenente il preventivo iniziale
| * 166d45b (master) Aggiunge tastiera e mouse all'elenco dei componenti
| * 8b976d0 aggiunge un hard disk a elenco_componenti.txt
|/
* 1140a74 inserisce in elenco_componenti.txt una prima possibile configurazione
* af44cd8 first commit
Come previsto, è stato creato un nuovo commit sul branch master che include le modifiche dei due vecchi commit che abbiamo unito (squash). Anche sul branch primo_negozio sono stati creati due nuovi commit visto che abbiamo corretto i messaggi dei vecchi commit.
Notate che in questo caso, per come abbiamo strutturato l’esempio, non ci sono stati particalari problemi, ma potrebbe capitare di dover risolvere dei conflitti così come abbiamo visto nel caso del comando git merge.
Conclusioni
In questa lezione abbiamo quindi visto come usare il comando git rebase nella modalità ‘standard’ e ‘interattiva’ per apportare cambiamenti al repository e riordinare i commit, cancellarli o unirli. Nella prossima lezione vedremo come utilizzare i comandi git reset e git revert.