Articolo originale: The Git Rebase Handbook – A Definitive Guide to Rebasing
Uno degli strumenti più potenti che uno sviluppatore può avere nella propria cassetta degli attrezzi è git rebase
. Eppure è noto per essere complesso e frainteso.
La verità è che se capisci cosa fa realmente, git rebase
è uno strumento molto elegante e diretto per ottenere tante cose diverse in Git.
Nei post precedenti, hai capito come funziona git diff, cosa sia un'azione di merge, come git risolve i conflitti di merge. In questo post, capirai cos'è il comando rebase di Git, perché è diverso da merge e come utilizzare git rebase
con sicurezza 💪🏻
Prima di iniziare
- Ho creato anche un video che tratta il contenuto di questo post. Se desideri guardarlo mentre stai leggendo, puoi trovarlo qui.
- Se vuoi fare esperimenti con il repository che ho usato e provare da solo i comandi, puoi trovarlo qui.
- Sto lavorando a un libro su Git! Ti interessa leggere la versione iniziale e darmi un feedback? Mandami una email: gitting.things@gmail.com
OK, sei pronto?
Breve riepilogo - Cos'e Git Merge? 🤔
Dietro le quinte, git rebase
e git merge
sono cose molto, molto diverse. Allora come mai la gente continua a confrontarli tutte le volte?
La ragione è il loro utilizzo. Quando si lavora con Git in genere si lavora su branch diversi, nei quali si introducono delle modifiche.
Nel tutorial precedente, ho fornito un esempio dove John e Paul (dei Beatles) scrivevano a 4 mani una nuova canzone. Avevano iniziato entrambi dal branch main
poi ognuno di loro ha preso una strada diversa, modificando le parole, quindi confermando le loro modifiche.
Poi entrambi volevano integrare i loro cambiamenti, il che è qualcosa che succede molto frequentemente quando lavori con Git.

