Unione: Hg / Git vs. SVN

Ho letto spesso che Hg (e Git e …) sono migliori per l’unione di SVN, ma non ho mai visto esempi pratici di dove Hg / Git possa unire qualcosa in cui SVN fallisce (o dove SVN necessita di intervento manuale). Potresti pubblicare alcuni elenchi passo-passo delle operazioni branch / modify / commit /…- che mostrano dove SVN fallirebbe mentre Hg / Git si muove felicemente? Casi pratici, non estremamente eccezionali, per favore …

Qualche retroscena: abbiamo alcune decine di sviluppatori che lavorano su progetti usando SVN, con ogni progetto (o gruppo di progetti simili) nel proprio repository. Sappiamo come applicare release e feature branch in modo da non incorrere in problemi molto spesso (ad esempio, siamo stati lì, ma abbiamo imparato a superare i problemi di Joel di “un programmatore che causa un trauma a tutta la squadra” o “aver bisogno di sei sviluppatori per due settimane per reintegrare un ramo”). Abbiamo rami di rilascio che sono molto stabili e usati solo per applicare correzioni di bug. Abbiamo tronchi che dovrebbero essere abbastanza stabili da essere in grado di creare un rilascio entro una settimana. E abbiamo feature-branch su cui singoli sviluppatori o gruppi di sviluppatori possono lavorare. Sì, vengono cancellati dopo il reinserimento in modo che non ingombrino il repository. 😉

Quindi sto ancora cercando di trovare i vantaggi di Hg / Git su SVN. Mi piacerebbe avere qualche esperienza pratica, ma non ci sono progetti più grandi che potremmo passare a Hg / Git, quindi sono bloccato a giocare con piccoli progetti artificiali che contengono solo alcuni file creati. E sto cercando alcuni casi in cui puoi percepire l’impressionante potere di Hg / Git, dal momento che finora ho spesso letto su di loro ma non sono riuscito a trovarli da solo.

Non uso me stesso Subversion, ma dalle note di rilascio per Subversion 1.5: Merge tracking (foundation) sembra che ci siano le seguenti differenze da come funzionano i controlli di merge nei sistemi di controllo di versione DAG completi come Git o Mercurial.

  • L’unione di un ramo in un ramo è diversa dall’unione di un ramo in un altro: per qualche motivo l’unione di un ramo in un ramo richiede --reintegrate opzione --reintegrate a svn merge .

    Nei sistemi di controllo delle versioni distribuite come Git o Mercurial non vi è alcuna differenza tecnica tra tronco e ramo: tutti i rami sono creati uguali (potrebbe esserci una differenza sociale , comunque). L’unione in entrambe le direzioni avviene allo stesso modo.

  • È necessario fornire la nuova -g ( --use-merge-history ) a svn log e svn blame per tenere conto del tracciamento di merge.

    In Git e Mercurial il tracciamento unione viene automaticamente preso in considerazione durante la visualizzazione della cronologia (registro) e la colpa. In Git puoi richiedere di seguire il primo genitore solo con --first-parent (suppongo che esista un’opzione simile anche per Mercurial) per “scartare” unire le informazioni di tracciamento nel git log .

  • Da quanto ho capito svn:mergeinfo memorizza le informazioni per-path sui conflitti (Subversion è basato su changeset), mentre in Git e Mercurial si tratta semplicemente di oggetti commit che possono avere più di un genitore.

  • La sottosezione “Problemi noti” per il tracciamento unione in Subversion suggerisce che la fusione ripetuta / ciclica / riflessiva potrebbe non funzionare correttamente. Significa che con le seguenti storie la seconda fusione potrebbe non fare la cosa giusta (‘A’ può essere tronco o ramo, e ‘B’ può essere ramo o tronco, rispettivamente):

     * --- * --- x --- * --- y --- * --- * --- * --- M2 <- A
              \ \ /
               - * ---- M1 --- * --- * --- / <- B
    

    Nel caso in cui la suddetta arte ASCII si interrompa: il ramo "B" viene creato (biforcuto) dal ramo "A" alla revisione "x", quindi il ramo successivo "A" viene unito alla revisione "y" nel ramo "B" come unire 'M1' e infine il ramo 'B' viene unito al ramo 'A' come unione 'M2'.

     * --- * --- x --- * ----- M1 - * --- * --- M2 <- A
              \ / / 
               \ - * --- y --- * --- * --- / <- B
    

    Nel caso in cui la suddetta arte ASCII venga interrotta: Il ramo "B" viene creato (biforcuto) dal ramo "A" alla revisione "x", viene fuso nel ramo "A" in "y" come "M1" e in seguito riunito di nuovo nel ramo "A" come "M2".

  • Subversion potrebbe non supportare il caso avanzato di unione incrociata .

     * --- b ----- B1 - M1 - * --- M3
          \ \ / /
           \ X /
            \ / \ /
             \ - B2 - M2 - *
    

    Git gestisce questa situazione in pratica usando la strategia di unione "ricorsiva". Non sono sicuro di Mercurial.

  • In "Problemi noti" si avverte che il tracciamento dell'unione non può funzionare con la rinomina dei file, ad esempio quando un lato rinomina il file (e forse lo modifica) e il secondo lato modifica il file senza ridenominarlo (con il vecchio nome).

    Sia Git che Mercurial gestiscono correttamente questo caso: Git usando il rilevamento del rename , Mercurial usando il rinominare il tracking .

