Il mio ufficio vuole una fusione infinita di succursali come politica; quali altre opzioni abbiamo?

119

Il mio ufficio sta cercando di capire come gestiamo le suddivisioni e le fusioni delle filiali e abbiamo riscontrato un grosso problema.

Il nostro problema è con sidebranches a lungo termine - il tipo in cui alcune persone lavorano su un sidebranch che si divide dal master, sviluppiamo per alcuni mesi e quando raggiungiamo un traguardo sincronizziamo i due.

Ora, IMHO, il modo naturale per gestire questo è, schiaccia il sidebranch in un singolo commit. master continua a progredire; come dovrebbe, non stiamo scaricando retroattivamente mesi di sviluppo parallelo nella storia di master . E se qualcuno ha bisogno di una migliore risoluzione per la storia del sidebranch, beh, ovviamente è tutto ancora lì - non è solo in master , è nel sidebranch.

Ecco il problema: lavoro esclusivamente con la riga di comando, ma il resto del mio team utilizza GUIS. E ho scoperto che il GUIS non ha un'opzione ragionevole per visualizzare la cronologia da altri rami. Quindi, se raggiungi uno squash commit, dicendo "questo sviluppo schiacciato dal ramo XYZ ", è un enorme dolore andare a vedere cosa c'è in XYZ .

Su SourceTree, per quanto riesco a trovare, è un enorme mal di testa: se sei su master , e vuoi vedere la cronologia da master+devFeature , devi controllare master+devFeature fuori (toccando ogni singolo file che è diverso), oppure scorri un registro che mostra TUTTI i rami del tuo repository in parallelo finché non trovi il posto giusto. E buona fortuna a capire dove sei.

I miei compagni di squadra, giustamente, non vogliono avere una cronologia dello sviluppo così inaccessibile. Quindi vogliono che queste grandi, lunghe sidebranches di sviluppo vengano unite, sempre con un commit di unione. Non vogliono alcuna cronologia che non sia immediatamente accessibile dal ramo principale.

I odio quell'idea; significa un intreccio infinito di storia di sviluppo parallela. Ma non vedo quale alternativa abbiamo. E sono piuttosto sconcertato; questo sembra bloccare quasi tutto ciò che so sulla buona gestione delle filiali, e sarà per me una costante frustrazione se non riesco a trovare una soluzione.

Abbiamo qualche opzione qui oltre a fondere costantemente i sidebranches in master con merge-commits? Oppure, c'è una ragione per cui l'uso costante dei commit-merge non è così grave come temo?

    
posta Standback 19.12.2016 - 16:23
fonte

7 risposte

242

Anche se uso Git sulla riga di comando, devo essere d'accordo con i tuoi colleghi. Non è sensato schiacciare grandi cambiamenti in un singolo commit. Stai perdendo la storia in questo modo, non solo rendendola meno visibile.

Il punto di controllo della sorgente è tracciare la cronologia di tutte le modifiche. Quando è cambiato cosa perché? A tal fine, ogni commit contiene puntatori a commit padre, a diff e metadati come un messaggio di commit. Ogni commit descrive lo stato del codice sorgente e la cronologia completa di tutte le modifiche che hanno portato a tale stato. Il garbage collector può eliminare i commit che non sono raggiungibili.

Azioni come rebasing, cherry-picking o compressione eliminano o riscrivono la cronologia. In particolare, il commit conseguente non fa più riferimento ai commit originali. Considera questo:

  • Schiaccia alcuni commit e annoti nel messaggio di commit che la cronologia dello squash è disponibile nel commit originale abcd123.
  • Elimina [1] tutti i rami o tag che includono abcd123 dal momento in cui vengono uniti.
  • Lascia che il garbage collector funzioni.

[1]: Alcuni server Git consentono alle filiali di essere protette dall'eliminazione accidentale, ma dubito che tu voglia mantenere tutte le tue funzionalità per l'eternità.

Ora non puoi più cercare quell'impegno, semplicemente non esiste.

Il riferimento a un nome di ramo in un messaggio di commit è anche peggio, poiché i nomi dei rami sono locali a un repository. Ciò che è master+devFeature nel checkout locale potrebbe essere doodlediduh nel mio. Le diramazioni si limitano a spostare le etichette che puntano a qualche oggetto di commit.

Tra tutte le tecniche di riscrittura della cronologia, il rebasing è il più favorevole perché duplica il commit completo con tutta la cronologia e sostituisce semplicemente un commit padre.

Che la cronologia master includa la cronologia completa di tutti i rami che sono stati uniti in essa è una buona cosa, perché ciò rappresenta la realtà. [2] Se ci fosse uno sviluppo parallelo, dovrebbe essere visibile nel registro.

