Come e / o perché si fonde in Git meglio che in SVN?

In alcuni punti ho sentito che uno dei motivi principali per cui i sistemi di controllo delle versioni distribuiti brillano, è molto più efficace la fusione rispetto agli strumenti tradizionali come SVN. Questo in realtà è dovuto a differenze intrinseche nel modo in cui funzionano i due sistemi, oppure le specifiche implementazioni DVCS come Git / Mercurial hanno solo algoritmi di fusione più intelligenti di SVN?

L’affermazione del perché la fusione è migliore in un DVCS che in Subversion era in gran parte basata su come branching e merge hanno funzionato in Subversion qualche tempo fa. Subversion precedente alla 1.5.0 non memorizzava alcuna informazione su quando i rami venivano uniti, quindi quando volevi unire dovevi specificare quale gamma di revisioni dovevi unire.

Allora perché Subversion si fonde succhiare ?

Medita 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, emetteremo il seguente comando, stando in piedi su una cartella con il trunk estratto:

 svn merge -r 2:7 {link to branch b1} 

… che tenterà di unire le modifiche da b1 alla directory di lavoro locale. E poi commetti le modifiche dopo aver risolto eventuali conflitti e verificato il risultato. Quando si commette l’albero di revisione dovrebbe assomigliare a questo:

  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 diventa rapidamente inutile quando l’albero delle versioni cresce man mano che subversion non ha avuto metadati su quando e quali revisioni sono state unite. Rifletti su cosa succederà 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 dal design del repository che Subversion ha, per creare un ramo è necessario creare una nuova directory virtuale nel repository che ospiterà una copia del trunk ma non memorizzerà alcuna informazione su quando e cosa le cose si sono unite di nuovo. Ciò potrebbe portare a conflitti di fusione sgradevoli a volte. Ciò che è stato ancora peggio è che Subversion ha utilizzato la fusione bidirezionale per impostazione predefinita, che ha alcune limitazioni paralizzanti nella fusione automatica quando le due diramazioni non sono confrontate con il loro antenato comune.

Per mitigare questa Subversion ora memorizza i metadati per il branch e l’unione. Questo risolverebbe tutti i problemi, giusto?

E, a proposito, Subversion fa ancora schifo …

Su un sistema centralizzato, come subversion, le directory virtuali fanno schifo. Perché? Perché tutti hanno accesso per vederli … anche quelli della spazzatura sperimentale. Branching è buono se vuoi sperimentare ma non vuoi vedere la sperimentazione di tutti e di loro . Questo è un rumore cognitivo serio. Più rami aggiungi, più merda vedrai.

Più filiali pubbliche hai in un repository più difficile sarà tenere traccia di tutti i diversi rami. Quindi la domanda che avrai sarà se il ramo è ancora in sviluppo o se è veramente morto, cosa difficile da dire in qualsiasi sistema di controllo di versione centralizzato.

Il più delle volte, da quello che ho visto, un’organizzazione utilizzerà di default una sola grande branca. Il che è un peccato perché a sua volta sarà difficile tenere traccia delle versioni di test e di rilascio e qualsiasi altra cosa derivante dalla ramificazione.

Quindi, perché DVCS, come Git, Mercurial e Bazaar, sono migliori di Subversion nella ramificazione e fusione?

C’è una ragione molto semplice per cui: la ramificazione è un concetto di prima class . Non ci sono directory virtuali per design e le filiali sono oggetti duri in DVCS che devono essere tali per poter lavorare semplicemente con la sincronizzazione dei repository (cioè push e pull ).

La prima cosa che fai quando lavori con un DVCS è clonare i repository ( clone di git, clone hg e branch di bzr). La clonazione è concettualmente la stessa cosa della creazione di un ramo nel controllo della versione. Alcuni chiamano questo biforcarsi o ramificarsi (anche se quest’ultimo è spesso usato anche per riferirsi ai rami localizzati), ma è solo la stessa cosa. Ogni utente esegue il proprio repository, il che significa che è in corso una ramificazione per utente .