HTH

Anch’io ho cercato un caso in cui, ad esempio, Subversion non riesce a fondere un ramo e Mercurial (e Git, Bazaar, …) fa la cosa giusta.

Il libro SVN descrive come i file rinominati vengono uniti in modo errato . Questo vale per Subversion 1.5 , 1.6 , 1.7 e 1.8 ! Ho provato a ricreare la situazione di seguito:

 cd / tmp
 rm -rf svn-repo svn-checkout
 svnadmin crea svn-repo
 svn checkout file: /// tmp / svn-repo svn-checkout
 cd svn-checkout
 rami di tronco mkdir
 echo "Arrivederci, Mondo!"  > trunk / hello.txt
 svn aggiungi rami di tronco
 svn commit -m 'Importazione iniziale.'
 svn copia '^ / trunk' '^ / branches / rename' -m 'Crea ramo'.
 interruttore svn '^ / trunk'.
 echo 'Ciao, Mondo!'  > ciao.txt
 svn commit -m 'Aggiornamento sul trunk.'
 svn passa a '^ / branches / rinomina'.
 svn rinomina ciao.txt ciao.en.txt
 svn commit -m 'Rinomina sul ramo.'
 interruttore svn '^ / trunk'.
 svn merge --reintegrate '^ / branches / rinomina'

Secondo il libro, l’unione dovrebbe terminare in modo pulito, ma con dati errati nel file rinominato poiché l’aggiornamento sul trunk è stato dimenticato. Invece ottengo un conflitto ad albero (questo è con Subversion 1.6.17, la versione più recente in Debian al momento della scrittura):

 --- Unione di differenze tra gli URL del repository in ".":
 Un ciao.en.txt
    C ciao.txt
 Riepilogo dei conflitti:
   Conflitti ad albero: 1

Non dovrebbe esserci alcun conflitto – l’aggiornamento dovrebbe essere unito al nuovo nome del file. Mentre Subversion fallisce, Mercurial lo gestisce correttamente:

 rm -rf /tmp/hg-repo hg init /tmp/hg-repo cd /tmp/hg-repo echo 'Goodbye, World!' > hello.txt hg add hello.txt hg commit -m 'Initial import.' echo 'Hello, World!' > hello.txt hg commit -m 'Update.' hg update 0 hg rename hello.txt hello.en.txt hg commit -m 'Rename.' hg merge 

Prima dell’unione, il repository appare come questo (da hg glog ):

 @ changeset: 2: 6502899164cc
 |  tag: tip
 |  genitore: 0: d08bcebadd9e
 |  utente: Martin Geisler 
 |  data: Gio Apr 01, 12:29:19 2010 +0200
 |  riepilogo: rinomina.
 |
 |  o changeset: 1: 9d06fa155634
 | / utente: Martin Geisler 
 |  data: Gio Apr 01, 12:29:18 2010 +0200
 |  riepilogo: aggiornamento.
 |
 o changeset: 0: d08bcebadd9e
    utente: Martin Geisler 
    data: Gio Apr 01, 12:29:18 2010 +0200
    riepilogo: importazione iniziale.

L’output dell’unione è:

 unendo hello.en.txt e hello.txt a hello.en.txt
 0 file aggiornati, 1 file uniti, 0 file rimossi, 0 file non risolti
 (unione si fondono, non dimenticare di impegnarsi)

In altre parole: Mercurial ha preso la modifica dalla revisione 1 e l’ha unita al nuovo nome del file dalla revisione 2 ( hello.en.txt ). Gestire questo caso è ovviamente essenziale per supportare il refactoring e il refactoring è esattamente il tipo di cosa che vorrete fare su una filiale.

Senza parlare dei soliti vantaggi (commit offline, processo di pubblicazione , …) ecco un esempio di “fusione” che mi piace:

Lo scenario principale che continuo a vedere è un ramo su cui … due compiti non collegati sono effettivamente sviluppati
(è iniziato da una funzione, ma ha portato allo sviluppo di questa altra funzionalità.
O è partito da una patch, ma ha portato allo sviluppo di un’altra funzionalità).