paul_branch
e john_branch
divergono da main
(Fonte: Brief)Ci sono due modi principali per integrare modifiche introdotte in branch diversi in Git, oppure, in altre parole, diversi commit e diverse cronologie di commit. Questi sono merge e rebase.
In un tutorial precedente, abbiamo imparato a conoscere git merge
molto bene. Abbiamo visto come eseguire un merge, abbiamo creato il merge di un commit – dove il contenuto di questo commit è una combinazione di due branch, e aveva anche due genitori, uno in ciascun branch.
Quindi, diciamo che sei sul branch john_branch
(ipotizzando la cronologia descritta nell'immagine qui sopra) ed esegui git merge paul_branch
. Arriverai a questo stato – dove su john_branch
, c'è un nuovo commit con due genitori. Il primo sarà il commit su john_branch
dove HEAD
stava puntando prima di eseguire il merge, in questo caso - "Commit 6". Il secondo sarà il commit puntato da paul_branch
, "Commit 9".

git merge paul_branch
: Un nuovo merge commit con due genitori (Fonte: Brief)Osserva nuovamente il grafico della cronologia: hai creato una cronologia divergente. Puoi in effetti vedere dove sono stati ramificati e integrati nuovamente.
Quindi quando usi git merge
, non riscrivi la cronologia – piuttosto aggiungi un commit alla cronologia esistente. E, nello specifico, un commit che crea cronologie divergenti.
In che modo git rebase
è diverso da git merge
? 🤔
Quando si usa git rebase
, succede qualcosa di diverso. 🥁
Partiamo dal quadro generale: se sei su paul_branch
, ed esegui git rebase john_branch
, Git va all'antenato comune per i branch di John e Paul. Poi prende le modifiche introdotte nei commit del branch di Paul e applica dette modifiche al branch di John.
In questo caso usi rebase
per prendere le modifiche effettuate e confermate (con commit) in un branch – quello di Paul (paul_branch
) – e le replichi in un branch diverso – quello di John (john_branch
).

git rebase john_branch
: i commit in paul_branch
sono stati "replicati" in john_branch
(Fonte: Brief)Aspetta, cosa significa? 🤔
Ora analizziamo questo concetto un poco per volta, in modo che tu possa capire pienamente cosa sta succedendo sotto il cofano 😎
cherry-pick
come base per rebase
È utile pensare a rebase come all'esecuzione di git cherry-pick
– un comando che prende un commit, calcola le differenze che questo introduce confrontando il commit del genitore con il commit stesso, e le replica nel branch corrente.
Facciamolo a mano.
Se diamo un'occhiata alle differenze introdotte da "Commit 5" eseguendo git diff main <SHA_DI_COMMIT_5>
:

git diff
per osservare le modifiche introdotte da "Commit 5" (Fonte: Brief)Se vuoi fare esperimenti con il repository che ho usato e provare da solo i comandi, puoi trovare il repository qui.
Puoi notare in questo commit, che John ha iniziato a lavorare a una canzone chiamata "Lucy in the Sky with Diamonds":

Ti ricordo che puoi usare anche il comando git show
per ottenere lo stesso risultato:
git show <SHA_DI_COMMIT_5>
Ora se esegui il cherry-pick
di questo commit, introdurrai questa specifica modifica, sul branch attivo. Prima portati su main
:
git checkout main
(oppure git switch main
)
Poi crea un altro branch, giusto per essere più chiari:
git checkout -b my_branch
(oppure git switch -c my_branch
)

my_branch
che si dirama da main
(Fonte: Brief)Poi esegui il cherry-pick
di questo commit:
git cherry-pick <SHA_DI_COMMIT_5>

cherry-pick
per applicare le modifiche introdotte da "Commit 5" in main
(Fonte: Brief)Esamina questo log (risultato di git lol
):

git lol
(Fonte: Brief)(git lol
è un alias che ho aggiunto a Git per vedere chiaramente in modo grafico la cronologia. Puoi trovare il comando che sostituisce qui).
Sembra che tu abbia fatto un copia-incolla di "Commit 5". Ricorda che sebbene abbia lo stesso messaggio di commit e introduca le stesse modifiche, e punti anche allo stesso albero di oggetti del "Commit 5" originale, in questo caso è comunque un oggetto commit diverso, visto che è stato creato con una diversa marca temporale.
Se osserviamo le modifiche usando git show HEAD
:

Sono le stesse di "Commit 5"'.
Naturalmente, se osservi il contenuto del file con un editor (diciamo usando il comando nano lucy_in_the_sky_with_diamonds.md
), sarà nello stesso stato nel quale si trovava dopo il "Commit 5" originale.
Forte! 😎
OK, ora puoi eliminare il nuovo branch così che non appaia nella tua cronologia tutte le volte:
git checkout main
git branch -D my_branch
Andare oltre cherry-pick
– Come usare git rebase
Puoi considerare git rebase
come un modo per eseguire più cherry-pick
uno dopo l'altro – vale a dire "replicare" diversi commit. Questa non è la sola cosa che puoi fare con rebase
, ma è un buon punto di partenza per la nostra spiegazione.
È ora di giocare con git rebase
! 👏🏻👏🏻
In precedenza, hai integrato paul_branch
in john_branch
. Cosa sarebbe successo se avessi eseguito il rebase di paul_branch
su john_branch
? Avresti ottenuto una cronlogia molto diversa.
In sostanza, sarebbe come se avessimo preso le modifiche introdotte nei commit su paul_branch
e le avessimo replicate su john_branch
. Il risultato sarebbe stato una cronologia lineare.
Per capire il processo, ti fornirò una visione ad alto livello, poi approfondiremo ogni passaggio. Il processo di rebase di un branch su un altro branch è il seguente:
- Trova l'antenato comune.
- Identifica i commit da "replicare".
- Per ogni commit
X
, calcoladiff(genitore(X), X)
, e conserva il risultato comepatch(X)
. - Sposta
HEAD
verso la nuova base. - Applica le patch generate in ordine sul branch di destinazione. Ogni volta, crea un nuovo oggetto commit con il nuovo stato.
Il processo di creare nuovi commit con lo stesso insieme di modifiche di commit esistenti è detto "replica" di questi commit, un termine che abbiamo già usato.
È ora di provare rebase🙌🏻
Partiamo dal branch di Paul:
git checkout paul_branch
Questa è la cronologia:

git rebase
(Fonte: Brief)Ora veniamo alla parte eccitante:
git rebase john_branch
Osserva la cronologia:

gg
è un alias per uno strumento esterno che ho introdotto nel video (è il programma git-graph che puoi trovare in questo repository - n.d.t.).
Pertanto mentre con git merge
hai aggiunto alla cronologia, con git rebase
hai riscritto la cronologia. Hai creato oggetti commit nuovi. Inoltre il risultato è un grafico di cronologia lineare, non divergente.

In sostanza abbiamo "copiato" i commit che si trovavano in paul_branch
, introdotti dopo "Commit 4", e "incollati" in john_branch
.
Il comando è chiamato "rebase", in quanto modifica la base di commit del branch dal quale viene eseguito. Nel nostro caso, prima di eseguire git rebase
, la base di paul_branch
era "Commit 4" – visto che da lì è "nato" il branch (derivato da main
). Con rebase
, hai chiesto a Git di darti un'altra base, vale a dire: fai finta che paul_branch
sia "nato" da "Commit 6".
Per fare questo Git ha preso quello che era il "Commit 7", e ha "replicato" le modifiche introdotte in questo commit in "Commit 6", poi ha creato un nuovo oggetto commit. Questo oggetto differisce dal "Commit 7" originale in tre aspetti:
- Ha una marca temporale diversa.
- Ha un commit genitore diverso – "Commit 6" invece che "Commit 4".
- L'albero di oggetti a cui punta è diverso - visto che le modifiche sono state introdotte all'albero di oggetti puntato da "Commit 6", non a quello puntato da "Commit 4".
Nota l'ultimo commit qui, "Commit 9'". L'istantanea che rappresenta (cioè l'albero a cui punta) è esattamente la stessa che avresti se avessi integrato i due branch. Lo stato dei file nel tuo repository Git sarebbe stato uguale se avessi usato git merge
. Solo che la cronologia è diversa, e l'oggetto commit naturalmente.
Ora puoi semplicemente usare:
git checkout main
git merge paul_branch
Hmm... Cosa succederebbe se eseguissi questo ultimo comando? 🤔 Esamina nuovamente la cronologia dei commit, dopo esserti portato in main
:

Cosa comporta integrare main
e paul_branch
?
In effetti, Git può semplicemente eseguire un merge fast-forward, visto che la cronologia è perfettamente lineare (se ti serve una rinfrescata su cosa sia un merge fast-forward, dai un'occhiata a questo post). Come risultato, main
e paul_branch
ora puntano allo stesso commit:

Rebase avanzato in Git💪🏻
Ora che conosci le basi di rebase, è ora di prendere in considerazione casi più avanzati, laddove torneranno utili opzioni addizionali e argomenti per il comando rebase
.
Nell'esempio precedente, quando hai usato rebase
senza opzioni aggiuntive, Git ha replicato tutti i commit a partire dall'antenato comune fino all'inizio del branch corrente.
Ma rebase è potentissimo, è un comando possente in grado di riscrivere la cronologia e può tornare utile se vuoi modificare la cronologia e generarne una tua propria.
Annulla l'ultimo merge facendo puntare nuovamente main
a "Commit 4":
git reset -–hard <COMMIT 4_ORIGINALE>

Annulla anche il rebase in questo modo:
git checkout paul_branch
git reset -–hard <COMMIT 9_ORIGINALE>

Nota che ora la cronologia è esattamente quella che avevi in precedenza:

Giusto per essere chiari, "Commit 9" non sparisce semplicemente quando non è raggiungibile dall'HEAD
corrente. Viceversa è ancora conservato nel database degli oggetti. Visto che hai usato git reset
per fare in modo che HEAD
punti a questo commit, sei in grado di recuperarlo, assieme ai suoi commit genitori visto che anch'essi sono conservati nel database. Non male, non è vero? 😎
Adesso diamo un veloce sguardo alle modifiche introdotte da Paul:
git show HEAD

git show HEAD
mostra le modifiche introdotte da "Commit 9" (Fonte: Brief)Proseguiamo a ritroso nel grafico dei commit:
git show HEAD~

git show HEAD~
(uguale a git show HEAD~1
) mostra le modifiche introdotte da "Commit 8" (Fonte: Brief)Andiamo indietro di un altro commit:
git show HEAD~2

git show HEAD~2
mostra le modifiche introdotte da "Commit 7" (Fonte: Brief)Quindi, queste modifiche sono buone, ma forse Paul non vuole questo tipo di cronologia. Piuttosto vuole che sembri che le modifiche introdotte in "Commit 7" e "Commit 8" appaiano come un singolo commit.
Per fare questo, puoi usare un rebase interattivo, aggiungendo l'opzione -i
(oppure --interactive
) al comando rebase
:
git rebase -i <SHA_DI_COMMIT_4>
Oppure, visto che main
sta puntando a "Commit 4", possiamo semplicemente eseguire:
git rebase -i main
Eseguendo questo comando, dici a Git di usare una nuova base, "Commit 4". Pertanto stai chiedendo a Git di tornare a tutti i commit che sono stati introdotti dopo "Commit 4", che sono raggiungibili dall' HEAD
corrente, e di replicarli.
Per ogni commit che viene replicato, Git ci chiede come vogliamo agire:

git rebase -i main
ti chiede di selezionare cosa deve essere fatto con ciascun commit (Fonte: Brief)In questo contesto, è utile pensare a un commit come a una modifica. Vale a dire, "Commit 7" come fosse "la modifica che 'Commit 7' ha introdotto sopra il suo genitore".
Una opzione è usare pick
. Questo è il comportamento predefinito, che dice a Git di replicare le modifiche introdotte in questo commit. In questo caso, non devi fare nulla e scegliere (pick
) per tutti i commit – otterrai la stessa cronologia e Git non dovrà neppure creare dei nuovi oggetti commit.
Un'altra opzione è squash
. Un commit si definisce squashed (accorpato) quando ha tutto il suo contenuto inserito nel commit precedente. Nel nostro caso Paul vorrebbe accorpare "Commit 8" in "Commit 7":

Come puoi vedere, git rebase -i
fornisce opzioni aggiuntive, ma non le esamineremo tutte in questo post. Se provi ad eseguire il rebase, ti verrà richiesto di indicare un messaggio per il nuovo commit che sarà creato (vale a dire quello che introdurrà le modifiche sia di "Commit 7" che di "Commit 8"):

Commits 7+8
(Fonte: Brief)Dai un'occhiata alla cronologia:

Esattamente quello che volevamo! Abbiamo su paul_branch
"Commit 9" (ovviamente un oggetto diverso rispetto al "Commit 9" originale). Questo punta a "Commits 7+8", che è un singolo commit che contiene le modifiche dei "Commit 7" e "Commit 8" originali. Il genitore di questi commit è "Commit 4", a cui sta puntando main
, che è il genitore di john_branch
.

Wow, forte, non è vero? 😎
git rebase
ti consente controllo illimitato sulla forma di qualsiasi branch. Puoi usarlo per riordinare i commit, oppure per rimuovere modifiche non corrette, o cambiare una modifica in retrospettiva. In alternativa, potresti spostare la base del tuo branch in un altro commit di tua scelta.
Come usare l'opzione di switch --onto
di git rebase
Consideriamo un altro esempio. Andiamo su main
nuovamente:
git checkout main
Eliminiamo i puntatori a paul_branch
e john_branch
in modo che non compaiono più nel grafico della cronologia:
git branch -D paul_branch
git branch -D john_branch
Ora generiamo un nuovo branch da main
e ci spostiamo su di esso:
git checkout -b new_branch

new_branch
che si dirama da main
(Fonte: Brief)
new_branch
che si dirama da main
(Fonte: Brief) Ora facciamo qualche modifica ed eseguiamo il commit:
nano code.py

new_branch
a code.py
(Fonte: Brief)git add code.py
git commit -m "Commit 10"
Torniamo su main
:
git checkout main
Poi introduciamo una modifica:

Adesso eseguiamo il commit queste modifiche:
git add code.py
git commit -m "Commit 11"
Poi effettuiamo un altro cambiamento:

@Author
alla docstring (Fonte: Brief)Eseguiamo il commit anche di questa modifica:
git add code.py
git commit -m "Commit 12"
Aspetta, ora mi sono reso conto che avrei voluto apportare le modifiche introdotte nel "Commit 11" in new_branch
. Che si può fare adesso? 🤔
Esaminiamo la cronologia:

Quello che voglio è che invece di avere "Commit 10" situato solo sul branch main
, sia anche in new_branch
. Visivamente, vorrei spostarlo in fondo al grafico mostrato qui sotto:

git push
) il "Commit 10" (Fonte: Brief)Riesci a vedere dove voglio arrivare? 😇
Bene, come sappiamo rebase ci consente in pratica di replicare le modifiche introdotte in new_branch
, quelle del "Commit 10", come se in origine fossero state apportate in "Commit 11" invece che in "Commit 4".
Per fare questo, puoi usare altri argomenti di git rebase
. Dovresti dire a Git che vuoi prendere tutta la cronologia introdotta tra l'antenato comune di main
e new_branch
, che sarebbe "Commit 4", e fare in modo che la nuova base per quella cronologia sia "Commit 11". Per farlo usa:
git rebase -–onto <SHA_DI_COMMIT_11> main new_branch

new_branch
(Fonte: Brief) Ora dai uno sguardo alla nostra meravigliosa cronologia! 😍

new_branch
(Fonte: Brief)Consideriamo un altro caso.
Diciamo che ho iniziato a lavorare su un branch e per errore ho iniziato a lavorare da feature_branch_1
, invece che da main
.
Per emulare questa situazione crea feature_branch_1
:
git checkout main
git checkout -b feature_branch_1
Poi elimina new_branch
così che non appaia più nel grafico della cronologia:
git branch -D new_branch
Crea un semplice file Python chiamato 1.py
:

1.py
, con print("Hello World!")
(Fonte: Brief)Aggiungi a Git questo file ed esegui il commit:
git add 1.py
git commit -m "Commit 13"
Ora esci (per errore) da feature_branch_1
ed entra in un nuovo branch (feature_branch_2
):
git checkout -b feature_branch_2
Poi crea un altro file, 2.py
:

2.py
(Fonte: Brief)Aggiungi anche questo file ed esegui il commit:
git add 2.py
git commit -m "Commit 14"
Poi inserisci dell'altro codice a 2.py
:

2.py
(Fonte: Brief)Fai il commit anche di queste modifiche:
git add 2.py
git commit -m "Commit 15"
Fino ad ora dovresti avere questa cronologia:

Ritorna su feature_branch_1
e modifica 1.py
:
git checkout feature_branch_1

1.py
(Fonte: Brief)Fai il commit del file modificato:
git add 1.py
git commit -m "Commit 16"
La tua cronologia dovrebbe essere questa:

Diciamo che ora ti rendi conto di avere fatto un errore. In realtà avresti voluto far nascere feature_branch_2
da main
, invece che da feature_branch_1
.
Come puoi rimediare? 🤔
Prova a pensarci tenendo conto del grafico della cronologia e di quello che hai imparato fino ad ora sull'opzione --onto
per il comando rebase
Bene, vuoi "sostituire" il genitore del tuo primo commit su feature_branch_2
, che è "Commit 14", in modo che sia alla sommità del branch main
, in questo caso "Commit 12", invece che all'inizio di feature_branch_1
, in questo caso "Commit 13". Pertanto, ancora una volta, andrai a creare una nuova base, questa volta per il primo commit su feature_branch_2
.

Come faresti?
Per prima cosa portati su feature_branch_2
:
git checkout feature_branch_2
Da qui puoi usare:
git rebase -–onto main <SHA_DI_COMMIT_13>
Come risultato avrai feature_branch_2
con base su main
invece che su feature_branch_1
:

La sintassi del comando è:
git rebase --onto <nuovo_genitore> <vecchio_genitore>
Come effettuare il rebase su un singolo branch
Puoi anche usare git rebase
in relazione alla cronologia di un singolo branch.
Vediamo se mi puoi aiutare.
Diciamo che ho lavorato da feature_branch_2
, e nello specifico ho modificato il file code.py
. Ho iniziato modificando gli apici singoli che racchiudono le stringhe in apici doppi:

'
in "
nel file code.py
(Fonte: Brief)Poi ho eseguito il commit delle modifiche:
git add code.py
git commit -m "Commit 17"
Quindi ho deciso di aggiungere una nuova funzione all'inizio del file:

another_feature
(Fonte: Brief)Ho nuovamente portato nell'area di stage code.py
ed eseguito il commit:
git add code.py
git commit -m "Commit 18"
Ora mi sono reso conto che mi sono dimenticato di cambiare gli apici che racchiudono l'istruzione __main__
da singoli a doppi (come potresti aver notato), pertanto ho fatto anche questo:

'__main__'
in "__main__"
(Fonte: Brief)Naturalmente ho eseguito il commit anche di questa modifica:
git add code.py
git commit -m "Commit 19"
Ora esamina la cronologia:

Non è molto bella, giusto? Voglio dire, ho due commit (in relazione tra loro), "Commit 17" e "Commit 19" (sostituzione di '
con "
), ma sono separati l'uno dall'altro da un "Commit 18" che non ha niente a che vedere con quelli (ho aggiunto una nuova funzione). Cosa posso fare? 🤔 Puoi aiutarmi?
Andando a intuito, voglio modificare la cronologia qui:

Quindi, cosa dovrei fare?
Hai ragione! 👏🏻
Posso eseguire il rebase della cronologia da "Commit 17" a "Commit 19", sopra il "Commit 15". Per fare questo:
git rebase --interactive --onto <SHA_DI_COMMIT_15> <SHA_DI_COMMIT_15>
Nota che ho specificato "Commit 15" come inizio del gruppo di commit, escludendo questo commit. Non ho avuto bisogno di specificare HEAD
come ultimo parametro.

rebase --onto
su un singolo branch (Fonte: Brief)Dopo aver seguito il tuo consiglio ed eseguito il comando rebase
(grazie! 😇) mi viene presentata la seguente schermata:

Quindi cosa dovrei fare? Voglio inserire "Commit 19" prima di "Commit 18", in modo che venga appena dopo "Commit 17". Posso proseguire e accorparli, in questo modo:

Ora quando mi viene richiesto di inserire un messaggio per il commit, posso indicare "Commit 17+19":

Ora guardiamo la nostra stupenda cronologia:

Grazie ancora! 🙌🏻
Altri casi d'uso per rebase + altri esercizi
A questo punto, spero tu sia a tuo agio con la sintassi di rebase. Il modo migliore per comprenderla veramente è considerare varie situazioni e cercare di risolverle da solo.
Per quanto riguarda i casi d'uso che andrò a presentare, ti suggerisco vivamente di interrompere la lettura dopo che ho introdotto ciascun caso e cercare di risolverlo autonomamente.
Come escludere commit
Ipotizziamo che in un altro repository tu abbia questa cronologia:

Prima di iniziare a sperimentare, inserisci un etichetta (tag) per "Commit F", in modo che tu possa poi tornarci più tardi:
git tag original_commit_f
In realtà non vuoi includere le modifiche in "Commit C" e "Commit D". Potresti usare un rebase interattivo come prima ed eliminare quelle modifiche, oppure potresti nuovamente usare git rebase -–onto
. In che modo useresti l'opzione --onto
per "rimuovere" quei due commit?
Puoi portare la base di HEAD
sopra a "Commit B", dove il vecchio genitore era in realtà "Commit D", e ora dovrebbe essere "Commit B". Esamina nuovamente la cronologia:

Effettuare il rebase in modo che "Commit B" costituisca la base di "Commit E", significa "spostare" sia "Commit E" che "Commit F", e dargli un'altra base – "Commit B". Puoi comporre da solo il comando per fare questo?
git rebase --onto <SHA_DI_COMMIT_B> <SHA_OF_COMMIT_D> HEAD
Nota che usando la sintassi qui sopra non viene spostato main
per puntare al nuovo commit, pertanto il risultato è un HEAD
staccato. Se usi gg
(git-graph) o un altro strumento che visualizza la cronologia raggiungibile dai branch potresti essere confuso:

--onto
risulta in un HEAD
staccato (Fonte: Brief)Tuttavia se usi semplicemente git log
(oppure il mio alias git lol
), vedrai la cronologia desiderata:

Non so tu come la pensi, ma questo tipo di cose mi fanno davvero felice. 😊😇
A proposito, potresti omettere HEAD
dal comando precedente poiché questo è il valore predefinito per il terzo parametro. Quindi usando solo:
git rebase --onto <SHA_DI_COMMIT_B> <SHA_DI_COMMIT_D>
otterresti lo stesso risultato. L'ultimo parametro in effetti dice a Git dove si trova la fine della sequenza corrente di commit per i quali effettuare il rebase. Quindi la sintassi git rebase --onto
con tre argomenti è:
git rebase --onto <nuovo_genitore> <vecchio_genitore> <fino_a>
Come spostare commit tra branch
Diciamo di avere la stessa cronologia di prima, alla quale torniamo usando il tag applicato nella sezione precedente:
git checkout original_commit_f
Ora voglio che solo "Commit E", sia in un branch basato su "Commit B". Vale a dire, voglio avere un nuovo branch, che si dirama da "Commit B", con solo "Commit E".

Quindi, cosa significa questo in termini di rebase? Osserva l'immagine qui sopra. Quale commit (o quali) dovrebbero essere oggetto di rebase, e quale commit dovrebbe costituire la nuova base?
So che posso contare su di te qui 😉
Quello che voglio è prendere "Commit E", e solo questo commit, e modificare la sua base in "Commit B". In altre parole, replicare le modifiche introdotte in "Commit E" su "Commit B".
Puoi applicare questa logica alla sintassi di git rebase
?
Eccola (questa volta scrivo <COMMIT_B>
invece di <SHA_DI_COMMIT_B>
, per brevità):
git rebase –-onto <COMMIT_B> <COMMIT_D> <COMMIT_E>
Ora la cronologia risulta questa:

Meraviglioso!
Una nota sui conflitti
Nota che quando esegui un rebase, potresti imbatterti in conflitti, come se stessi facendo un'azione di integrazione con merge. Potresti avere conflitti perché quando si esegue il rebase, stai cercando di applicare modifiche su una base diversa, nella quale forse le modifiche non si applicano.
Per esempio prendi di nuovo il repository precedente e in particolare esamina le modifiche introdotte nel "Commit 12", puntato da main
:
git show main

Ho già trattato il formato di git diff
in dettaglio in un post precedente, ma come veloce ripasso questo commit dice a Git di aggiungere una riga dopo le due righe:
```
This is a sample file
e prima di queste tre righe:
```
def new_feature():
print('new feature')
Supponiamo che tu stia tentando di effettuare un rebase di "Commit 12" su un altro commit. Se, per qualche motivo, queste righe non esistono come nella patch sul commit verso il quale stai effettuando il rebase, allora avrai un conflitto. Per saperne di più sui conflitti e su come risolverli, consulta questa guida.
La prospettiva dal quadro generale

All'inizio di questa guida, ho iniziato citando le similitudini tra git merge
e git rebase
: entrambi sono usati per integrare modifiche introdotte in diverse cronologie.
Tuttavia, come ora sai, le modalità con le quali operano sono molto diverse. Con merge l'integrazione ha come risultato cronologie divergenti, con rebase la cronologia risultante è lineare. I conflitti sono possibili in entrambi i casi. C'è un'ulteriore colonna nella tabella qui sopra (la terza) che richiede una particolare attenzione.
Ora che sai cos'è git rebase
e come usare il rebase interattivo oppure rebase --onto
, spero che tu sia d'accordo con me nell'affermare che git rebase
sia uno strumento potentissimo. Tuttavia ha un grosso inconveniente se confrontato con l'integrazione via merge.
Git rebase modifica la cronologia.
Ciò significa che non dovresti effettuare il rebase di commit che si trovano al di fuori della tua copia locale del repository, sul quali altre persone potrebbero aver basato i loro commit.
In altre parole, se i commit in oggetto sono solo quelli che tu hai creato localmente, vai pure avanti, usa rebase, scatenati.
Ma se i commit sono stati portati sul repository remoto, si può generare un grosso problema, visto che qualcun altro potrebbe fare affidamento su questi commit, che tu più tardi hai sovrascritto, pertanto tu e gli altri avrete versioni diverse del repository.
Questo è improbabile avvenga con merge
in quanto, come abbiamo visto, non modifica la cronologia.
Considera ad esempio l'ultimo caso nel quale abbiamo effettuato un rebase che ha generato questa cronologia:

Ora, supponi che io abbia già portato questo branch nel repository remoto con git push
, e dopo aver fatto questo, un altro sviluppatore abbia scaricato il repository derivandolo da "Commit C". L'altro sviluppatore non sa che, nel frattempo, io avevo effettuato localmente il rebase del mio branch e l'avevo successivamente inviato nuovamente al repository remoto.
Ne deriva un'inconsistenza: l'altro sviluppatore lavora da un commit che non è più disponibile sulla mia copia del repository.
Non approfondirò le conseguenze esatte di quanto esposto qui sopra in questa guida, visto che il mio messaggio principale è che dovresti evitare assolutamente queste situazioni. Se ti interessa sapere cosa sarebbe veramente accaduto, ti lascio un link a un'utile risorsa qui sotto. Per ora riepiloghiamo quanto abbiamo trattato.
Riepilogo
In questo tutorial, hai appreso il comando git rebase
, uno strumento potentissimo per riscrivere la cronologia in Git. Hai preso in considerazione alcune situazioni dove git rebase
può essere di aiuto, e come usarlo con uno, due o tre parametri, con e senza l'opzione --onto
.
Spero di essere stato in grado di convincerti che git rebase
è potente, tuttavia è piuttosto semplice una volta che hai capito il concetto. È uno strumento per "copiare-incollare" i commit (o più precisamente, le modifiche), ed è utile da avere a tua disposizione.
Riferimenti aggiuntivi
- Git Internals YouTube playlist — by Brief (il mio canale YouTube).
- Un mio post precedente sui meccanismi interni di Git.
- Un mio tutorial su Git UNDO - riscrivere la cronologia con Git.
- Documentazione di Git su rebase
- Branch e la potenza di rebase
- Rebase interattivo
- Git rebase --onto
Notizie sull'autore
Omer Rosenbaum è Chief Technology Officer per Swimm . È l'autore del canale YouTube Brief. È anche un esperto di cyber training e fondatore della Checkpoint Security Academy. È l'autore di Computer Networks (in Ebraico). Lo puoi trovare su Twitter.