[2]: Per questo motivo, preferisco anche i commit espliciti di merge sulla cronologia linearizzata, ma in fin dei conti falsa risultante dalla ridefinizione.

Sulla riga di comando, git log cerca di semplificare la cronologia visualizzata e tieni tutti visualizzati commette rilevanti. Puoi modificare la semplificazione della cronologia in base alle tue esigenze. Potresti essere tentato di scrivere il tuo proprio strumento log git che cammina sul grafico del commit, ma è generalmente impossibile rispondere "era questo commit originariamente commesso su questo o quel ramo?". Il primo genitore di un commit di unione è il precedente HEAD , cioè il commit nel ramo in cui ci si sta fondendo. Ma questo presuppone che tu non abbia eseguito una fusione inversa dal master nel ramo della funzione, quindi il master forward-forwarding all'unione.

La migliore soluzione per i rami a lungo termine che ho incontrato è quella di impedire i rami che vengono uniti solo dopo un paio di mesi. L'unione è più semplice quando le modifiche sono recenti e di piccole dimensioni. Idealmente, ti unirai almeno una volta alla settimana. L'integrazione continua (come in Extreme Programming, non come in "impostiamo un server Jenkins"), suggerisce anche più fusioni al giorno, vale a dire non mantenere rami di funzionalità separati ma condividere un ramo di sviluppo come una squadra. L'unione prima che una caratteristica sia richiesta da QA richiede che la funzione sia nascosta dietro un flag di funzionalità.

In cambio, l'integrazione frequente consente di individuare i potenziali problemi molto prima e aiuta a mantenere un'architettura coerente: sono possibili cambiamenti di vasta portata perché questi cambiamenti sono rapidamente inclusi in tutte le filiali. Se una modifica rompe del codice, interromperà solo un paio di giorni di lavoro, non un paio di mesi.

La riscrittura della storia può avere senso per progetti davvero enormi quando ci sono più milioni di righe di codice e centinaia o migliaia di sviluppatori attivi. È discutibile il motivo per cui un progetto così ampio dovrebbe essere un singolo repository git invece di essere diviso in librerie separate, ma a tale scala è più conveniente se il repository centrale contiene solo "release" dei singoli componenti. Per esempio. il kernel di Linux utilizza lo schiacciamento per mantenere la storia principale gestibile. Alcuni progetti open source richiedono l'invio di patch via e-mail, invece di un'unione a livello git.

    
risposta data 19.12.2016 - 17:31
fonte
110

Mi piace la risposta di Amon , ma sentivo che una piccola parte aveva bisogno di molta più enfasi: Tu puoi facilmente semplificare la cronologia mentre visualizzi i registri per soddisfare le tue esigenze, ma altri non possono aggiungere la cronologia mentre visualizzano i registri per soddisfare le loro esigenze. Questo è il motivo per cui è preferibile mantenere la cronologia come avveniva.

Ecco un esempio da uno dei nostri repository. Usiamo un modello di richiesta di pull, quindi la funzione ogni assomiglia alle ramificazioni a lungo termine nella storia, anche se di solito vengono eseguite solo una settimana o meno. I singoli sviluppatori a volte scelgono di comprimere la loro cronologia prima di unirli, ma spesso accoppiamo le funzionalità, quindi è relativamente insolito. Ecco i primi commit in gitk , il gui fornito in bundle con git:

Sì,unpo'ungroviglio,macipiaceancheperchépossiamovedereconprecisionechihacosacambiaacheora.Rifletteaccuratamentelanostrastoriadisviluppo.Sevogliamovedereunavistadilivellosuperiore,unafusionedirichiestedipullallavolta,possiamoguardarelaseguentevista,cheèequivalentealcomandogitlog--first-parent:

git log ha molte più opzioni progettate per darti esattamente le visualizzazioni che desideri. gitk può prendere qualsiasi arbitrario argomento git log per costruire una vista grafica. Sono sicuro che altre GUI hanno capacità simili. Leggi i documenti e impara a usarli correttamente, anziché applicare la tua visualizzazione git log preferita a tutti in fase di unione.

    
risposta data 19.12.2016 - 18:34
fonte
34

Our issue is with long-term sidebranches -- the kind where you've got a few people working a sidebranch that splits from master, we develop for a few months, and when we reach a milestone we sync the two up.

Il mio primo pensiero è - non farlo nemmeno a meno che non sia assolutamente necessario. A volte le tue fusioni devono essere difficili. Mantenere i rami indipendenti e il più breve possibile. È un segno che devi rompere le tue storie in blocchi di implementazione più piccoli.

