Ramos nomeados x vários repositórios

130

Atualmente, estamos usando o subversion em uma base de código relativamente grande. Cada versão obtém sua própria ramificação e as correções são executadas no tronco e migradas para ramificações de liberação usandosvnmerge.py

Acredito que chegou a hora de avançar para um melhor controle de fonte, e estou brincando com a Mercurial há algum tempo.

Parece haver duas escolas no gerenciamento de uma estrutura de liberação usando o Mercurial. Cada versão obtém seu próprio repositório e as correções são feitas no ramo de lançamento e enviadas para o ramo principal (e qualquer outro ramo de lançamento mais recente). OU usando ramos nomeados em um único repositório (ou várias cópias correspondentes).

Em ambos os casos, parece que eu poderia estar usando algo como transplante para fazer mudanças de paleta para inclusão nos ramos de lançamento.

Eu peço a você; quais são os méritos relativos de cada abordagem?

James Emerton
fonte

Respostas:

129

A maior diferença é como os nomes das filiais são registrados no histórico. Com ramificações nomeadas, o nome da ramificação é incorporado em cada conjunto de alterações e, portanto, se torna uma parte imutável do histórico. Com os clones, não haverá registro permanente de onde veio um conjunto de alterações específico.

Isso significa que os clones são ótimos para experimentos rápidos nos quais você não deseja registrar um nome de ramificação, e ramificações nomeadas são boas para ramificações de longo prazo ("1.x", "2.x" e similares).

Observe também que um único repositório pode acomodar facilmente várias ramificações leves no Mercurial. Tais ramificações no repositório podem ser marcadas para que você possa encontrá-las facilmente novamente. Digamos que você clonou o repositório da empresa quando este se parece com:

[a] --- [b]

Você corta e faz [x]e [y]:

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

Enquanto alguém coloca [c]e [d]no repositório, então, quando você puxa, obtém um gráfico de histórico como este:

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

Aqui existem duas cabeças em um único repositório. Sua cópia de trabalho sempre refletirá um único conjunto de alterações, o chamado conjunto de alterações pai da cópia de trabalho. Verifique isso com:

% hg parents

Digamos que ele reporte [y]. Você pode ver as cabeças com

% hg heads

e isso irá relatar [y]e [d]. Se você deseja atualizar seu repositório para um checkout limpo de [d], basta fazer (substitua [d]pelo número da revisão [d]):

% hg update --clean [d]

Você verá esse hg parentsrelatório [d]. Isso significa que seu próximo commit terá [d]como pai. Assim, você pode corrigir um erro que você notou na ramificação principal e criar o conjunto de alterações [e]:

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

Para enviar [e]apenas o changeset , você precisa fazer

% hg push -r [e]

onde [e]está o hash do conjunto de alterações. Por padrão hg pushsimplesmente comparar os repositórios e ver que [x], [y]e [e]estão faltando, mas você não pode querer compartilhar [x]e [y]ainda.

Se o bugfix também afetar você, você deseja mesclá-lo com o seu ramo de recurso:

% hg update [y]
% hg merge

Isso deixará o gráfico do repositório assim:

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

onde [z]está a mesclagem entre [y]e [e]. Você também pode ter optado por jogar o galho fora:

% hg strip [x]

Meu ponto principal desta história é o seguinte: um único clone pode representar facilmente várias trilhas de desenvolvimento. Isso sempre foi verdadeiro para "plain hg" sem usar nenhuma extensão. A extensão de favoritos é uma grande ajuda, no entanto. Isso permitirá que você atribua nomes (favoritos) aos conjuntos de alterações. No caso acima, você desejará um marcador em sua cabeça de desenvolvimento e um na cabeça de upstream. Os marcadores podem ser pressionados e puxados com o Mercurial 1.6 e se tornaram um recurso interno do Mercurial 1.8.

Se você tivesse optado por criar dois clones, seu clone de desenvolvimento seria assim depois de criar [x]e [y]:

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

E seu clone upstream conterá:

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

Agora você percebe o bug e o corrige. Aqui você não precisa, hg updatepois o clone upstream está pronto para uso. Você confirma e cria [e]:

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

Para incluir a correção de bug no seu clone de desenvolvimento, você a puxa para lá:

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

e mesclar:

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

O gráfico pode parecer diferente, mas tem a mesma estrutura e o resultado final é o mesmo. Usando os clones, você teve que fazer um pouco menos de contabilidade mental.

As ramificações nomeadas não entraram em cena aqui porque são bastante opcionais. O Mercurial em si foi desenvolvido usando dois clones por anos antes de passarmos a usar ramificações nomeadas. Mantemos uma ramificação chamada 'stable' além da ramificação 'default' e fazemos nossos lançamentos com base na ramificação 'stable'. Veja a página de ramificação padrão no wiki para obter uma descrição do fluxo de trabalho recomendado.

