Branch nominati vs Repository multipli


130

Attualmente stiamo usando la sovversione su una base di codice relativamente grande. Ogni versione ottiene il proprio ramo e le correzioni vengono eseguite sul trunk e migrate nei rami di rilascio utilizzandosvnmerge.py

Credo che sia giunto il momento di passare a un migliore controllo del codice sorgente e ho giocato con Mercurial per un po '.

Sembra che ci siano due scuole su come gestire una tale struttura di rilascio usando Mercurial. Ciascuna versione ottiene il proprio repository e le correzioni vengono apportate al ramo di rilascio e inviate al ramo principale (e a qualsiasi altro ramo di rilascio più recente). OPPURE utilizzando i rami denominati all'interno di un singolo repository (o più copie corrispondenti).

In entrambi i casi sembra che potrei usare qualcosa di simile al trapianto per modificare i cambiamenti per l'inclusione nei rami di rilascio.

Ti chiedo; quali sono i meriti relativi di ciascun approccio?

Risposte:


129

La differenza più grande è come i nomi delle filiali sono registrati nella storia. Con i rami nominati il ​​nome del ramo è incorporato in ogni changeset e diventerà così una parte immutabile della storia. Con i cloni non ci sarà alcuna registrazione permanente della provenienza di un particolare changeset.

Ciò significa che i cloni sono ottimi per esperimenti rapidi in cui non si desidera registrare un nome di ramo e i rami con nome sono utili per i rami a lungo termine ("1.x", "2.x" e simili).

Si noti inoltre che un singolo repository può facilmente ospitare più rami leggeri in Mercurial. Tali rami nel repository possono essere aggiunti ai segnalibri in modo da poterli ritrovare facilmente. Diciamo che hai clonato il repository dell'azienda quando sembrava così:

[a] --- [b]

Fai a pezzi e fai [x]e [y]:

[a] --- [b] --- [x] --- [y]

Intendi mentre qualcuno mette [c]e [d]nel repository, quindi quando tiri ottieni un grafico cronologico come questo:

            [x] --- [y]
           /
[a] --- [b] --- [c] --- [d]

Qui ci sono due teste in un unico repository. La tua copia di lavoro rifletterà sempre un singolo changeset, il cosiddetto changeset parent della copia di lavoro. Verifica questo con:

% hg parents

Diciamo che riporta [y]. Puoi vedere le teste con

% hg heads

e questo riporterà [y]e [d]. Se vuoi aggiornare il tuo repository a un checkout pulito di [d], allora semplicemente (sostituisci [d]con il numero di revisione per [d]):

% hg update --clean [d]

Vedrai quindi quel hg parentsrapporto [d]. Questo significa che il tuo prossimo commit avrà [d]come genitore. Puoi quindi correggere un bug che hai notato nel ramo principale e creare il changeset [e]:

            [x] --- [y]
           /
[a] --- [b] --- [c] --- [d] --- [e]

Per inviare [e]solo il changeset , devi farlo

% hg push -r [e]

dov'è [e]l'hash del changeset. Per impostazione predefinita hg push, confronterà semplicemente i repository e vedrà che [x], [y]e [e]mancano, ma potresti non voler condividere [x]e [y]ancora.

Se anche il bugfix ti interessa, vuoi unirlo al tuo ramo di funzionalità:

% hg update [y]
% hg merge

In questo modo il grafico del tuo repository apparirà così:

            [x] --- [y] ----------- [z]
           / /
[a] --- [b] --- [c] --- [d] --- [e]

dov'è [z]l'unione tra [y]e [e]. Potresti anche aver scelto di gettare via il ramo:

% hg strip [x]

Il mio punto principale di questa storia è questo: un singolo clone può rappresentare facilmente diverse tracce di sviluppo. Questo è sempre stato vero per "plain hg" senza usare alcuna estensione. L' estensione dei segnalibri è di grande aiuto, comunque. Ti permetterà di assegnare nomi (segnalibri) ai changeset. Nel caso sopra, vorrai un segnalibro sulla tua testa di sviluppo e uno sulla testa a monte. I segnalibri possono essere spinti e estratti con Mercurial 1.6 e sono diventati una funzionalità integrata in Mercurial 1.8.

