version-control push - Git workflow e rebase contro unire le domande




tag delete (9)

Utilizzo Git da un paio di mesi su un progetto con un altro sviluppatore. Ho diversi anni di esperienza con SVN , quindi credo di portare molto bagaglio alla relazione.

Ho sentito che Git è eccellente per ramificazioni e fusioni, e finora non lo vedo. Certo, la ramificazione è semplice, ma quando provo a unirmi, tutto va al diavolo. Ora, sono abituato a questo da SVN, ma mi sembra che ho appena scambiato un sistema di controllo delle versioni parziali per un altro.

Il mio compagno mi dice che i miei problemi derivano dal mio desiderio di unirmi volenti o nolenti, e che dovrei usare rebase invece di unirmi in molte situazioni. Ad esempio, ecco il flusso di lavoro che ha stabilito:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature
git checkout master
git merge my_new_feature

In sostanza, crea un ramo di funzionalità, SEMPRE rebase dal master al ramo e unisci dal ramo al master. È importante notare che il ramo rimane sempre locale.

Ecco il flusso di lavoro con cui ho iniziato

clone remote repository
create my_new_feature branch on remote repository
git checkout -b --track my_new_feature origin/my_new_feature
..work, commit, push to origin/my_new_feature
git merge master (to get some changes that my partner added)
..work, commit, push to origin/my_new_feature
git merge master
..finish my_new_feature, push to origin/my_new_feature
git checkout master
git merge my_new_feature
delete remote branch
delete local branch

Ci sono due differenze essenziali (credo): utilizzo sempre il merge invece di rebasing e spingo il mio branch di funzionalità (e il mio branch di funzionalità commette) nel repository remoto.

Il mio ragionamento per la filiale remota è che voglio che il mio lavoro venga eseguito nel backup mentre sto lavorando. Il nostro repository viene automaticamente sottoposto a backup e può essere ripristinato se qualcosa va storto. Il mio portatile non è, o non così a fondo. Pertanto, odio avere un codice sul mio laptop che non è speculare altrove.

Il mio ragionamento per l'unione invece di rebase è che l'unione sembra essere standard e rebase sembra essere una funzionalità avanzata. La mia sensazione istintiva è che quello che sto cercando di fare non è una configurazione avanzata, quindi rebase non dovrebbe essere necessario. Ho persino esaminato il nuovo libro di Pragmatic Programming su Git, e si occupano di unire ampiamente e di menzionare a malapena rebase.

Ad ogni modo, stavo seguendo il mio flusso di lavoro su un ramo recente, e quando ho provato a fonderlo di nuovo per diventare master, tutto è andato al diavolo. C'erano tonnellate di conflitti con cose che non avrebbero dovuto importare. I conflitti non avevano senso per me. Mi ci è voluto un giorno per risolvere tutto, e alla fine è culminato in una spinta forzata al master remoto, dal momento che il mio maestro locale ha risolto tutti i conflitti, ma quello remoto non era ancora felice.

Qual è il flusso di lavoro "corretto" per qualcosa di simile? Git dovrebbe creare ramificazioni e fusioni in modo super facile, e io non lo vedo.

Aggiornamento 2011-04-15

Questa sembra essere una domanda molto popolare, quindi ho pensato di aggiornare con la mia esperienza di due anni da quando ho chiesto per la prima volta.

Si scopre che il flusso di lavoro originale è corretto, almeno nel nostro caso. In altre parole, questo è ciò che facciamo e funziona:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge my_new_feature

In effetti, il nostro flusso di lavoro è leggermente diverso, poiché tendiamo a fare lo squash anziché le unioni non elaborate. ( Nota: questo è controverso, vedi sotto ) . Questo ci permette di trasformare il nostro intero ramo di funzionalità in un singolo commit su master. Quindi cancelliamo il nostro branch di funzionalità. Questo ci permette di strutturare logicamente i nostri commit sul master, anche se sono un po 'disordinati sui nostri rami. Quindi, questo è quello che facciamo:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge --squash my_new_feature
git commit -m "added my_new_feature"
git branch -D my_new_feature