Martin Geisler
fonte
1
se o conjunto de alterações viesse de um usuário diferente, isso teria sido gravado; portanto, usar clones não é nada ruim. Ao enviar um novo recurso, muitas vezes é desinteressante saber que você fez isso de um repositório separado. Há também uma extensão localbranch, que fornece uma ramificação local única. Útil ao clonar o repositório está associado a altos custos (tempo / espaço).
Johannes Rudolph
2
referenciando a: 'clones são ótimos para experimentos rápidos' - Não, não são! E se você tiver alguns milhares de arquivos em repositório? A clonagem levará idades (a qualquer momento acima de 1 minuto) enquanto a ramificação alterna apenas um momento (<1 segundo). Ainda usando ramos nomeados poluirá o changelog. Não é um beco sem saída? Ou estou faltando alguma coisa?
Seler 23/08
Okay seler; Parece uma modificação do argumento original; Os clones são bons quando a sobrecarga de várias cópias completas não é importante para você ou quando você pode usar os links simbólicos / hardlinks da hg para reduzir o custo de cópias de trabalho locais separadas por filial.
Warren P
@eler: você está certo de que os clones são impraticáveis ​​se o código for grande. Os favoritos são a solução então.
Martin Geisler
29

Eu acho que você quer toda a história em um repo. Desovar um repositório de curto prazo é para experimentos de curto prazo, não para grandes eventos como lançamentos.

Uma das decepções do Mercurial é que parece não haver uma maneira fácil de criar um galho de vida curta, brincar com ele, abandoná-lo e coletar o lixo. Ramos são para sempre. Simpatizo por nunca querer abandonar a história, mas os galhos descartáveis ​​e super baratos são um gitrecurso que eu realmente gostaria de ver hg.

Norman Ramsey
fonte
20
Você pode facilmente fazer uma ramificação desse recurso: "hg update" para o seu ponto de ramificação, edite e "hg commit". Você criou uma linha de desenvolvimento divergente - novas confirmações estenderão esse ramo. Use "hg clone -r" para se livrar dele ou remova-o na linha por "hg strip". Portanto, não fique desapontado ou acesse as listas de distribuição do Mercurial com suas solicitações de recursos.
Martin Geisler
8
Parece que hg stripé o que eu quero. Por que a documentação online afirma que ramificações não podem ser excluídas?
Norman Ramsey
11
Veja também este post para uma explicação sobre como Mercurial tem, de certa forma, ramos mais baratos do que o git: stevelosh.com/blog/entry/2009/8/30/...
Martin Geisler
9
Você pode fechar uma ramificação nomeada com hg ci --close-branch.
Andrey Vlasovskikh 14/10/09
3
@ Norman Ramsey: quando as pessoas dizem que as ramificações não podem ser excluídas, elas significam que você não pode alterar o nome da ramificação incorporada nos conjuntos de alterações. Um changeset nos no ramo, define um ramo. Você precisará excluir o conjunto de alterações e recriá-lo com um nome de filial diferente, se desejar "movê-lo" para uma filial diferente.
Martin Geisler
14

Você deveria fazer as duas coisas .

Comece com a resposta aceita do @Norman: use um repositório com uma ramificação nomeada por release.

Em seguida, tenha um clone por ramificação de liberação para construção e teste.

Uma observação importante é que, mesmo se você usar vários repositórios, evite usá-lo transplantpara mover conjuntos de alterações entre eles, porque 1) altera o hash e 2) pode introduzir bugs que são muito difíceis de detectar quando há alterações conflitantes entre o conjunto de alterações que você transplante e ramo alvo. Em vez disso, você deseja fazer a mesclagem usual (e sem pré-emergir: sempre inspecione visualmente a mesclagem), o que resultará no que o mg disse no final de sua resposta:

O gráfico pode parecer diferente, mas tem a mesma estrutura e o resultado final é o mesmo.

Mais detalhadamente, se você usar vários repositórios, o repositório "trunk" (ou padrão, principal, desenvolvimento, qualquer que seja) contém TODOS os conjuntos de alterações em TODOS os repositórios. Cada repositório de release / ramificação é simplesmente uma ramificação no tronco, todas mescladas de uma maneira ou de outra para o tronco, até que você queira deixar uma versão antiga para trás. Portanto, a única diferença real entre esse repositório principal e o repositório único no esquema de ramificação nomeada é simplesmente se as ramificações são nomeadas ou não.

Isso deve tornar óbvio o motivo pelo qual eu disse "comece com um repo". Esse repositório único é o único lugar em que você precisará procurar por qualquer conjunto de alterações em qualquer versão . Você pode marcar ainda mais conjuntos de alterações nas ramificações da versão para controle de versão. É conceitualmente claro e simples, e simplifica a administração do sistema, pois é a única coisa que absolutamente precisa estar disponível e recuperável o tempo todo.

Mas ainda é necessário manter um clone por ramificação / versão que você precisa criar e testar. É trivial como você pode hg clone <main repo>#<branch> <branch repo>e, hg pullno repositório de ramificação, apenas puxa novos conjuntos de alterações nesse ramo (além de conjuntos de alterações ancestrais em ramos anteriores que foram mesclados).

