Una storia Git ordinata e lineare

Una delle cose che viene trascurata in molti progetti basati su Git è il valore di una storia di commit lineare. In effetti, molti strumenti e linee guida scoraggiano i flussi di lavoro Git che mirano a una cronologia lineare. Trovo questo triste, dal momento che una storia ordinata è molto preziosa, e c’è un flusso di lavoro diretto che garantisce una storia lineare.

Storia lineare vs non lineare

Una storia lineare è semplicemente una storia Git in cui tutti i commit vengono uno dopo l’altro. Cioè. non troverai fusioni di rami con storie di commit indipendenti.

1 - nonlinear-vs-linear

Perché vuoi una cronologia lineare?

Oltre ad essere ordinato e logico, una storia lineare è utile quando:

  • Guardando la storia. Una storia non lineare può essere molto difficile da seguire-a volte al punto che la storia è semplicemente incomprensibile.
  • Modifiche di backtracking. Ad esempio: “La funzionalità A è stata introdotta prima o dopo il bugfix B?”.
  • Rintracciare i bug. Git ha una funzione molto ordinata chiamata Git bisect, che può essere utilizzata per trovare rapidamente quale commit ha introdotto un bug o una regressione. Tuttavia, con una storia non lineare, Git bisect diventa difficile o addirittura impossibile da usare.
  • Ripristino delle modifiche. Supponiamo di aver trovato un commit che ha causato una regressione o di voler rimuovere una funzionalità che non doveva uscire in una versione specifica. Con un po ‘ di fortuna (se il tuo codice non è cambiato troppo) puoi semplicemente ripristinare i commit indesiderati usando Git revert. Tuttavia, se si dispone di una cronologia non lineare, forse con molte fusioni tra rami, questo sarà significativamente più difficile.

Ci sono probabilmente un’altra manciata di situazioni in cui una cronologia lineare è molto preziosa, a seconda di come usi Git.

Il punto è: meno lineare è la tua storia, meno preziosa è.

Cause di una cronologia non lineare

In breve, ogni commit di unione è una potenziale fonte di una cronologia non lineare. Tuttavia, esistono diversi tipi di commit di unione.

Unisci da un ramo di argomento in master

Quando hai finito con il tuo ramo di argomento e vuoi integrarlo in master, un metodo comune è quello di unire il ramo di argomento in master. Forse qualcosa del genere:

git checkout mastergit pullgit merge --no-ff my-topic-branch

Una buona proprietà con questo metodo è che conservi le informazioni su quali commit facevano parte del tuo ramo di argomento (un’alternativa sarebbe quella di lasciare fuori “–no-ff”, che consentirebbe a Git di fare un avanzamento rapido invece di un merge, nel qual caso potrebbe non essere così chiaro quale dei commit apparteneva effettivamente al tuo ramo di argomento).

Il problema con l’unione a master sorge quando il ramo dell’argomento è basato su un vecchio master invece dell’ultimo suggerimento di master. In questo caso si otterrà inevitabilmente una storia non lineare.

2 - merging-old-branch

Se questo sarà un problema comune o meno dipende in gran parte da quanto è attivo il repository Git, da quanti sviluppatori stanno lavorando contemporaneamente, ecc.

Unisci dal master in un ramo di argomento

A volte vuoi aggiornare il tuo ramo in modo che corrisponda all’ultimo master (ad es. ci sono alcune nuove funzionalità su master che si desidera ottenere nel ramo argomento, o si scopre che non è possibile unire il ramo argomento in master perché ci sono conflitti).

Un metodo comune, che è anche raccomandato da alcuni, è quello di unire la punta del master nel ramo dell’argomento. Questa è una delle principali fonti di storia non lineare!

3 - merging-master-in-topic

La soluzione: Rebase!

Git rebase è una funzione molto utile che dovresti usare se vuoi una cronologia lineare. Alcuni trovano il concetto di rebasing imbarazzante, ma è davvero abbastanza semplice: ripeti le modifiche (commit) nel tuo ramo sopra un nuovo commit.

Ad esempio, puoi usare Git rebase per cambiare la radice del tuo ramo topic da un vecchio master alla punta dell’ultimo master. Supponendo che tu abbia estratto il tuo ramo di argomento, puoi farlo:

git fetch origingit rebase origin/master

4 - rebasing