Polemica di Squash Merge - Come diversi commentatori hanno sottolineato, l'unione di squash cancellerà tutta la cronologia sul tuo ramo delle funzionalità. Come suggerisce il nome, abbatte tutti i commit in uno solo. Per le piccole caratteristiche, questo ha senso in quanto condensa in un unico pacchetto. Per caratteristiche più grandi, probabilmente non è una grande idea, specialmente se i tuoi singoli commit sono già atomici. Dipende davvero dalle preferenze personali.

Github e Bitbucket (altri?) Richieste di pull - Nel caso ti stia chiedendo in che modo fusione / rebase si riferisce a Pull Requests, ti consiglio di seguire tutti i passaggi precedenti fino a quando non sarai pronto per unire nuovamente il master. Invece di fondersi manualmente con git, devi solo accettare il PR. Nota che questo non farà un'unione di squash (almeno non di default), ma non-squash, non-fast-forward è la convenzione di fusione accettata nella comunità di richieste di pull (per quanto ne so). In particolare, funziona in questo modo:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git push # May need to force push
...submit PR, wait for a review, make any changes requested for the PR
git rebase master
git push # Will probably need to force push (-f), due to previous rebases from master
...accept the PR, most likely also deleting the feature branch in the process
git checkout master
git branch -d my_new_feature
git remote prune origin

Sono arrivato ad amare Git e non voglio mai tornare su SVN. Se stai lottando, limitati a seguirlo e alla fine vedrai la luce alla fine del tunnel.


Answers

Nella tua situazione, penso che il tuo partner sia corretto. La cosa bella della ridefinizione è che al di fuori dei tuoi cambiamenti sembra che siano tutti accaduti in una sequenza pulita da soli. Questo significa

  • le tue modifiche sono molto facili da recensire
  • puoi continuare a fare piccoli commit, ma puoi rendere pubblici tutti quei set di commit (unendoli in master) tutto in una volta
  • quando guardi il ramo master pubblico vedrai diverse serie di commit per caratteristiche diverse da parte di sviluppatori diversi ma non saranno tutti mescolati

È comunque possibile continuare a trasferire il proprio ramo di sviluppo privato all'archivio remoto a scopo di backup, ma altri non dovrebbero trattarlo come un ramo "pubblico" dal momento che si procederà a una ridistribuzione. A proposito, un semplice comando per farlo è git push --mirror origin .

L'articolo Il software di packaging che usa Git fa un buon lavoro spiegando i compromessi tra fusione e rebasing. Si tratta di un contesto leggermente diverso ma i principi sono gli stessi: fondamentalmente si tratta di stabilire se le filiali sono pubbliche o private e in che modo si prevede di integrarle nella linea principale.


Con Git non esiste un flusso di lavoro "corretto". Usa quello che fa galleggiare la tua barca. Tuttavia, se si verificano costantemente conflitti durante la fusione dei rami, è possibile coordinare meglio gli sforzi con gli altri sviluppatori? Sembra che voi due continuiate a modificare gli stessi file. Inoltre, fai attenzione alle parole chiave di spazi bianchi e di sovversione (ad es. "$ Id $" e altri).


TL; DR

Un flusso di lavoro di git rebase non ti protegge da persone che hanno problemi con la risoluzione dei conflitti o da persone che sono abituate a un flusso di lavoro SVN, come suggerito in Evitare Git Disasters: A Gory Story . Rende solo più noiosa la risoluzione dei conflitti per loro e rende più difficile il recupero da una cattiva risoluzione dei conflitti. Invece, usa diff3 in modo che non sia così difficile in primo luogo.

Il flusso di lavoro di Rebase non è migliore per la risoluzione dei conflitti!

Sono molto pro-rebase per ripulire la cronologia. Tuttavia, se mai avrò colpito un conflitto, interromperò immediatamente il rebase e farò invece una fusione! Mi fa davvero schifo che le persone raccomandino un flusso di lavoro di rebase come alternativa migliore a un flusso di lavoro di unione per la risoluzione dei conflitti (che è esattamente ciò di cui trattava questa domanda).

Se va "tutto al diavolo" durante una fusione, andrà "tutto al diavolo" durante un rebase, e potenzialmente molto più dell'inferno! Ecco perché:

Motivo n. 1: Risolvi i conflitti una volta, invece di una volta per ogni commit

Quando esegui il rebase invece di unire, dovrai eseguire la risoluzione del conflitto fino a quante volte hai commesso il rebase, per lo stesso conflitto!

Scenario reale