Nel caso in cui tu debba fare questo, allora è possibile unire in git con l'opzione --no-ff in modo che le cronologie siano mantenute distinte sul proprio ramo. I commit appariranno ancora nella cronologia unita, ma possono anche essere visti separatamente sul feature branch in modo tale che almeno sia possibile determinare la linea di sviluppo di cui facevano parte.

Devo ammettere che quando ho iniziato a usare git ho trovato un po 'strano che il ramo commetti apparisse nella stessa storia del ramo principale dopo l'unione. Era un po 'sconcertante perché non sembrava che quei commit appartenessero a quella storia. Ma in pratica, non è qualcosa di veramente doloroso, se si considera che il ramo dell'integrazione è proprio questo: il suo intero scopo è quello di combinare i rami delle funzionalità. Nel nostro team, non schiacciamo e facciamo frequenti commit di commit. Usiamo --no-ff tutto il tempo per assicurarci che sia facile vedere la cronologia esatta di qualsiasi funzione se vogliamo investigarla.

    
risposta data 19.12.2016 - 20:11
fonte
11

Schiacciare un sidebranch a lungo termine ti farebbe perdere molte informazioni.

Quello che vorrei fare è cercare di rebase il master nel sidebranch a lungo termine prima di unire il sidebranch in master . In questo modo mantieni ogni commit nel master, mentre rendi la cronologia del commit lineare e più facile da capire.

Se non potessi farlo facilmente ad ogni commit, lo farei essere non lineare , per mantenere chiaro il contesto di sviluppo. A mio parere, se ho una fusione problematica durante il rebase del master nella sidebranche, significa che la non-linearità ha un significato nel mondo reale. Ciò significa che sarà più facile capire cosa è successo nel caso avessi bisogno di scavare nella storia. Ho anche l'immediato vantaggio di non dover fare un rebase.

    
risposta data 20.12.2016 - 11:55
fonte
10

Permettimi di rispondere ai tuoi punti in modo diretto e chiaro:

Our issue is with long-term sidebranches -- the kind where you've got a few people working a sidebranch that splits from master, we develop for a few months, and when we reach a milestone we sync the two up.

Di solito non vuoi lasciare i tuoi rami in sincronia per mesi.

Il tuo ramo di funzionalità è derivato da qualcosa che dipende dal tuo flusso di lavoro; chiamiamolo master per semplicità. Ora, ogni volta che ti impegni a dominare, puoi e dovresti git checkout long_running_feature ; git rebase master . Ciò significa che i tuoi rami sono, per impostazione, sempre sincronizzati.

git rebase è anche la cosa giusta da fare qui. Non è un hack o qualcosa di strano o pericoloso, ma completamente naturale. Si perde un po 'di informazioni, che è il "compleanno" del ramo della funzione, ma il gioco è fatto. Se qualcuno lo trova importante, potrebbe essere fornito salvandolo da qualche altra parte (nel tuo sistema di ticket o, se il bisogno è grande, in git tag ...).

Now, IMHO, the natural way to handle this is, squash the sidebranch into a single commit.

No, non lo vuoi assolutamente, vuoi un commit di fusione. Un merge commit è anche un "commit singolo". In qualche modo, non inserisce tutto il singolo ramo che commette "dentro" il master. È un singolo commit con due genitori: il master head e il branch head al momento dell'unione.

Assicurati di specificare l'opzione --no-ff , ovviamente; la fusione senza --no-ff dovrebbe, nel tuo scenario, essere severamente vietata. Sfortunatamente --no-ff non è il valore predefinito; ma credo che ci sia un'opzione che puoi impostare che lo rende così. Vedi git help merge per cosa --no-ff fa (in breve: attiva il comportamento che ho descritto nel paragrafo precedente), è fondamentale.

we're not retroactively dumping months of parallel development into master's history.

Assolutamente no - non si sta mai scaricando qualcosa "nella cronologia" di qualche ramo, specialmente non con un commit di unione.

And if anybody needs better resolution for the sidebranch's history, well, of course it's all still there -- it's just not in master, it's in the sidebranch.

Con un commit di unione, è ancora lì. Non nel master, ma nel sidebranch, chiaramente visibile come uno dei genitori del commit di unione, e mantenuto per l'eternità, come dovrebbe essere.

Vedi cosa ho fatto? Tutte le cose che descrivi per il tuo commit di squash sono proprio lì con il merge --no-ff commit.

Here's the problem: I work exclusively with the command line, but the rest of my team uses GUIS.