Si noti che l’operazione di unione corrispondente sarebbe quella di unire la punta del master nel ramo dell’argomento (come illustrato nella figura precedente). Il contenuto risultante dei file nel ramo dell’argomento sarebbe lo stesso, indipendentemente dal fatto che si esegua una rebase o un’unione. Tuttavia, la storia è diversa (lineare vs non lineare!).

Questo suona tutto bene e bene. Tuttavia, ci sono un paio di avvertimenti con rebase di cui dovresti essere a conoscenza.

Avvertenza 1: Rebase crea nuovi commit

Rebasing un ramo creerà effettivamente nuovi commit. I nuovi commit avranno SHA: s diversi rispetto ai vecchi commit. Questo di solito non è un problema, ma ti imbatterai in problemi se rebase un ramo che esiste al di fuori del tuo repository locale (ad esempio se il tuo ramo esiste già sull’origine).

Se si dovesse spingere un ramo rebased a un repository remoto che contiene già lo stesso ramo (ma con la vecchia cronologia), si:

  1. Devi forzare il push del ramo (ad esempio git push --force-with-lease), poiché Git non ti permetterà di spingere una nuova cronologia a un ramo esistente altrimenti. Ciò sostituisce efficacemente la cronologia per il ramo remoto specificato con una nuova cronologia.
  2. Forse rendere qualcun altro molto infelice, dal momento che la loro versione locale del ramo dato non corrisponde più al ramo sul telecomando, il che può portare a tutti i tipi di problemi.

In generale, evitare di sovrascrivere la cronologia di un ramo su un telecomando (l’unica eccezione è riscrivere la cronologia di un ramo che è in revisione del codice – a seconda di come funziona il sistema di revisione del codice – ma questa è una discussione diversa).

Se è necessario rebase un ramo che è condiviso con altri su un telecomando, un semplice flusso di lavoro consiste nel creare un nuovo ramo che si rebase, invece di rebase il ramo originale. Supponendo che tu abbia my-topic-branch estratto, puoi fare:

git checkout -b my-topic-branch-2git fetch origingit rebase origin/mastergit push -u origin my-topic-branch-2

…e poi dire alle persone che lavorano su my-topic-branch di continuare a lavorare su my-topic-branch-2 invece. Il vecchio ramo è quindi effettivamente obsoleto e non dovrebbe essere unito a master.

Avvertenza 2: Risolvere i conflitti in una rebase può essere più lavoro che in un’unione

Se si ottiene un conflitto in un’operazione di unione, si risolveranno tutti i conflitti come parte di quel commit di unione.

Tuttavia, in un’operazione di rebase, è potenzialmente possibile ottenere un conflitto per ogni commit nel ramo che si rebase.

In realtà, molte volte scoprirai che se ottieni un conflitto in un commit, incontrerai conflitti correlati (molto simili) nei commit successivi nel tuo ramo, semplicemente perché i commit in un ramo di argomento tendono ad essere correlati (ad esempio modificando le stesse parti del codice).

Il modo migliore per ridurre al minimo i conflitti è tenere traccia di ciò che sta accadendo nel master ed evitare di lasciare che un ramo di argomento venga eseguito troppo a lungo senza rebasing. Trattare con piccoli conflitti in anticipo ogni tanto è più facile che gestirli tutti in un unico grande pasticcio di conflitto felice alla fine.

Alcuni avvisi per gli utenti GitHub

GitHub è grande in molte cose. È fantastico per l’hosting Git e ha una meravigliosa interfaccia web con navigazione del codice,funzionalità di Markdown, Gist, ecc.

Pull requests, d’altra parte, ha alcune funzioni che ostacolano attivamente la cronologia Git lineare. Sarebbe molto gradito se GitHub risolvesse effettivamente questi problemi, ma fino ad allora dovresti essere consapevole delle carenze:

” Merge pull request “consente fusioni non lineari a master

Si può essere tentati di premere il pulsante verde e amichevole” Merge pull request ” per unire un ramo di argomento in master. Soprattutto come si legge ” Questo ramo è aggiornato con il ramo base. La fusione può essere eseguita automaticamente”.

GitHub merge pull request

Quello che GitHub sta davvero dicendo qui, è che il ramo può essere unito per padroneggiare senza conflitti. Non controlla se il ramo pull request è basato sull’ultimo master.