Mi allontano dal padrone per refactoring un metodo complicato in una filiale. Il mio lavoro di refactoring è composto da 15 commit totali mentre lavoro per refactoring e ottenere revisioni del codice. Parte del mio refactoring comporta il fissaggio delle schede miste e degli spazi che erano presenti in master in precedenza. Questo è necessario, ma sfortunatamente sarà in conflitto con qualsiasi modifica apportata in seguito a questo metodo in master. Certo, mentre sto lavorando a questo metodo, qualcuno apporta una modifica semplice e legittima allo stesso metodo nel ramo principale che dovrebbe essere unito alle mie modifiche.

Quando è il momento di unire il mio ramo con il master, ho due opzioni:

Git si fondono: ottengo un conflitto. Vedo il cambiamento che hanno fatto per padroneggiarlo e unirlo a (il prodotto finale di) il mio ramo. Fatto.

git rebase: ottengo un conflitto con il mio primo commit. Risolvendo il conflitto e proseguo con il rebase. Ho un conflitto con il mio secondo commit. Risolvendo il conflitto e proseguo con il rebase. Ho un conflitto con il mio terzo commit. Risolvendo il conflitto e proseguo con il rebase. Ho un conflitto con il mio quarto impegno. Risolvendo il conflitto e proseguo con il rebase. Ho un conflitto con il mio quinto impegno. Risolvendo il conflitto e proseguo con il rebase. Ho un conflitto con il mio sesto impegno. Risolvendo il conflitto e proseguo con il rebase. Ho un conflitto con il mio settimo impegno. Risolvendo il conflitto e proseguo con il rebase. Ottengo un conflitto con il mio ottavo impegno. Risolvendo il conflitto e proseguo con il rebase. Prendo un conflitto con il mio nono impegno. Risolvendo il conflitto e proseguo con il rebase. Ho un conflitto con il mio decimo impegno. Risolvendo il conflitto e proseguo con il rebase. Ho un conflitto con il mio undicesimo impegno. Risolvendo il conflitto e proseguo con il rebase. Ho un conflitto con il mio dodicesimo commit. Risolvendo il conflitto e proseguo con il rebase. Prendo un conflitto con il mio tredicesimo impegno. Risolvendo il conflitto e proseguo con il rebase. Ottengo un conflitto con il mio quattordicesimo commit. Risolvendo il conflitto e proseguo con il rebase. Ho un conflitto con il mio quindicesimo impegno. Risolvendo il conflitto e proseguo con il rebase.

Mi stai prendendo in giro se questo è il tuo flusso di lavoro preferito. Tutto ciò che serve è una correzione di uno spazio bianco in conflitto con una modifica apportata al master e ogni commit sarà in conflitto e deve essere risolto. E questo è uno scenario semplice con solo un conflitto di spazi bianchi. Il cielo ti impedisce di avere un vero conflitto che coinvolge le principali modifiche del codice tra i file e deve risolverlo più volte.

Con tutta la risoluzione extra dei conflitti che devi fare, aumenta la possibilità che tu commetta un errore . Ma gli errori vanno bene perché puoi annullare, vero? Tranne naturalmente ...

Motivo n. 2: Con rebase, non è possibile annullare!

Penso che tutti possiamo essere d'accordo sul fatto che la risoluzione dei conflitti può essere difficile, e anche che alcune persone sono molto cattive. Può essere molto incline agli errori, motivo per cui è così bello che git rende facile annullare!

Quando unisci un ramo, git crea un commit unione che può essere scartato o modificato se la risoluzione del conflitto non va a buon fine. Anche se hai già eseguito il push del commit della merge in un repository pubblico / autorevole, puoi utilizzare git revert per annullare le modifiche introdotte dall'unione e ripetere correttamente l'unione in un nuovo commit di unione.

Quando rebase un ramo, nel probabile caso in cui la risoluzione dei conflitti è sbagliata, sei fregato. Ogni commit ora contiene l'unione errata e non puoi semplicemente ripetere il rebase *. Nella migliore delle ipotesi, devi tornare indietro e modificare ciascuno dei commit compromessi. Non è divertente.

Dopo un rebase, è impossibile determinare quale parte originariamente era stata commessa e ciò che è stato introdotto come risultato di una cattiva risoluzione dei conflitti.

* È possibile annullare un rebase se riesci a scavare i vecchi ref dai log interni di git, o se crei un terzo ramo che punta all'ultimo commit prima di ribasare.