La struttura della versione non è un albero , ma piuttosto un grafico . Più specificamente un grafo aciclico diretto (DAG, che significa un grafico che non ha cicli). In realtà, non è necessario soffermarsi sulle specifiche di un DAG diverso da ogni commit con uno o più riferimenti parent (su cui si basava il commit). Quindi i seguenti grafici mostreranno le frecce tra le revisioni al contrario a causa di questo.

Un esempio molto semplice di fusione sarebbe questo; immagina un repository centrale chiamato origin e un utente, Alice, clonando 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 

Quello che succede durante un clone è che ogni revisione viene copiata in Alice esattamente come erano (che è convalidata dagli hash-id identificabili in modo univoco), e segna dove sono i rami di origine.

Alice quindi lavora al suo repository, impegnandosi nel proprio repository e decide di spingere i suoi cambiamenti:

  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 repository di origin deve fare è prendere tutte le nuove revisioni e spostare il ramo verso la revisione più recente (che git chiama "avanti veloce"):

  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 in realtà non è con gli algoritmi di fusione poiché l'algoritmo di fusione a tre vie è praticamente lo stesso tra tutti i sistemi di controllo di versione. Il problema riguarda più la struttura che altro .

Che ne dici di mostrarmi un esempio che ha una vera unione?

Ammetto che l'esempio sopra è un caso d'uso molto semplice, quindi facciamo uno molto più contorto, anche se più comune. Ricorda che l' origin iniziata con tre revisioni? Bene, il ragazzo che li ha fatti, lascia chiamarlo Bob , ha lavorato da solo e ha fatto un commit sul proprio 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ò trasferire le sue modifiche direttamente al repository di origin . Il modo in cui il sistema rileva questo è controllando se le revisioni di Bob discendono direttamente origin , cosa che in questo caso non avviene. Qualsiasi tentativo di spingere si tradurrà nel sistema dicendo qualcosa di simile a " Uh ... temo che non posso lasciarti fare quel Bob ".