Come si fonde solo una delle due funzionalità sul ramo principale?
O come si isolano le due funzioni nei propri rami?

Potresti provare a generare qualche tipo di patch, il problema è che non sei più sicuro delle dipendenze funzionali che potrebbero esistere tra:

  • il commit (o revisione per SVN) usato nelle tue patch
  • l’altro non fa parte della patch

Git (e anche Mercurial suppongo) propongono l’opzione rebase –onto per rebase (resettare la radice del ramo) parte di un ramo:

Dal post di Jefromi

 - x - x - x (v2) - x - x - x (v2.1) \ x - x - x (v2-only) - x - x - x (wss) 

puoi sbrogliare questa situazione in cui hai le patch per la v2 e una nuova funzionalità wss in:

 - x - x - x (v2) - x - x - x (v2.1) |\ | x - x - x (v2-only) \ x - x - x (wss) 

, permettendoti di:

  • prova ogni ramo in isolamento per verificare se tutto si compila / funziona come previsto
  • unire solo ciò che si desidera main.

L’altra caratteristica che mi piace (che influenza le fusioni) è la capacità di schiacciare i commit (in un ramo non ancora spinto ad un altro repo) per presentare:

  • una storia più pulita
  • commit che sono più coerenti (invece di commit1 per function1, commit2 per function2, commit3 di nuovo per function1 …)

Ciò garantisce fusioni molto più semplici, con meno conflitti.

Di recente abbiamo migrato da SVN a GIT e abbiamo affrontato questa stessa incertezza. C’era un sacco di prove aneddotiche sul fatto che GIT fosse migliore, ma era difficile trovare esempi.

Posso dirvi, però, che GIT è MOLTO BELLO nella fusione rispetto a SVN. Questo è ovviamente aneddotico, ma c’è un tavolo da seguire.

Ecco alcune delle cose che abbiamo trovato:

  • SVN soleva sollevare un sacco di conflitti tra alberi in situazioni in cui sembrava non doverlo fare. Non siamo mai arrivati ​​a fondo, ma non succede in GIT.
  • Mentre meglio, GIT è significativamente più complicato. Dedica un po ‘di tempo alla formazione.
  • Eravamo abituati a Tortoise SVN, che ci piaceva. GIT Tortoise non è buono e questo potrebbe scoraggiare. Comunque ora utilizzo la riga di comando GIT che preferisco di gran lunga a Tortoise SVN o a qualsiasi GUI GIT.

Durante la valutazione di GIT abbiamo eseguito i seguenti test. Questi mostrano GIT come il vincitore quando si tratta di unire, ma non di molto. In pratica la differenza è molto più grande, ma suppongo che non siamo riusciti a replicare le situazioni che SVN gestisce male.

GIT vs SVN Valutazione di fusione

Altri hanno coperto gli aspetti più teorici di questo. Forse posso dare una prospettiva più pratica.

Attualmente sto lavorando per un’azienda che utilizza SVN in un modello di sviluppo “feature branch”. Questo è:

  • Nessun lavoro può essere svolto sul bagagliaio
  • Ogni sviluppatore può aver creato le proprie filiali
  • I rami dovrebbero durare per la durata dell’attività intrapresa
  • Ogni attività dovrebbe avere il proprio ramo
  • Le fusioni di nuovo al tronco devono essere autorizzate (normalmente via bugzilla)
  • Nei momentjs in cui sono necessari alti livelli di controllo, le fusioni possono essere effettuate da un gatekeeper

In generale, funziona. SVN può essere usato per un stream come questo, ma non è perfetto. Ci sono alcuni aspetti di SVN che si intromettono e modellano il comportamento umano. Questo gli conferisce alcuni aspetti negativi.

  • Abbiamo avuto parecchi problemi con le persone che si diramano da punti inferiori a ^/trunk . Queste figlie uniscono i record di informazioni in tutto l’albero e alla fine interrompono il tracciamento delle unioni. Cominciano a comparire falsi conflitti e regna la confusione.
  • Raccogliere i cambiamenti dal tronco a un ramo è relativamente semplice. svn merge fa quello che vuoi. L’unione delle modifiche richiede (ci viene detto) – --reintegrate nel comando di unione. Non ho mai veramente capito questo interruttore, ma significa che il ramo non può essere nuovamente unito al tronco. Questo significa che è un ramo morto e devi crearne uno nuovo per continuare il lavoro. (Vedi nota)
  • L’intera attività di fare operazioni sul server tramite URL durante la creazione e l’eliminazione di rami confonde e fa davvero paura. Così loro lo evitano.
  • Passare da un ramo all’altro è facile da sbagliare, lasciando una parte di un albero guardando il ramo A, lasciando un’altra parte guardando il ramo B. Quindi le persone preferiscono fare tutto il loro lavoro in un ramo.