(Nota a margine: lavoro quasi esclusivamente anche con la riga di comando (beh, questa è una bugia, di solito uso emacs magit, ma questa è un'altra storia - se non sono in un posto conveniente con la mia configurazione individuale di emacs, io preferisci la riga di comando), ma per favore fai un favore a te stesso e prova almeno git gui una volta. È così molto più efficiente per il prelievo di linee, hunk ecc. per aggiungere / annullare aggiunte.)

And I've discovered the GUIS don't have a reasonable option to display history from other branches.

Questo perché ciò che stai cercando di fare è totalmente contro lo spirito di git . git costruisce dal nucleo su un "grafo aciclico diretto", il che significa che molte informazioni sono nella relazione genitore-figlio di commit. E, per le unioni, ciò significa che la vera unione si impegna con due genitori e un bambino. Le GUI dei tuoi colleghi andranno bene non appena usi no-ff merge commit.

So if you reach a squash commit, saying "this development squashed from branch XYZ", it's a huge pain to go see what's in XYZ.

Sì, ma questo non è un problema della GUI, ma del commit dello squash. Usare una zucca significa lasciare pendere la testa del ramo della caratteristica e creare un nuovo commit in master . Questo rompe la struttura su due livelli, creando un grande casino.

So they want these big, long development-sidebranches merged in, always with a merge commit.

E hanno assolutamente ragione. Ma non sono "uniti in ", sono semplicemente uniti. Un'unione è una cosa veramente bilanciata, non ha un lato preferito che viene unito "nell'altro" ( git checkout A ; git merge B è esattamente uguale a git checkout B ; git merge A tranne che per piccole differenze visive come i rami scambiati in git log ecc. ).

They don't want any history that isn't immediately accessible from the master branch.

Che è completamente corretto. In un momento in cui non ci sono caratteristiche non divise, avresti un singolo ramo master con una ricca storia che incapsula tutte le linee di commit delle caratteristiche che c'erano, tornando al git init commit dall'inizio del tempo (nota che io in particolare evitato di usare il termine "rami" nell'ultima parte di quel paragrafo perché la cronologia in quel momento non è più "rami", sebbene il grafico di commit sia piuttosto ramoso).

I hate that idea;

Quindi ti trovi un po 'di dolore, dal momento che stai lavorando contro lo strumento che stai utilizzando. L'approccio git è molto elegante e potente, specialmente nell'area di branching / merging; se lo fai bene (come accennato in precedenza, specialmente con --no-ff ) è a passi da gigante superiour ad altri approcci (ad esempio, il disordine di sovversione di avere strutture di directory parallele per i rami).

it means an endless, unnavigable tangle of parallel development history.

Senza fine, parallelo - sì.

Unnavigable, tangle - no.

But I'm not seeing what alternative we have.

Perché non funziona proprio come l'inventore di git , i tuoi colleghi e il resto del mondo lo fanno, ogni giorno?

Do we have any option here besides constantly merging sidebranches into master with merge-commits? Or, is there a reason that constantly using merge-commits is not as bad as I fear?

Nessuna altra opzione; non così male.

    
risposta data 20.12.2016 - 23:09
fonte
1

Personalmente preferisco fare lo sviluppo in un fork, quindi estrarre le richieste per unirle nel repository principale.

Ciò significa che se voglio rebase le mie modifiche in cima al master, o schiacciare alcuni commit WIP, posso farlo completamente. Oppure posso solo richiedere che venga incorporata anche tutta la mia storia.

Quello che come fare è fare il mio sviluppo su un ramo, ma frequentemente rebase contro master / dev. In questo modo ottengo i cambiamenti più recenti dal master senza che un gruppo di merge si impegna nel ramo mio , o che deve gestire un intero carico di conflitti di unione quando è ora di unire al master.

Per rispondere esplicitamente alla tua domanda:

Do we have any option here besides constantly merging sidebranches into master with merge-commits?

Sì - puoi unirli una volta per ramo (quando la funzionalità o la correzione è "completa") o se non ti piace che l'unione si impegna nella tua cronologia, puoi semplicemente eseguire un'unione avanti veloce sul master dopo aver eseguito una finale rebase.

    
risposta data 19.12.2016 - 22:23
fonte
-1

Il controllo della revisione è spazzatura in uscita.

Il problema è che il work in progress sul feature branch può contenere un sacco di "proviamo questo ... no che non ha funzionato, sostituiamolo con quello" e tutti i commit tranne il finale "che "Finisco per inquinare inutilmente la storia.

In definitiva, la cronologia dovrebbe essere mantenuta (in alcuni casi potrebbe essere utile in futuro), ma solo una "copia pulita" dovrebbe essere unita.

Con Git, questo può essere fatto ramificando innanzitutto il ramo della funzione (per conservare tutta la cronologia), quindi (interattivamente) ribasando il ramo del ramo della funzione da master e quindi unendo il ramo rebased.

    
risposta data 19.12.2016 - 20:41
fonte

Leggi altre domande sui tag