Perché dovresti smettere di usare Git rebase

Dopo aver usato Git per diversi anni, mi sono trovato gradualmente a utilizzare comandi Git sempre più avanzati come parte del mio flusso di lavoro quotidiano. Poco dopo aver scoperto Git rebase, l'ho incorporato rapidamente nel mio flusso di lavoro quotidiano. Coloro che hanno familiarità con il rebasing sanno quanto sia potente uno strumento e quanto sia allettante usarlo tutto il tempo. Tuttavia, ho presto scoperto che il rebasing presenta alcune sfide che non sono ovvie quando inizi a farlo. Prima di presentarli, ricapitolerò rapidamente le differenze tra fusione e rebasing.

Consideriamo innanzitutto l'esempio di base in cui si desidera integrare un ramo di funzionalità con master. Con l'unione, creiamo un nuovo commit g che rappresenta l'unione tra i due rami. Il grafico di commit mostra chiaramente cosa è successo e possiamo vedere i contorni del grafico "binario del treno" familiare dai repository Git più grandi.

Esempio di fusione

In alternativa, potremmo rifare prima di fonderci. I commit vengono rimossi e il ramo della funzione viene reimpostato su master, dopodiché i commit vengono riapplicati in cima alla funzione. Le differenze di questi commit riapplicati sono in genere identiche alle loro controparti originali, ma hanno commit dei genitori diversi e quindi chiavi SHA-1 diverse.

Esempio di rebasing

Ora abbiamo cambiato il commit di base della funzione da b a c, rifondandolo letteralmente. L'unione della funzione con master ora è un'unione di avanzamento rapido, poiché tutti gli commit sulla funzione sono discendenti diretti del master.

Esempio di fusione rapida

Rispetto all'approccio di unione, la storia risultante è lineare senza rami divergenti. La migliore leggibilità è stata la ragione per cui preferivo riordinare i rami prima della fusione e mi aspetto che ciò avvenga anche per altri sviluppatori.

Tuttavia, questo approccio presenta alcune sfide che potrebbero non essere ovvie.

Si consideri il caso in cui una dipendenza ancora in uso sulla funzione è stata rimossa dal master. Quando le feature vengono ridisegnate su master, il primo commit riapplicato interromperà la compilazione, ma finché non ci sono conflitti di unione, il processo di rebase continuerà senza interruzioni. L'errore del primo commit rimarrà presente in tutti i commit successivi, risultando in una catena di commit interrotti.

Questo errore viene rilevato solo dopo il completamento del processo di rebase e di solito viene corretto applicando un nuovo bugfix commit g in cima.

Esempio di rebasing fallito

Se si verificano conflitti durante il rebasing, tuttavia, Git si interromperà sul commit in conflitto, consentendo di risolvere il conflitto prima di procedere. Risolvere i conflitti nel mezzo del rifacimento di una lunga catena di impegni è spesso confuso, difficile da correggere e un'altra fonte di potenziali errori.

L'introduzione di errori è ancora più problematica quando si verifica durante il rebasing. In questo modo, nuovi errori vengono introdotti quando si riscrive la cronologia e possono mascherare veri e propri bug introdotti quando la cronologia è stata scritta per la prima volta. In particolare, ciò renderà più difficile l'uso di Git bisect, probabilmente lo strumento di debug più potente nella casella degli strumenti di Git. Ad esempio, considerare il seguente ramo di funzionalità. Diciamo che abbiamo introdotto un bug verso la fine del ramo.

Un ramo con bug introdotto verso la fine

Potresti non scoprire questo bug fino a settimane dopo che il ramo è stato unito al master. Per trovare il commit che ha introdotto il bug, potresti dover cercare tra decine o centinaia di commit. Questo processo può essere automatizzato scrivendo uno script che verifica la presenza del bug ed eseguendolo automaticamente tramite Git bisect, usando il comando git bisect run .

Bisect eseguirà una ricerca di bisection nella cronologia, identificando il commit che ha introdotto il bug. Nell'esempio mostrato di seguito, riesce a trovare il primo commit difettoso, poiché tutti i commit non funzionanti contengono l'effettivo bug che stiamo cercando.

Esempio di successo Git bisect

D'altra parte, se abbiamo introdotto ulteriori commit non funzionanti durante il rebasing (qui, d ed e), la bisect si troverà nei guai. In questo caso, speriamo che Git identifichi il commit f come il cattivo, ma identifica erroneamente la dinstead, poiché contiene qualche altro errore che interrompe il test.