Allontanati dalla risoluzione dei conflitti: usa diff3

Prendi questo conflitto per esempio:

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

Guardando al conflitto, è impossibile dire cosa cambiava ogni ramo o quale fosse il suo intento. Questa è la ragione più grande, a mio avviso, perché la risoluzione dei conflitti sia confusa e difficile.

diff3 al salvataggio!

git config --global merge.conflictstyle diff3

Quando si utilizza la diff3, ogni nuovo conflitto avrà una terza sezione, l'antenato comune unito.

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
||||||| merged common ancestor
EmailMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

In primo luogo esaminare l'antenato comune unito. Quindi confronta ogni lato per determinare l'intento di ogni ramo. Puoi vedere che HEAD ha cambiato EmailMessage in TextMessage. Il suo scopo è quello di cambiare la classe utilizzata in TextMessage, passando gli stessi parametri. Puoi anche vedere che l'intento di quella feature-branch è di passare false invece di true per l'opzione: include_timestamp. Per unire queste modifiche, combina l'intento di entrambi:

TextMessage.send(:include_timestamp => false)

In generale:

  1. Confronta l'antenato comune con ciascun ramo e determina quale ramo ha il cambiamento più semplice
  2. Applica quella semplice modifica alla versione del codice dell'altro ramo, in modo che contenga sia la modifica più semplice che quella più complessa
  3. Rimuovi tutte le sezioni del codice conflitto diverse da quella in cui sono state unite le modifiche

Alternativo: Risolvi applicando manualmente le modifiche del ramo

Infine, alcuni conflitti sono terribili da capire anche con diff3. Questo accade specialmente quando diff trova linee comuni che non sono semanticamente comuni (ad esempio, entrambi i rami hanno una linea vuota nello stesso posto!). Ad esempio, un ramo cambia il rientro del corpo di una classe o riordina metodi simili. In questi casi, una strategia di risoluzione migliore può essere quella di esaminare la modifica da entrambi i lati dell'unione e applicare manualmente la differenza sull'altro file.

Diamo un'occhiata a come possiamo risolvere un conflitto in uno scenario in cui l' origin/feature1 fusione in cui lib/message.rb conflitto.

  1. Decidi se il nostro branch attualmente estratto ( HEAD , o --ours ) o il ramo che stiamo unendo ( origin/feature1 , o --theirs ) è una modifica più semplice da applicare. Usando diff con triplo punto ( git diff a...b ) mostra i cambiamenti avvenuti su b dalla sua ultima divergenza da a , o in altre parole, confronta l'antenato comune di aeb con b.

    git diff HEAD...origin/feature1 -- lib/message.rb # show the change in feature1
    git diff origin/feature1...HEAD -- lib/message.rb # show the change in our branch
    
  2. Dai un'occhiata alla versione più complicata del file. Questo rimuoverà tutti i segnalini di conflitto e userà il lato che scegli.

    git checkout --ours -- lib/message.rb   # if our branch's change is more complicated
    git checkout --theirs -- lib/message.rb # if origin/feature1's change is more complicated
    
  3. Con il complicato cambiamento estratto, richiama il differenziale del cambiamento più semplice (vedi il punto 1). Applica ogni modifica da questo diff al file in conflitto.


Ho una domanda dopo aver letto la tua spiegazione: potrebbe essere che non hai mai fatto un

git checkout master
git pull origin
git checkout my_new_feature

prima di fare il 'git rebase / merge master' nel tuo branch feature?

Perché il tuo ramo principale non si aggiornerà automaticamente dal repository del tuo amico. Devi farlo con l' git pull origin . Cioè potresti sempre ribattere da un ramo master locale in continua evoluzione? E poi arriva il momento di spingere, stai spingendo in un deposito che ha (locale) commette non hai mai visto e quindi la spinta fallisce.


Ad ogni modo, stavo seguendo il mio flusso di lavoro su un ramo recente, e quando ho provato a fonderlo di nuovo per diventare master, tutto è andato al diavolo. C'erano tonnellate di conflitti con cose che non avrebbero dovuto importare. I conflitti non avevano senso per me. Mi ci è voluto un giorno per risolvere tutto, e alla fine è culminato in una spinta forzata al master remoto, dal momento che il mio maestro locale ha risolto tutti i conflitti, ma quello remoto non era ancora felice.