Se avessi scelto di creare due cloni, il tuo clone di sviluppo sarebbe apparso così dopo aver creato [x]e [y]:

[a] --- [b] --- [x] --- [y]

E il tuo clone a monte conterrà:

[a] --- [b] --- [c] --- [d]

Ora noti il ​​bug e lo risolvi. Qui non è necessario hg updatepoiché il clone a monte è pronto per l'uso. Ti impegni e crei [e]:

[a] --- [b] --- [c] --- [d] --- [e]

Per includere il bugfix nel tuo clone di sviluppo devi inserirlo qui:

[a] --- [b] --- [x] --- [y]
           \
            [c] --- [d] --- [e]

e unire:

[a] --- [b] --- [x] --- [y] --- [z]
           \ /
            [c] --- [d] --- [e]

Il grafico potrebbe apparire diverso, ma ha la stessa struttura e il risultato finale è lo stesso. Usando i cloni hai dovuto fare un po 'meno contabilità mentale.

I rami nominati non sono davvero entrati in scena qui perché sono abbastanza opzionali. Lo stesso Mercurial è stato sviluppato utilizzando due cloni per anni prima di passare all'utilizzo di filiali denominate. Manteniamo un ramo chiamato 'stabile' oltre al ramo 'predefinito' e rendiamo le nostre versioni basate sul ramo 'stabile'. Vedere la pagina di ramificazione standard nel wiki per una descrizione del flusso di lavoro consigliato.


1
se il changeset provenisse da un altro utente, sarebbe stato registrato, quindi usare i cloni non è niente di male. Quando si spinge una nuova funzione è spesso poco interessante sapere che lo si è fatto da un repository separato. C'è anche un'estensione localbranch, che ti dà un solo ramo locale. Utile durante la clonazione del repository è associato a costi elevati (tempo / spazio).
Johannes Rudolph,

2
riferendosi a: "i cloni sono ottimi per esperimenti rapidi" - No, non lo sono! E se hai un paio di migliaia di file in repository? La clonazione richiederà anni (in qualsiasi momento sopra 1 minuto) mentre il cambio di ramo dura un momento (<1 secondo). Usando ancora i rami con nome, si inquinerà il log delle modifiche. Non è un vicolo cieco? O mi manca qualcosa?
seler

Va bene seler; Sembra una modifica al suo argomento originale; I cloni sono utili laddove l'overhead di più copie complete non è importante per te o quando è possibile utilizzare i collegamenti simbolici / hardlink di hg per mitigare il costo di copie di lavoro locali separate per ramo.
Warren P

@seler: hai perfettamente ragione sul fatto che i cloni sono poco pratici se il codice è grande. I segnalibri sono la soluzione quindi.
Martin Geisler,

29

Penso che tu voglia l'intera storia in un repository. Generare un repo a breve termine è per esperimenti a breve termine, non per eventi importanti come i rilasci.

Una delle delusioni di Mercurial è che non sembra esserci un modo semplice per creare un ramo di breve durata, giocarci, abbandonarlo e raccogliere la spazzatura. I rami sono per sempre. Mi rallegro di non voler mai abbandonare la storia, ma i rami super economici e usa e getta sono una gitcaratteristica che mi piacerebbe davvero vedere hg.


20
Puoi facilmente creare un tale ramo di funzionalità: "hg update" al tuo punto di diramazione, modifica via e "hg commit". Hai creato una linea di sviluppo divergente: nuovi impegni estenderanno questo ramo. Usa "hg clone -r" per sbarazzartene o rimuovilo in linea con "hg strip". Quindi, non essere deluso o vieni nelle mailing list di Mercurial con le tue richieste di funzionalità.
Martin Geisler,

8
Sembra che hg stripsia quello che voglio. Perché le filiali di rivendicazione della documentazione online non possono essere eliminate?
Norman Ramsey,

11
Vedi anche questo post sul blog per una spiegazione di come Mercurial abbia, in un certo senso, rami più economici del solito
Martin Geisler,