Quello che tende ad accadere è che un ingegnere crea una filiale il giorno 1. Inizia il suo lavoro e si dimentica di ciò. Qualche tempo dopo arriva un capo e chiede se può rilasciare il suo lavoro sul tronco. L’ingegnere ha temuto questo giorno perché reintegrare significa:

  • Unendo il suo ramo di lunga vita nel tronco e risolvendo tutti i conflitti, rilasciando codice non correlato che avrebbe dovuto essere in un ramo separato, ma non lo era.
  • Cancellando il suo ramo
  • Creare un nuovo ramo
  • Passando la sua copia di lavoro al nuovo ramo

… e poiché l’ingegnere fa questo il meno ansible, non riescono a ricordare “l’incantesimo magico” per fare ogni passo. Interruttori e URL sbagliati accadono, e improvvisamente sono in disordine e ottengono l'”esperto”.

Alla fine tutto si assesta e le persone imparano come affrontare le carenze, ma ogni nuovo antipasto passa attraverso gli stessi problemi. La realtà finale (al contrario di quello che ho iniziato all’inizio) è:

  • Nessun lavoro è stato fatto sul tronco
  • Ogni sviluppatore ha un ramo principale
  • I rami durano fino a quando il lavoro deve essere rilasciato
  • Le correzioni di bug con restrizioni tendono a ottenere il proprio ramo
  • Si fonde di nuovo al tronco sono fatti quando autorizzato

…ma…

  • A volte il lavoro lo porta al tronco quando non dovrebbe, perché è nello stesso ramo di qualcos’altro.
  • Le persone evitano tutte le fusioni (anche le cose facili), quindi le persone spesso lavorano nelle loro piccole bolle
  • Le grandi fusioni tendono a verificarsi e causano una quantità limitata di caos.

Per fortuna la squadra è abbastanza piccola da farcela, ma non sarebbe scalabile. Il fatto è che niente di questo è un problema con CVCS, ma più che le unioni non sono così importanti come in DVCS non sono così fluide. Quella “fusione di attrito” causa un comportamento che significa che un modello “Feature Branch” inizia ad andare in pezzi. Buone fusioni devono essere una caratteristica di tutti i VCS, non solo DVCS.


In base a questo c’è ora un --record-only che potrebbe essere usato per risolvere il problema --reintegrate , e apparentemente v1.8 sceglie quando eseguire un reintegro automatico, e non causa che il ramo sia morto in seguito

Prima della sovversione 1.5 (se non sbaglio), la sovversione aveva uno svantaggio significativo in quanto non ricordava la cronologia delle unioni.

Diamo un’occhiata al caso delineato da VonC:

 - x - x - x (v2) - x - x - x (v2.1) |\ | x - A - x (v2-only) \ x - B - x (wss) 

Notare le revisioni A e B. Dire che le modifiche unite dalla revisione A sul ramo “wss” al ramo “v2-only” alla revisione B (per qualsiasi motivo), ma hanno continuato ad utilizzare entrambi i rami. Se si tenta di unire nuovamente i due rami usando mercurial, si uniranno solo le modifiche dopo le revisioni A e B. Con la sovversione, si dovrà unire tutto, come se prima non si fosse fatto unire.

Questo è un esempio tratto dalla mia esperienza personale, in cui la fusione da B ad A ha richiesto diverse ore a causa del volume di codice: sarebbe stato un vero dolore ripetere ancora , il che sarebbe stato il caso di subversion pre-1.5.

Un’altra, probabilmente più rilevante differenza nel comportamento di fusione da Hginit: Subversion Re-education :

Immagina che tu ed io stiamo lavorando su un codice, e lo ramifichiamo, e ognuno di noi va nei nostri spazi di lavoro separati e apporta molte e molte modifiche a quel codice separatamente, quindi hanno diverguto parecchio.

Quando dobbiamo unirci, Subversion prova a guardare entrambe le revisioni, il mio codice modificato e il codice modificato, e cerca di indovinare come distruggerli insieme in un unico grande disastro. Di solito fallisce, producendo pagine e pagine di “conflitti di fusione” che non sono realmente conflitti, semplicemente luoghi in cui Subversion non riesce a capire cosa abbiamo fatto.

Al contrario, mentre lavoravamo separatamente in Mercurial, Mercurial era impegnato a tenere una serie di changeset. E così, quando vogliamo unire il nostro codice, Mercurial ha davvero molte più informazioni: sa cosa ognuno di noi ha cambiato e può riapplicare quei cambiamenti, piuttosto che guardare solo il prodotto finale e cercare di indovinare come metterlo insieme.

In breve, il modo in cui Mercurial analizza le differenze è (era?) Superiore a quello di sovversione.