Esempio di una bisetta Git fallita

Questo problema è maggiore di quanto possa sembrare all'inizio.

Perché usiamo Git? Perché è il nostro strumento più importante per rintracciare la fonte di bug nel nostro codice. Git è la nostra rete di sicurezza. Rifacendo, diamo a questo meno priorità, a favore del desiderio di raggiungere una storia lineare.

Qualche tempo fa, ho dovuto passare in rassegna diverse centinaia di commit per rintracciare un bug nel nostro sistema. Il commit difettoso è stato individuato nel mezzo di una lunga catena di commit che non è stata compilata, a causa di un rebase difettoso eseguito da un collega. Questo errore inutile e totalmente evitabile mi ha portato a spendere quasi un giorno in più per rintracciare il commit.

Quindi, come possiamo evitare queste catene di commit non funzionanti durante il rebasing? Un approccio potrebbe essere quello di consentire il completamento del processo rebase, testare il codice per identificare eventuali bug e tornare indietro nella storia per correggere i bug in cui sono stati introdotti. A tal fine, potremmo utilizzare il rebasing interattivo.

Un altro approccio sarebbe quello di mettere in pausa Git durante ogni fase del processo di rebase, testare eventuali bug e correggerli immediatamente prima di procedere.

Questo è un processo ingombrante e soggetto a errori, e l'unica ragione per farlo sarebbe quella di raggiungere una storia lineare. C'è un modo più semplice e migliore?

C'è; Git merge. È un processo semplice, in una sola fase, in cui tutti i conflitti vengono risolti in un unico commit. L'impegno di fusione risultante segna chiaramente il punto di integrazione tra i nostri rami e la nostra storia descrive ciò che è realmente accaduto e quando è successo.

L'importanza di mantenere vera la tua storia non dovrebbe essere sottovalutata. Rifacendo, stai mentendo a te stesso e alla tua squadra. Fai finta che i commit siano stati scritti oggi, quando in realtà sono stati scritti ieri, sulla base di un altro commit. Hai eliminato i commit dal loro contesto originale, nascondendo ciò che è realmente accaduto. Puoi essere sicuro che il codice venga compilato? Puoi essere sicuro che i messaggi di commit abbiano ancora senso? Potresti credere che stai pulendo e chiarendo la tua storia, ma il risultato potrebbe benissimo essere il contrario.

È impossibile dire quali errori e sfide porterà il futuro per la tua base di codice. Tuttavia, puoi essere certo che una storia vera sarà più utile di una riscritta (o falsa).

Cosa motiva le persone a ribaltare i rami?

Sono giunto alla conclusione che si tratta di vanità. Rebasing è un'operazione puramente estetica. La storia apparentemente pulita ci affascina come sviluppatori, ma non può essere giustificata, dal punto di vista tecnico né funzionale.

Storia non lineare. Figura di Paul Hammant

I grafici della storia non lineare, i "binari del treno", possono essere intimidatori. All'inizio mi sono sentito così, ma non c'è motivo di averne paura. Esistono molti magnifici strumenti in grado di analizzare e visualizzare la complessa storia di Git, sia basata sulla GUI che sulla CLI. Questi grafici contengono informazioni preziose su ciò che è accaduto e quando è accaduto, e non otteniamo nulla linearizzandolo.

Git è fatto e incoraggia la storia non lineare. Se questo ti scoraggia, potresti stare meglio usando un VCS più semplice che supporta solo la cronologia lineare.

Penso che dovresti mantenere la tua storia vera. Mettiti comodo con gli strumenti per analizzarlo e non cadere nella tentazione di riscriverlo. I premi per la riscrittura sono minimi, ma i rischi sono grandi. Mi ringrazierai la prossima volta che passerai in rassegna la tua storia per rintracciare un bug subdolo.

Grazie a Paul Hammant e Aslak Hellesøy per il prezioso feedback sulle bozze di questo post. Grazie a Paul Hammant per la figura della storia non lineare. Il suo eccellente sito è una lettura altamente raccomandata. Un ringraziamento speciale ad Aslak per avermi incoraggiato a scrivere questo post in primo luogo.

Questo post è basato su un discorso che ho tenuto in norvegese a JavaZone 2016: https://vimeo.com/182068915