Quindi Bob deve eseguire il pull-in e quindi unire le modifiche (con git's pull o hg's pull and merge o l' merge di bzr). Questo è un processo in due fasi. Prima Bob deve recuperare le nuove revisioni, che verranno copiate così come sono dal repository di origin . Ora possiamo vedere che il grafico diverge:

  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 di 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 

Speriamo che l'unione non si verifichi in conflitti (se li anticipi puoi fare manualmente i due passaggi in git con fetch e merge ). Quello che più avanti deve essere fatto è inserire nuovamente tali modifiche origin , il che si tradurrà in un'unione di avanzamento rapido poiché il commit di unione è un discendente diretto dell'ultimo nel repository di origin :

  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 sposta le modifiche di Bob in dopo le ultime modifiche. Dal momento che non voglio che questa risposta sia più verbosa, ti permetterò di leggere i documenti di git , mercurial o bazaar a riguardo.

Come esercizio per il lettore, prova a capire come funzionerà con un altro utente coinvolto. È similmente fatto come nell'esempio sopra con Bob. L'unione tra repository è più semplice di quanto si possa pensare poiché tutte le revisioni / commit sono identificabili in modo univoco.

C'è anche il problema di inviare patch tra ogni sviluppatore, che è stato un grosso problema in Subversion che viene mitigato in git, hg e bzr da revisioni identificabili in modo univoco. Una volta che qualcuno ha unito le sue modifiche (cioè fatto un commit di fusione) e lo invia a tutti gli altri membri del team da consumare spingendo verso un repository centrale o inviando patch, allora non devono preoccuparsi della fusione, perché è già successo . Martin Fowler chiama questo modo di lavorare all'integrazione promiscua .

Poiché la struttura è diversa da Subversion, utilizzando invece un DAG, consente di eseguire ramificazioni e unioni in modo più semplice non solo per il sistema ma anche per l'utente.

Storicamente, Subversion è stato in grado di eseguire un’unione bidirezionale diretta perché non memorizzava alcuna informazione di unione. Ciò comporta l’adozione di una serie di modifiche e l’applicazione a un albero. Anche con le informazioni di unione, questa è ancora la strategia di unione più comunemente utilizzata.

Git utilizza un algoritmo di fusione a 3 vie per impostazione predefinita, che consiste nel trovare un antenato comune alle teste che vengono unite e che fa uso della conoscenza esistente su entrambi i lati dell’unione. Ciò consente a Git di essere più intelligente nell’evitare i conflitti.

Git ha anche qualche sofisticato codice per la ricerca della rinomina, che aiuta anche. Non memorizza i changeset o memorizza le informazioni di tracciamento: memorizza semplicemente lo stato dei file a ogni commit e utilizza l’euristica per individuare i nomi e i movimenti del codice come richiesto (la memoria su disco è più complicata di così, ma l’interfaccia presenta al livello logico non mostra tracciamento).

In parole semplici, l’implementazione di merge viene eseguita meglio in Git che in SVN . Prima dell’1,5, SVN non registrava un’azione di unione, quindi non era in grado di eseguire future fusioni senza l’aiuto dell’utente che aveva bisogno di fornire informazioni che SVN non registrava. Con 1.5 è migliorato, e in effetti il ​​modello di archiviazione SVN è leggermente più capace di DAG di Git. Ma SVN ha memorizzato le informazioni di fusione in una forma piuttosto complicata che consente alle unioni di ottenere un tempo maggiore rispetto a Git: ho osservato fattori di 300 in termini di tempo di esecuzione.

Inoltre, SVN sostiene di rintracciare i nomi dei nomi per facilitare l’unione dei file spostati. Ma in realtà li memorizza ancora come una copia e un’azione di eliminazione separata, e l’algoritmo di fusione incappa ancora su di loro in situazioni di modifica / rinomina, cioè, dove un file viene modificato su un ramo e rinominato sull’altro, e quei rami sono da unire. Tali situazioni produrranno ancora conflitti di fusione spuri e, nel caso di ridenominazioni di directory, causerà persino la perdita silenziosa delle modifiche. (Le persone SVN tendono quindi a sottolineare che le modifiche sono ancora nella storia, ma questo non aiuta molto quando non sono in un risultato di fusione dove dovrebbero apparire.

Git, d’altra parte, non rintraccia nemmeno i nomi, ma li calcola dopo il fatto (al momento della fusione), e lo fa piuttosto magicamente.

Anche la rappresentazione di unione SVN presenta problemi; in 1.5 / 1.6 si poteva unire da un tronco all’altro tutte le volte che gli piaceva, automaticamente, ma era necessario annunciare --reintegrate nell’altra direzione ( --reintegrate ) e lasciare il ramo in uno stato inutilizzabile. Molto più tardi hanno scoperto che questo in realtà non è il caso, e che a) il – --reintegrate può essere capito automaticamente, e b) sono possibili fusioni ripetute in entrambe le direzioni.

Ma dopo tutto questo (che IMHO mostra una mancanza di comprensione di ciò che stanno facendo), sarei (OK, sono) molto cauto nell’usare SVN in qualsiasi scenario di ramificazione non banale, e idealmente proverei a vedere cosa pensa Git di il risultato dell’unione.

Altri punti fatti nelle risposte, come la visibilità globale forzata delle filiali in SVN, non sono rilevanti per unire le capacità (ma per l’usabilità). Inoltre, la ‘Git memorizza le modifiche mentre gli archivi SVN (qualcosa di diverso)’ sono per lo più off-point. Git memorizza concettualmente ogni commit come un albero separato (come un file tar ), e quindi usa un bel po ‘di euristica per archiviarlo in modo efficiente. Il calcolo delle modifiche tra due commit è separato dall’implementazione dello storage. Ciò che è vero è che Git memorizza il DAG storico in una forma molto più semplice che SVN fa il mergeinfo. Chiunque tenti di capire il secondo capirà cosa intendo.