9
È possibile chiudere un ramo denominato con hg ci --close-branch.
Andrey Vlasovskikh,

3
@Norman Ramsey: quando le persone dicono che i rami non possono essere cancellati, significano che non puoi cambiare il nome del ramo incorporato nei cambiamenti. Un changeset ci no su un ramo, definisce un ramo. È necessario eliminare il changeset e ricrearlo con un nome di ramo diverso se si desidera "spostarlo" in un ramo diverso.
Martin Geisler,

14

Dovresti fare entrambe le cose .

Inizia con la risposta accettata da @Norman: usa un repository con un ramo denominato per versione.

Quindi, disporre di un clone per ramo di rilascio per la creazione e il test.

Una nota chiave è che anche se usi più repository, dovresti evitare di usare transplantper spostare i changeset tra loro perché 1) cambia hash e 2) può introdurre bug che sono molto difficili da rilevare quando ci sono cambiamenti contrastanti tra i changeset che trapianto e il ramo target. Invece vuoi fare la solita fusione (e senza premergere: ispeziona sempre visivamente la fusione), il che si tradurrà in ciò che @mg ha detto alla fine della sua risposta:

Il grafico potrebbe apparire diverso, ma ha la stessa struttura e il risultato finale è lo stesso.

Più verbalmente, se si utilizzano più repository, il repository "trunk" (o predefinito, principale, sviluppo, qualunque cosa) contenga TUTTI i changeset in TUTTI i repository. Ogni repository release / branch è semplicemente un ramo nel trunk, tutti uniti in un modo o nell'altro nel trunk, fino a quando non si desidera lasciare indietro una versione precedente. Pertanto, l'unica vera differenza tra quel repository principale e il singolo repository nello schema di filiale denominato è semplicemente se i rami sono denominati o meno.

Ciò dovrebbe rendere ovvio il motivo per cui ho detto "inizia con un repository". Quel singolo repository è l'unico posto in cui avrai mai bisogno di cercare eventuali cambiamenti in qualsiasi versione . È inoltre possibile taggare i changeset sui rami di rilascio per il versioning. È concettualmente chiaro e semplice e semplifica l'amministratore di sistema, poiché è l'unica cosa che deve assolutamente essere disponibile e recuperabile in ogni momento.

Tuttavia, è comunque necessario mantenere un clone per ramo / versione che è necessario creare e testare. È banale come puoi hg clone <main repo>#<branch> <branch repo>, e quindi hg pullnel repository di succursali estrarrà solo nuovi changeset su quel ramo (più i changeset di antenati sui rami precedenti che sono stati uniti).

Questa configurazione si adatta meglio al modello di commit puller del kernel Linux di single puller (non è bello comportarsi come Lord Linus. Nella nostra azienda chiamiamo integratore di ruoli ), poiché il repository principale è l'unica cosa che gli sviluppatori devono clonare e il l'estrattore deve entrare. La manutenzione dei repository delle filiali è puramente per la gestione dei rilasci e può essere completamente automatizzata. Gli sviluppatori non devono mai estrarre / spingere verso i repository delle filiali.


Ecco l'esempio di @ mg rifuso per questa configurazione. Punto di partenza:

[a] - [b]

Crea un ramo con nome per una versione di rilascio, ad esempio "1.0", quando arrivi alla versione alpha. Commit correzioni di bug su di esso:

[a] - [b] ------------------ [m1]
         \                 /
          (1.0) - [x] - [y]

