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

  1. Ho creato anche un video che tratta il contenuto di questo post. Se desideri guardarlo mentre stai leggendo, puoi trovarlo qui.
  2. Se vuoi fare esperimenti con il repository che ho usato e provare da solo i comandi, puoi trovarlo qui.
  3. 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.

rebase_integrazione
Una cronologia divergente - 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".

rebase_divergenze
Il risultato dell'esecuzione di 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).

integrazione-con-rebase
Il risultato dell'esecuzione di 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>:

image-199
Esecuzione di 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":

image-200
Il risultato di git diff - le modifiche introdotte da "Commit 5" (Fonte: Brief)

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)

image-201-1
Creazione del 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>
image-202
Uso di cherry-pick per applicare le modifiche introdotte da "Commit 5" in main (Fonte: Brief)

Esamina questo log (risultato di git lol):

image-205
Il risultato di 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:

image-204
Il risultato di git show HEAD (Fonte: Brief)

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-pickCome 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:

  1. Trova l'antenato comune.
  2. Identifica i commit da "replicare".
  3. Per ogni commit X, calcola diff(genitore(X), X), e conserva il risultato come patch(X).
  4. Sposta HEAD verso la nuova base.
  5. 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:

image-206
Cronologia dei commit prima di eseguire git rebase (Fonte: Brief)

Ora veniamo alla parte eccitante:

git rebase john_branch

Osserva la cronologia:

image-207
La cronologia dopo il rebase (Fonte: Brief)

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.

image-209
La cronologia dopo il rebase (Fonte: Brief)

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:

  1. Ha una marca temporale diversa.
  2. Ha un commit genitore diverso – "Commit 6" invece che "Commit 4".
  3. 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:

image-210
La cronologia dopo il rebase e l'entrata in main (Fonte Brief)

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:

image-211
Il risultato di un merge fast-forward (Fonte Brief)

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>
image-238
Annullamento dell'ultima operazione di merge (Fonte: Brief)

Annulla anche il rebase in questo modo:

git checkout paul_branch
git reset -–hard <COMMIT 9_ORIGINALE>
image-239
Annullamento dell'operazione di rebase (Fonte: Brief)

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

Visualizing the history after "undoing" the rebase operation (Source: Brief)
Visualizzazione della cronologia dopo l'annullamento dell'operazione di rebase (Fonte: Brief)è 

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
image-241
git show HEAD mostra le modifiche introdotte da "Commit 9" (Fonte: Brief)

Proseguiamo a ritroso nel grafico dei commit:

git show HEAD~
image-242
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
image-243
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:

image-250
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":

image-251
L'accorpamento di "Commit 8" in "Commit 7" (Fonte: Brief)

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"):

image-252
Inserimento del messaggio di commit: Commits 7+8 (Fonte: Brief)

Dai un'occhiata alla cronologia:

image-253
La cronologia dopo il rebase interattivo (Fonte: Brief)

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.

image-254-1
La cronologia dopo il rebase interattivo - visualizzata (Fonte: Brief)

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
image-255-1
Creazione di new_branch che si dirama da main (Fonte: Brief)
image-256
Una cronologia pulita per new_branch che si dirama da main (Fonte: Brief

Ora facciamo qualche modifica ed eseguiamo il commit:

nano code.py
image-257
Aggiunta della funzione 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:

image-258
Aggiunta una docstring (stringa di documentazione in Python) all'inizio del file (Fonte: Brief)

Adesso eseguiamo il commit queste modifiche:

git add code.py
git commit -m "Commit 11"

Poi effettuiamo un altro cambiamento:

image-259
Aggiunto @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:

image-260
La cronologia dopo l'introduzione di "Commit 12" (Fonte: Brief)

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:

image-261
Visivamente, vorrei che tu inglobassi (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
image-262
La cronologia prima e dopo il rebase, "Commit 10" è stato inserito in new_branch (Fonte: Brief

Ora dai uno sguardo alla nostra meravigliosa cronologia! 😍

image-263
La cronologia prima e dopo il rebase, "Commit 10" è stato inserito in 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:

image-264
Un nuovo file, 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:

image-265
Creazione di 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:

image-266
Modifica del file 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:

image-267
La cronologia dopo l'introduzione di "Commit 15" (Fonte: Brief)

Ritorna su feature_branch_1 e modifica 1.py:

git checkout feature_branch_1
image-268
Modifica di 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:

image-270
La cronologia dopo l'introduzione di "Commit 16" (Fonte: Brief)

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.

image-271
Vuoi spostare "Commit 14" e "Commit 15" (Fonte: Brief)

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:

image-272
La cronologia dei commit dopo il rebase (Fonte: Brief)

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:

image-273
Modifica di ' 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:

image-274
Inserimento della funzione 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:

image-275
Modificato '__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:

image-276
La cronologia di commit dopo l'introduzione di "Commit 19" (Fonte: Brief)

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:

image-277
Questi sono i commit che voglio modificare (Fonte: Brief)

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.

image-279
Uso di 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:

image-280
Rebase interattivo (Fonte: Brief)

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:

image-281
Rebase interattivo - modifica dell'ordine dei commit e accorpamento (Fonte: Brief)

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

image-282
Inserimento di un messaggio per il nuovo commit (Fonte: Brief)

Ora guardiamo la nostra stupenda cronologia:

image-283
La cronologia risultante (Fonte: Brief)

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:

image-284
Un'altra cronologia di commit (Fonte: Brief)

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:

image-284--1-
Di nuovo la cronologia (Fonte: Brief)

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:

image-285
Il rebase con --onto risulta in un HEAD staccato (Fonte: Brief)

Tuttavia se usi semplicemente git log (oppure il mio alias git lol), vedrai la cronologia desiderata:

image-286--1-
La cronologia risultante (Fonte: Brief)

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".

image-287
La cronologia corrente, considerando "Commit E" (Fonte: Brief

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:

image-288
La cronologia dopo il rebase (Fonte: Brief)

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
image-289
Le modifiche introdotte da "Commit 12" (Fonte: Brief)

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

tabella-merge-rebase
Confronto fra rebase e merge (Fonte: Brief)

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:

image-288--1-
La cronologia dopo il rebase (Fonte: Brief)

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.

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

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.