In breve: Git utilizza un modello di dati molto più semplice per archiviare le revisioni di SVN, e quindi potrebbe mettere molta energia negli algoritmi di fusione effettivi piuttosto che cercare di far fronte alla rappresentazione => fusione praticamente migliore.

Ho letto la risposta accettata. È semplicemente sbagliato.

La fusione di SVN può essere un dolore e può anche essere ingombrante. Ma, ignorare come funziona in realtà per un minuto. Non ci sono informazioni che Git tenga o possa dedurre che SVN non conserva o può derivare. Ancora più importante, non vi è alcuna ragione per cui la conservazione di copie separate (a volte parziali) del sistema di controllo della versione fornirà più informazioni effettive. Le due strutture sono completamente equivalenti.

Supponiamo che tu voglia fare “qualcosa di intelligente” Git è “meglio a”. E la tua cosa è controllata in SVN.

Converti il ​​tuo SVN nel modulo Git equivalente, fallo in Git, quindi controlla il risultato, magari usando più commit, alcuni rami extra. Se riesci a immaginare un modo automatico per trasformare un problema SVN in un problema Git, Git non ha alcun vantaggio fondamentale.

Alla fine della giornata, qualsiasi sistema di controllo della versione me lo permetterà

 1. Generate a set of objects at a given branch/revision. 2. Provide the difference between a parent child branch/revisions. 

Inoltre, per la fusione è anche utile (o critico) sapere

 3. The set of changes have been merged into a given branch/revision. 

Mercurial , Git e Subversion (ora nativamente, che in precedenza utilizzava svnmerge.py) possono fornire tutte e tre le informazioni. Per dimostrare qualcosa di fondamentalmente migliore con DVC, si prega di indicare una quarta informazione disponibile in Git / Mercurial / DVC non disponibile in SVN / VC centralizzato.

Questo non vuol dire che non siano strumenti migliori!

Una cosa che non è stata menzionata nelle altre risposte, e che è davvero un grande vantaggio di un DVCS, è che è ansible eseguire il commit localmente prima di inviare le modifiche. In SVN, quando ho avuto qualche cambiamento volevo controllare, e qualcuno aveva già fatto un commit sullo stesso ramo nel frattempo, questo significava che dovevo fare un svn update prima che potessi commettere. Ciò significa che i miei cambiamenti e le modifiche dell’altro utente sono ora mescolati insieme e non c’è modo di annullare l’unione (come con git reset o hg update -C ), perché non ci sono commit a cui tornare. Se l’unione non è banale, significa che non è ansible continuare a lavorare sulla funzionalità prima di aver eliminato il risultato della fusione.

Ma poi, forse questo è solo un vantaggio per le persone che sono troppo stupide per utilizzare filiali separate (se ricordo bene, avevamo solo un ramo che è stato utilizzato per lo sviluppo di nuovo nella società in cui ho usato SVN).

SVN tiene traccia dei file mentre Git tiene traccia delle modifiche del contenuto . È abbastanza intelligente per tracciare un blocco di codice che è stato rifattorizzato da una class / file a un’altra. Usano due approcci completamente diversi per rintracciare la tua fonte.

Uso ancora SVN pesantemente, ma sono molto soddisfatto delle poche volte che ho usato Git.

Una bella lettura se hai tempo: perché ho scelto Git

Basta leggere un articolo sul blog di Joel (purtroppo il suo ultimo). Questo parla di Mercurial, ma in realtà parla dei vantaggi dei sistemi VC distribuiti come Git.

Con il controllo della versione distribuita, la parte distribuita non è in realtà la parte più interessante. La parte interessante è che questi sistemi pensano in termini di modifiche, non in termini di versioni.

Leggi l’articolo qui .