In nessuno dei flussi di lavoro suggeriti né dal tuo partner né dal tuo partner dovresti aver riscontrato conflitti che non avevano senso. Anche se lo avessi, se stai seguendo i flussi di lavoro suggeriti, dopo la risoluzione non dovrebbe essere richiesta una spinta "forzata". Suggerisce di non aver effettivamente unito il ramo a cui stavi spingendo, ma di aver dovuto premere un ramo che non era un discendente del suggerimento remoto.

Penso che tu debba guardare attentamente a quello che è successo. Qualcuno potrebbe aver (deliberatamente o meno) riavvolto il ramo master remoto tra la creazione del ramo locale e il punto in cui si è tentato di unirlo nuovamente al ramo locale?

Rispetto a molti altri sistemi di controllo delle versioni, ho scoperto che l'utilizzo di Git comporta una riduzione dello strumento e consente di lavorare sui problemi fondamentali per i flussi di origine. Git non esegue la magia, quindi le modifiche conflittuali causano conflitti, ma dovrebbe rendere facile fare la cosa di scrittura con il monitoraggio della parentela di commit.


Nel mio flusso di lavoro, ho rebase il più possibile (e provo a farlo spesso. Non lasciare che le discrepanze si accumulino drasticamente riduce la quantità e la gravità delle collisioni tra i rami).

Tuttavia, anche in un flusso di lavoro basato principalmente su rebase, esiste un posto per le unioni.

Ricorda che l'unione crea effettivamente un nodo che ha due genitori. Consideriamo ora la seguente situazione: ho due funzioni indipendenti A e B, e ora voglio sviluppare cose sul ramo di funzionalità C che dipende da A e B, mentre A e B vengono revisionati.

Quello che faccio allora è il seguente:

  1. Creare (e verificare) il ramo C in cima a A.
  2. Uniscilo con B

Ora il ramo C include i cambiamenti da A e B, e posso continuare a svilupparlo. Se faccio qualche modifica ad A, ricostruisco il grafico dei rami nel seguente modo:

  1. crea il ramo T sulla nuova cima di A
  2. unire T con B
  3. rebase C su T
  4. elimina il ramo T

In questo modo posso effettivamente mantenere grafici arbitrari di rami, ma fare qualcosa di più complesso rispetto alla situazione descritta sopra è già troppo complesso, dato che non c'è uno strumento automatico per effettuare il rebasing quando il genitore cambia.


"Conflitti" significa "evoluzioni parallele di uno stesso contenuto". Quindi se va "tutto al diavolo" durante un'unione, significa che hai enormi evoluzioni sullo stesso set di file.

Il motivo per cui un rebase è quindi migliore di un'unione è che:

  • riscrivi la tua cronologia di commit locale con quella del master (e poi riapplica il tuo lavoro, risolvendo quindi eventuali conflitti)
  • l'unione finale sarà sicuramente una "avanti veloce", perché avrà tutta la cronologia di commit del master, oltre alle sole modifiche apportate per riapplicare.

Confermo che il flusso di lavoro corretto in quel caso (evoluzione sul set comune di file) è prima rebase, quindi unire .

Tuttavia, ciò significa che, se si spinge il ramo locale (per ragioni di backup), quel ramo non deve essere tirato (o almeno utilizzato) da nessun altro (poiché la cronologia del commit verrà riscritta dal rebase successivo).

Su quell'argomento (rebase quindi unisci flusso di lavoro), barraponto menziona nei commenti due post interessanti, entrambi da randyfay.com :

Usando questa tecnica, il tuo lavoro va sempre in cima alla sezione pubblica come una patch aggiornata con l'attuale HEAD .

( esiste una tecnica simile per il bazar )


Da quanto ho osservato, git merge tende a tenere separati i rami anche dopo la fusione, mentre il rebase e quindi l'unione si combinano in un unico ramo. Quest'ultimo risulta molto più pulito, mentre nel primo caso, sarebbe più facile scoprire quali commit appartengono a quel ramo anche dopo la fusione.


  1. rimuovere l'origine usando il comando su gitbash git remote rm origin
  2. E ora aggiungi nuovi Origin usando gitbash git remote add origin (Copia URL HTTP dal tuo repository di progetto in bit bucket) fatto




git version-control git-merge git-rebase