In altre parole, se si desidera una cronologia lineare, è necessario assicurarsi che il ramo pull request sia basato sull’ultimo master. Per quanto posso dire, nessuna di queste informazioni è disponibile tramite l’interfaccia Web GitHub (a meno che tu non stia usando “rami protetti” – vedi sotto), quindi devi farlo dal tuo client Git locale.

Anche se la richiesta pull è correttamente rebased, non vi è alcuna garanzia che l’operazione di unione nell’interfaccia Web GitHub sarà atomica (cioè qualcuno può spingere le modifiche a master prima che l’operazione di unione vada a buon fine-e GitHub non si lamenterà).

Quindi, in realtà, l’unico modo per essere sicuri che i tuoi rami siano correttamente basati sull’ultimo master è eseguire l’operazione di unione localmente e spingere manualmente il master risultante. Qualcosa di simile:

git checkout mastergit pullgit checkout my-pullrequest-branchgit rebase mastergit checkout mastergit merge --no-ff my-pullrequest-branchgit push origin master

Se sei sfortunato e qualcuno riesce a spingere le modifiche a padroneggiare tra le operazioni di pull e push, l’operazione push verrà negata. Questa è una buona cosa, tuttavia, poiché garantisce che la tua operazione sia atomica. Solo git reset --hard origin/master e ripetere i passaggi precedenti fino a quando non passa attraverso.

Nota: Rispettare le linee guida del progetto w. r. t. codice revisione e test. Ad esempio, se stai eseguendo test automatici (build, analisi statica, test unitari,…) come parte di una richiesta pull, dovresti probabilmente ripresentare il tuo ramo rebased (usando git push-f o aprendo un nuovo PR) piuttosto che aggiornare manualmente il ramo master.

La funzionalità dei rami protetti incoraggia le fusioni dal master

Se si utilizzano rami protetti e controlli di stato nel progetto GitHub, si ottiene effettivamente la protezione dall’unione di un ramo di richiesta pull in master a meno che non sia basato sull’ultimo master (penso che la logica sia che i controlli di stato eseguiti sul ramo PR

Tuttavia If se il ramo pull request non è basato sull’ultimo master, ti viene presentato un pulsante amichevole chiamato “Aggiorna ramo”, con il testo “Questo ramo non è aggiornato con il ramo base. Unisci le ultime modifiche dal master in questo ramo”.

github-update-branch

A questo punto, l’opzione migliore è quella di rebase il ramo localmente e forza-spingerlo alla richiesta di pull. Supponendo che tu abbia estratto my-pullrequest-branch, fai:

git fetch origingit rebase origin/mastergit push -f

Sfortunatamente, le richieste di pull GitHub non funzionano bene con i push forzati, quindi alcune delle informazioni sulla revisione del codice potrebbero perdersi nel processo. Se ciò non è accettabile, prendere in considerazione la creazione di una nuova richiesta pull (ad esempio, spingere il ramo rebased a un nuovo ramo remoto e creare una richiesta pull dal nuovo ramo).

Conclusione

Se ti interessa una cronologia lineare:

  • Rebase il ramo argomento sulla parte superiore del master più recente prima di unire al master.
  • Non unire master nel ramo argomento. Rebase invece.
  • Quando condividi il tuo ramo di argomenti con altri, crea un nuovo ramo ogni volta che devi rebase (my-topic-branch-2, my-topic-branch-3, …).
  • Se stai utilizzando le richieste pull GitHub, tieni presente:
    • Il pulsante “Unisci” non garantisce che il ramo PR sia basato sull’ultimo master. Rebase manualmente quando necessario.
    • Se si utilizzano rami protetti con controlli di stato, non premere mai il pulsante “Aggiorna ramo”. Rebase manualmente invece.

Se non ti interessa troppo una storia Git lineare – felice fusione!

Wishlist per GitHub

A voi persone che lavorano in GitHub: Si prega di aggiungere il supporto per i flussi di lavoro rebase nelle richieste pull (queste potrebbero essere opzioni di repository opt-in).

  • Aggiungi la possibilità di disabilitare il pulsante di unione/operazione nelle richieste pull se il ramo non è basato sull’ultimo master (questo non dovrebbe richiedere l’uso di controlli di stato).
  • Aggiungi un pulsante “Rebase su latest master”. In molti casi questa dovrebbe essere un’operazione senza conflitti che può essere facilmente eseguita tramite l’interfaccia web.
  • Conserva la cronologia delle richieste di pull (commit, commenti, …) dopo un push rebase / force.

Leave a Reply