L'affermazione del perché la fusione sia migliore in un DVCS che in Subversion era in gran parte basata sul modo in cui la ramificazione e l'unione funzionavano in Subversion qualche tempo fa. Subversion prima della 1.5.0 non memorizzava alcuna informazione su quando le filiali fossero unite, quindi quando si voleva unire si doveva specificare quale intervallo di revisioni che dovevano essere unite.
Quindi perché le fusioni di Subversion fanno schifo ?
Rifletti su questo esempio:
1 2 4 6 8
trunk o-->o-->o---->o---->o
\
\ 3 5 7
b1 +->o---->o---->o
Quando vogliamo unire le modifiche di b1 nel trunk, emettiamo il seguente comando, mentre ci troviamo su una cartella con il trunk estratto:
svn merge -r 2:7 {link to branch b1}
... che tenterà di unire le modifiche b1
nella directory di lavoro locale. E quindi esegui il commit delle modifiche dopo aver risolto eventuali conflitti e verificato il risultato. Quando esegui il commit, l'albero della revisione sarà simile al seguente:
1 2 4 6 8 9
trunk o-->o-->o---->o---->o-->o "the merge commit is at r9"
\
\ 3 5 7
b1 +->o---->o---->o
Tuttavia, questo modo di specificare intervalli di revisioni sfugge rapidamente alla mano quando l'albero delle versioni cresce poiché la sovversione non aveva metadati su quando e quali revisioni si sono fuse insieme. Rifletti su cosa succede dopo:
12 14
trunk …-->o-------->o
"Okay, so when did we merge last time?"
13 15
b1 …----->o-------->o
Questo è in gran parte un problema del design del repository che Subversion ha, al fine di creare un ramo è necessario creare una nuova directory virtuale nel repository che ospiterà una copia del trunk ma non memorizza alcuna informazione su quando e cosa le cose sono state ricongiunte. Ciò a volte porterà a cattivi conflitti di fusione. Ciò che è stato anche peggio è che Subversion ha usato la fusione a due vie per impostazione predefinita, che ha alcune limitazioni paralizzanti nella fusione automatica quando due teste di ramo non vengono confrontate con il loro antenato comune.
Per mitigare questo Subversion ora memorizza i metadati per diramazione e unione. Ciò risolverebbe tutti i problemi, giusto?
E oh, a proposito, Subversion fa ancora schifo ...
Su un sistema centralizzato, come la sovversione, le directory virtuali fanno schifo. Perché? Perché tutti hanno accesso per vederli ... anche quelli spazzatura sperimentali. La ramificazione è buona se vuoi sperimentare ma non vuoi vedere la sperimentazione di tutti e delle loro zie . Questo è un grave rumore cognitivo. Più rami aggiungi, più schifezze vedrai.
Più filiali pubbliche hai in un repository, più difficile sarà tenere traccia di tutte le diverse filiali. Quindi la domanda che avrai è se il ramo è ancora in sviluppo o se è davvero morto, il che è difficile da dire in qualsiasi sistema di controllo versione centralizzato.
La maggior parte delle volte, da quello che ho visto, un'organizzazione utilizzerà comunque un ramo grande per impostazione predefinita. Il che è un peccato perché a sua volta sarà difficile tenere traccia dei test e delle versioni di rilascio, e tutto il resto viene dalla ramificazione.
Allora perché i DVCS, come Git, Mercurial e Bazaar, sono meglio di Subversion per ramificarsi e fondersi?
C'è una ragione molto semplice per cui: la ramificazione è un concetto di prima classe . Non ci sono directory virtuali in base alla progettazione e i rami sono oggetti duri in DVCS che deve essere tale per funzionare semplicemente con la sincronizzazione dei repository (ovvero push and pull ).
La prima cosa che fai quando lavori con un DVCS è clonare i repository (git's clone
, hg's clone
e bzr's branch
). La clonazione è concettualmente la stessa cosa della creazione di un ramo nel controllo versione. Alcuni lo chiamano biforcazione o ramificazione (sebbene quest'ultimo sia spesso usato anche per riferirsi a filiali localizzate), ma è la stessa cosa. Ogni utente esegue il proprio repository, il che significa che hai una diramazione per utente in corso.
La struttura della versione non è un albero , ma piuttosto un grafico . Più specificamente un grafico aciclico diretto (DAG, che significa un grafico che non ha alcun ciclo). In realtà non è necessario soffermarsi sulle specifiche di un DAG diverso da ogni commit con uno o più riferimenti principali (su cui si basava il commit). Quindi i seguenti grafici mostreranno le frecce tra le revisioni al contrario per questo motivo.
Un esempio molto semplice di fusione sarebbe questo; immagina un repository centrale chiamato origin
e un utente, Alice, che clona il repository sulla sua macchina.
a… b… c…
origin o<---o<---o
^master
|
| clone
v
a… b… c…
alice o<---o<---o
^master
^origin/master
Ciò che accade durante un clone è che ogni revisione è copiata su Alice esattamente come erano (che è validata dagli hash-id identificabili in modo univoco) e segna dove si trovano i rami dell'origine.
Alice lavora quindi sul suo repository, impegnandosi nel suo repository e decide di spingere le sue modifiche:
a… b… c…
origin o<---o<---o
^ master
"what'll happen after a push?"
a… b… c… d… e…
alice o<---o<---o<---o<---o
^master
^origin/master
La soluzione è piuttosto semplice, l'unica cosa che il origin
repository deve fare è prendere tutte le nuove revisioni e spostare il suo ramo sulla revisione più recente (che git chiama "avanzamento rapido"):
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
a… b… c… d… e…
alice o<---o<---o<---o<---o
^master
^origin/master
Il caso d'uso, che ho illustrato sopra, non ha nemmeno bisogno di unire nulla . Quindi il problema non riguarda gli algoritmi di fusione poiché l'algoritmo di fusione a tre vie è praticamente lo stesso tra tutti i sistemi di controllo della versione. Il problema riguarda più la struttura che altro .
Che ne dici di mostrarmi un esempio che ha una vera unione?
Certamente l'esempio sopra è un caso d'uso molto semplice, quindi facciamolo molto più contorto sebbene più comune. Ricordi che è origin
iniziato con tre revisioni? Bene, il ragazzo che li ha fatti, lo chiama Bob , ha lavorato da solo e si è impegnato sul suo repository:
a… b… c… f…
bob o<---o<---o<---o
^ master
^ origin/master
"can Bob push his changes?"
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
Ora Bob non può inviare le sue modifiche direttamente al origin
repository. Il modo in cui il sistema lo rileva è verificando se le revisioni di Bob discendono direttamente da quelle origin
, cosa che in questo caso non accade. Qualsiasi tentativo di spingere si tradurrà nel sistema dicendo qualcosa di simile a " Uh ... temo di non poterti permettere Bob ."
Quindi Bob deve fare il pull-in e quindi unire le modifiche (con git's pull
; o hg's pull
e merge
; o bzr's merge
). Questo è un processo in due fasi. Prima Bob deve recuperare le nuove revisioni, che le copieranno così come sono dal origin
repository. Ora possiamo vedere che il grafico differisce:
v master
a… b… c… f…
bob o<---o<---o<---o
^
| d… e…
+----o<---o
^ origin/master
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
Il secondo passo del processo pull è quello di unire i suggerimenti divergenti e fare un commit del risultato:
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
^ origin/master
Si spera che l'unione non si scontri con conflitti (se li anticipi puoi fare i due passaggi manualmente in git con fetch
e merge
). Ciò che in seguito deve essere fatto è inserire nuovamente tali modifiche origin
, che si tradurranno in un'unione di avanzamento rapido poiché il commit di unione è un discendente diretto dell'ultimo nel origin
repository:
v origin/master
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
v master
a… b… c… f… 1…
origin o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
C'è un'altra opzione per unire in git e hg, chiamata rebase , che sposterà le modifiche di Bob dopo le ultime modifiche. Dal momento che non voglio che questa risposta sia più prolissa, ti lascio invece leggere i documenti git , mercurial o bazar su questo.
Come esercizio per il lettore, prova a capire come funzionerà con un altro utente coinvolto. Allo stesso modo viene fatto come nell'esempio sopra con Bob. La fusione tra i repository è più semplice di quanto si pensi, poiché tutte le revisioni / commit sono identificabili in modo univoco.
C'è anche il problema di inviare patch tra ogni sviluppatore, che era un grosso problema in Subversion che è mitigato in git, hg e bzr da revisioni identificabili in modo univoco. Una volta che qualcuno ha unito le sue modifiche (ovvero ha effettuato un commit di merge) e lo ha inviato a tutti gli altri membri del team, spingendolo verso un repository centrale o inviando patch, non devono preoccuparsi della fusione, perché è già successo . Martin Fowler chiama questo modo di lavorare l' integrazione promiscua .
Poiché la struttura è diversa da Subversion, impiegando invece un DAG, consente la ramificazione e l'unione in modo più semplice non solo per il sistema ma anche per l'utente.