(1.0)non è un vero changeset poiché il ramo denominato non esiste finché non si esegue il commit. (È possibile effettuare un commit banale, come l'aggiunta di un tag, per assicurarsi che i rami con nome siano creati correttamente.)

L'unione [m1]è la chiave di questa configurazione. A differenza di un repository per sviluppatori in cui può esserci un numero illimitato di head, NON si desidera avere più head nel repository principale (ad eccezione del vecchio ramo dead release come menzionato in precedenza). Pertanto, ogni volta che si hanno nuovi changeset sui rami di rilascio, è necessario ricollegarli immediatamente al ramo di default (o un ramo di rilascio successivo). Ciò garantisce che qualsiasi correzione di bug in una versione sia inclusa anche in tutte le versioni successive.

Nel frattempo lo sviluppo sul ramo predefinito continua verso la prossima versione:

          ------- [c] - [d]
         /
[a] - [b] ------------------ [m1]
         \                 /
          (1.0) - [x] - [y]

E come al solito, devi unire le due teste sul ramo predefinito:

          ------- [c] - [d] -------
         /                         \
[a] - [b] ------------------ [m1] - [m2]
         \                 /
          (1.0) - [x] - [y]

E questo è il clone del ramo 1.0:

[a] - [b] - (1.0) - [x] - [y]

Ora è un esercizio aggiungere il prossimo ramo di rilascio. Se è 2.0, si dirama sicuramente sul valore predefinito. Se è 1.1, puoi scegliere di diramare 1.0 o di default. Indipendentemente da ciò, qualsiasi nuovo changeset su 1.0 dovrebbe essere prima unito al ramo successivo, quindi al valore predefinito. Questo può essere fatto automaticamente se non ci sono conflitti, risultando in una semplice unione vuota.


Spero che l'esempio chiarisca i miei punti precedenti. In sintesi, i vantaggi di questo approccio sono:

  1. Repository autorevole unico che contiene il changeset completo e la cronologia delle versioni.
  2. Gestione delle versioni chiara e semplificata.
  3. Flusso di lavoro chiaro e semplificato per sviluppatori e integratori.
  4. Facilitare le iterazioni del flusso di lavoro (revisioni del codice) e l'automazione (unione vuota automatica).

AGGIORNAMENTO stesso hg fa questo : il repository principale contiene i rami predefiniti e stabili e il repository stabile è il clone dei rami stabili. Tuttavia, non utilizza il ramo con versione, poiché i tag di versione lungo il ramo stabile sono abbastanza buoni per i suoi scopi di gestione delle versioni.


5

La differenza principale, per quanto ne so, è qualcosa che hai già affermato: i nomi ramificati sono in un unico repository. I rami nominati hanno tutto a portata di mano in un unico posto. Repos separati sono più piccoli e facili da spostare. La ragione per cui ci sono due scuole di pensiero su questo è che non esiste un chiaro vincitore. Qualunque argomento della parte abbia più senso per te è probabilmente quello con cui dovresti andare, perché è probabile che il loro ambiente sia più simile al tuo.


2

Penso che sia chiaramente una decisione pragmatica a seconda della situazione attuale, ad esempio la dimensione di una caratteristica / riprogettazione. Penso che le forcelle siano davvero utili per i collaboratori con ruoli non ancora committer di unirsi al team di sviluppatori dimostrando la loro attitudine con un sovraccarico tecnico trascurabile.


0

Vorrei davvero sconsigliare l'utilizzo di rami con nome per le versioni. Ecco a cosa servono i tag. I rami nominati sono pensati per diversioni durature, come un stableramo.

Quindi perché non usare solo i tag? Un esempio di base:

  • Lo sviluppo avviene su un singolo ramo
  • Ogni volta che viene creata una versione, la si tagga di conseguenza
  • Lo sviluppo continua da lì
  • Se hai alcuni bug da correggere (o qualsiasi altra cosa) in una determinata versione, ti basta aggiornare al relativo tag, apportare le modifiche e impegnarti

Ciò creerà una nuova testa senza nome sul defaultramo, aka. un ramo anonimo, che va benissimo in hg. È quindi possibile unire in qualsiasi momento il bugfix ripristinato nella traccia di sviluppo principale. Non sono necessari rami con nome.


Questo dipende molto dal tuo processo. Un'app Web, ad esempio, funziona bene con una gerarchia di rami stable / testing / devel. Durante la creazione di software desktop, in genere abbiamo un ramo di sviluppo (predefinito) e uno o tre (!) Rami diversi nella manutenzione. È difficile prevedere quando potremmo aver bisogno di rivisitare un ramo, e c'è una certa eleganza nell'avere una diramazione in una versione major.minor.
James Emerton,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.