Essa configuração se encaixa melhor no modelo de commit de kernel do linux de extrator único (não é bom agir como Lord Linus. Em nossa empresa, chamamos de integrador de funções ), pois o repositório principal é a única coisa que os desenvolvedores precisam clonar e extrator precisa puxar para dentro. A manutenção dos repositórios de filial é puramente para gerenciamento de liberação e pode ser completamente automatizada. Os desenvolvedores nunca precisam extrair / enviar para os repositórios de filial.


Aqui está o exemplo de @ mg reformulado para esta configuração. Ponto de partida:

[a] - [b]

Faça uma ramificação nomeada para uma versão de lançamento, diga "1.0", quando chegar à versão alfa. Cometa correções de erros:

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

(1.0)não é um conjunto de alterações real, pois o ramo nomeado não existe até você confirmar. (Você pode fazer uma confirmação trivial, como adicionar uma tag, para garantir que as ramificações nomeadas sejam criadas corretamente.)

A mesclagem [m1]é a chave para essa configuração. Diferente de um repositório de desenvolvedores onde pode haver um número ilimitado de cabeças, você NÃO deseja ter várias cabeças no seu repositório principal (exceto o ramo antigo de liberação morta, como mencionado anteriormente). Portanto, sempre que houver novos conjuntos de alterações nas ramificações de liberação, você deverá mesclá-las novamente à ramificação padrão (ou uma ramificação de liberação posterior) imediatamente. Isso garante que qualquer correção de bug em uma versão também seja incluída em todas as versões posteriores.

Enquanto isso, o desenvolvimento na ramificação padrão continua na próxima versão:

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

E, como sempre, você precisa mesclar as duas cabeças na ramificação padrão:

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

E este é o clone da ramificação 1.0:

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

Agora é um exercício para adicionar o próximo ramo de lançamento. Se for 2.0, ele definitivamente se desviará do padrão. Se for 1.1, você pode optar por ramificar 1.0 ou padrão. Independentemente disso, qualquer novo conjunto de alterações na 1.0 deve ser mesclado primeiro à próxima ramificação, depois para o padrão. Isso pode ser feito automaticamente se não houver conflito, resultando apenas em uma mesclagem vazia.


Espero que o exemplo esclareça meus pontos anteriores. Em resumo, as vantagens dessa abordagem são:

  1. Repositório autoritário único que contém conjunto de alterações completo e histórico de versões.
  2. Gerenciamento de versão claro e simplificado.
  3. Fluxo de trabalho claro e simplificado para desenvolvedores e integradores.
  4. Facilite as iterações do fluxo de trabalho (revisões de código) e a automação (mesclagem vazia automática).

UPDATE O próprio hg faz isso : o repositório principal principal contém as ramificações padrão e estável e o repositório estável é o clone da ramificação estável. Porém, ele não usa ramificação com versão, pois as tags de versão ao longo da ramificação estável são boas o suficiente para fins de gerenciamento de versão.

Geoffrey Zheng
fonte
5

A principal diferença, até onde eu sei, é algo que você já declarou: o branch ramificado está em um único repositório. Ramos nomeados têm tudo à mão em um só lugar. Os repositórios separados são menores e fáceis de se mover. A razão pela qual existem duas escolas de pensamento sobre isso é que não há um vencedor claro. Qualquer argumento do lado que faça mais sentido para você provavelmente é o que você deve seguir, porque é provável que o ambiente deles seja mais semelhante ao seu.

dwc
fonte
2

Eu acho que é claramente uma decisão pragmática, dependendo da situação atual, por exemplo, o tamanho de um recurso / reprojeto. Eu acho que os garfos são realmente bons para os contribuidores com funções ainda não comprometidas em ingressar na equipe de desenvolvedores, provando sua aptidão com despesas técnicas negligenciáveis.

thSoft
fonte
0

Eu realmente desaconselho o uso de ramos nomeados para versões. É para isso que servem as tags. Ramos nomeados são destinados a diversões duradouras, como um stableramo.

Então, por que não usar apenas tags? Um exemplo básico:

  • O desenvolvimento acontece em uma única ramificação
  • Sempre que uma liberação é criada, você a marca de acordo.
  • O desenvolvimento continua a partir daí
  • Se você tiver alguns bugs para corrigir (ou o que for) em uma determinada versão, basta atualizar para a tag it, fazer suas alterações e confirmar

Isso criará uma nova cabeça sem nome no defaultramo, também conhecida como. um ramo anônimo, que é perfeitamente bom em hg. Você pode, a qualquer momento, mesclar a correção do bug novamente na faixa principal de desenvolvimento. Não há necessidade de ramificações nomeadas.

DanMan
fonte
Isso depende muito do seu processo. Um aplicativo Web, por exemplo, funciona bem com uma hierarquia de ramificação estável / testing / devel. Ao criar software de desktop, normalmente temos uma ramificação de desenvolvimento (padrão) e uma a três (!) Ramificações diferentes em manutenção. É difícil prever quando precisaremos revisitar uma ramificação, e há uma certa elegância em ter uma ramificação rastreando uma versão principal.
James Emerton