Comparação entre Subversion, Mercurial e Git – Parte 3

Introdução

Esta é a 3ª parte da série artigos comparando o Subversion, Mercurial e Git.

O primeiro artigo, definiu o público alvo das comparações como projetos comerciais querendo migrar do Subversion para um DVCS. Também estabeleceu os critérios de comparação que serão apresentados e mostrou que desempenho e funcionalidade não são significativos para definir uma escolha entre o Mercurial e o Git.

O segundo artigo mediu a complexidade das ferramentas baseado nas premissas de que: a) quanto maior o número de comandos, mais difícil é conhecer todos; e b) quanto maior o número de linha de um texto de ajuda usado para explicar um comando, mais difícil é o comando. Os resultados indicaram que o Git parece ser o mais complexo dos três. A surpresa foi o Mercurial que se mostrou mais simples até mesmo que o Subversion — de acordo com os critérios analisados –, e foi o vencedor da rodada.

Este terceiro artigo analisa a questão da ramificação e mesclagem, que são os grandes diferenciais do DVCS e os principais motivos para deixar o Subversion. Começa analisando as deficiências do Subversion e depois apresenta como o controle de versão distribuído (DVCS) resolve o problema. Em seguida, apresenta as diferenças na ramificação e mesclagem do Mercurial e do Git.

Problemas na Ramificação e Mesclagem do Subversion

O Subversion não possui um formato específico para registrar um ramo. A ramificação é baseada na convenção que estabelece diretórios específicos para cada tipo de ramo. A criação de um ramo é uma operação de cópia de um diretório para outro e é registrada internamente através de apontadores que criam "cópias baratas" para economia de espaço em disco.

                  +-----+-------------->   /branches/1.x
                 /       \
                /         \
  --------+----+-----+-----+----+------>   /trunk
           \          \          \
            \          \          \
             +----------+----------+--->   /branches/experimental

A ramificação é simples, mas a mesclagem não. O problema é causado pela estrutura inadequada para implementação da ramificação. Diretórios não registram o histórico de mesclagens realizadas e esta informação precisa ser mantida em outro lugar. A partir da versão 1.5, o Subversion passou a registrar intervalos mesclados de outros ramos na propriedade svn:mergeinfo, que fica associada a cada diretório e arquivo afetado pela mesclagem. É uma solução improvisada e insuficiente para fazer da mesclagem uma operação simples.

A complexidade da operação pode ser observada na documentação do comando svn merge, que apresenta quatro casos de mesclagem:

  1. Sync
  2. Cherry-pick
  3. Reintegrate
  4. 2-URL

O caso mais complexo com o qual o Subversion consegue lidar é apresentado na ilustração abaixo. Trata da mesclagem entre o ramo bar para o ramo foo, considerando uma sincronização anterior entre o trunk e o ramo bar na revisão r500:

                     foo  +-----------------------------------o
                        /                                    ^
                       /                                    /
                      /              r500                  /
  trunk ------+------+-----------------L--------->        /
               \                        .                /
                \                        ............   /
                 \                                   . /
             bar  +-----------------------------------R

Identificar qual o tipo e os parâmetros corretos de mesclagem requer uma boa compreensão do funcionamento da operação e dos históricos do ramos envolvidos. Não é algo que se executa por tentativa e erro. Para o exemplo anterior, os parâmetros corretos da mesclagem seriam:

# comando executado sobre cópia de trabalho do ramo 'foo'
$ svn merge ^/trunk@500 ^/bar

Devido a todos esses detalhes e variações, a mesclagem é considerada uma operação avançada no Subversion. O resultado é que a ramificação e a mesclagem no Subversion acabam sendo usados apenas em casos extremamente necessários (ramos de longa duração).

Ramificação e Mesclagem no DVCS

No DVCS, o histórico de revisões é registrado em um grafo acíclico direcionado (DAG em inglês), em que as revisões são os nós e os arcos são apontadores para revisões anteriores. Ramos são registrados naturalmente no DAG através das linhas formadas por sequências de revisões:

                      ( )<----( )             ramo 1
                      /
                     /
  ( )<---( )<------( )<------( )<----( )      ramo 2
           \                   \
            \                   \
             ( )<----( )<-------( )<----( )   ramo 3

As mesclagens no DVCS são sempre as mesmas, baseada em três pontos de referência (3-way merge) que estão registrados no DAG: a ponta de cada ramo sendo mesclado e o ancestral comum mais próximo a ambas. A ponta do primeiro ramo é a revisão em que se encontra o diretório de trabalho. A ponta do outro ramo é passado por parâmetro ao comando de mesclagem. O ancestral comum é encontrando automaticamente no DAG pela própria operação de mesclagem.

O caso anterior, considerado complexo pelo Subversion, é elementar no DVCS:

                            D                                    H
                       foo  +------------------------------------+
                           /                                    /
                          /                                    /
                 A       /                E         F         /
     trunk ------+------+-----------------+---------+        /
                  \     C                  .                /
                   \                        ............   /
                    \                                   . /
                bar  +-----------------------------------+
                     B                                   G

Para a mesclagem de bar com foo, o diretório de trabalho é posicionado em foo na revisão ‘H’, a revisão ou nome do outro ramo é passado como parâmetro (‘G’ ou bar), o ancestral comum (‘C’) é encontrado automaticamente no DAG e a mesclagem executada. Pelo Mercurial e pelo Git, os comandos seriam respectivamente:

$ hg merge bar

$ git merge bar

Independente da complexidade do DAG, os parâmetros da mesclagem são sempre os mesmos. Por isso, a mesclagem é muito simples no DVCS.

Ramos de Curta e Longa Duração

A diferenciação dos tipos de ramos de acordo com sua duração é importante para avaliar os diferentes modelos de ramificação entre o Mercurial e o Git.

A duração de um ramo é o intervalo de tempo desde que é criado até o momento em que não são mais necessários, deixando de receber novas revisões:

 início                               fim
   |                                   |
  ( )<---( )<-------( )<------( )<----( )  } ramo 1
           \                  /
            \                /
             ( )<---( )<---( )  } ramo 2
              |             |
            início         fim

Ramos de longa duração são usados por meses, anos ou por toda a vida do projeto. Um projeto possui poucos ramos de longa duração, geralmente apenas o ramo principal e alguns ramos de manutenção.

O tempo de um ramo de curta duração varia entre minutos a alguns dias, apenas o suficiente para cumprir determinada tarefa. São ramos de curta duração os ramos individuais, ramos de correção de defeitos e de novas funcionalidades. Uma característica peculiar dos ramos de curta duração é que são sempre subordinados a um ramo de longa duração, para onde são mesclados depois de concluídos.

Ramificação no Mercurial e no Git

O Mercurial e o Git seguem modelos diferentes de identificação de ramos no DAG. Como referência para comparação, será usado o seguinte DAG (inspirado neste artigo), em que os números representam a ordem de criação das revisões e os nomes, os apontadores para os ramos:

    (1)----(2)          } stable
   /         \
 (0)---(3)---(4)---(7)  } default (hg) | master (git)
   \     \         /
    \     (5)---(6)     } bug
     \            \
      (8)---------(9)   } feature

Ramificação no Git

O Git identifica um ramo a partir de um apontador para uma revisão específica, que passa a ser a ponta do ramo em questão. A posição do apontador avança automaticamente à medida que consolidações (commits) vão sendo feitas, acompanhando a evolução do ramo.

O script ramificacao-git-branches.sh produz o DAG de referência através dos apontadores do Git resultando em:

          stable
            |
    (1)----(2)    master
   /         \      |
 (0)---(3)---(4)---(7)
   \     \         /
    \     (5)---(6) <-- bug
     \            \
      (8)---------(9)
                   |
                feature

O histórico do ramo é formado pela sequência de revisões que antecederam a revisão sendo apontada. Para o ramo master, o histórico apresentado é o seguinte:

$ git log --graph --pretty=oneline master
*   704bef1300464c7042a1b8832c2f2b51668a1f09 sete - Merge branch 'bug'
|\
| * a7abc9cc40cbf86b04f1a9a35c5f757c93b0e459 seis
| * 605f68765441ced05b20c0d4c7dc9e454a54f31f cinco
* |   8ae938a75107e34acf7be6a058076483dd0deeef quatro - Merge branch 'stable'
|\ \
| |/
|/|
| * 95dc51bf9265a95ab45c19f3ce5642387bd92230 dois
| * 6185190cc7c1c98cd3ee2c06e4a90e65fa409957 um
* | 58eee0875af3d7e43739d3032d8e04e37976757b tres
|/
* 1b0d78aa1fb5865171c1a3833f0f825831676fad zero

O histórico do ramo master está misturado aos do ramo bug e stable. Não há como separá-los, nem estabelecer limites claros entre eles. Isto acontece com todos os ramos:

Ramo Histórico no Git Histórico Real
stable [0, 1, 2] [1, 2]
master [0, 3, 1, 2, 4, 5, 6, 7] [0, 3, 4, 7]
feature [0, 8, 3, 5, 6, 9] [8, 9]
bug [0, 3, 5, 6] [5, 6]

Ramificação no Mercurial

No Mercurial, há dois tipos de identificação de ramos. Um deles usa apontadores como os do Git e, portanto, possui as mesmas limitações quanto à mistura nos históricos com outros ramos, conforme mostra o DAG abaixo referente ao apontador default-bookmark. (veja o script de ramificação do Mercurial usando bookmarks).

$ hg log -Gqr default-bookmark:0
     o    7:9085e9b01d9e
    |\
    | o  6:ff15ec120c49
    | |
    | o  5:ac56d708b1af
    | |
    o |  4:9c094cf9b76a
    |\|
    | o  3:94c70cad470c
    | |
    o |  2:06c592fde550
    | |
    o |  1:80d7de968135
    |/
    o  0:65f889a703bb

Observação

Para apresentar o grafo acima, antes é necessário habilitar a extensão graphlog
que já vem com o Mercurial:

$ echo '[extensions]
graphlog = ' >> ~/.hgrc

O outro tipo de identificação são os ramos nomeados (named branches). Um ramo nomeado é uma sequência de revisões associadas ao mesmo nome de ramo. Toda revisão no Mercurial está associada diretamente a algum ramo nomeado através de uma propriedade específica que existe na revisão. Esta propriedade é copiada a partir da revisão-pai a não ser que um novo nome de ramo seja definido. Quando o repositório do Mercurial é criado, existe apenas um ramo nomeado chamado default.

Refazendo o DAG de referência usando apenas ramos nomeados, tem-se os seguintes resultados:

    <1> ------- <2>              } <stable>
    /             \
  *0* --- *3* --- *4* ---- *7*   } *default*
    \        \             /
     \       .5. ------- .6.     } .bug.
      \                    \
      '8' ---------------- '9'   } 'feature'

O histórico do ramo default é apresentado pelo comando abaixo:

$ hg log -Gqb default
    default

    o    7:34aa586f5c24
    |\
    o |  4:efedf773d73d
    |\|
    | o  3:162e6292c68f
    |/
    o  0:00f350f3d161

O histórico do ramo nomeado corresponde exatamente ao histórico real do ramo. O mesmo acontece com os outros ramos:

Ramo Histórico no Mercurial Histórico Real
stable [1, 2] [1, 2]
default [0, 3, 4, 7] [0, 3, 4, 7]
feature [8, 9] [8, 9]
bug [5, 6] [5, 6]

Bookmarks e ramos nomeados podem ser combinados livremente:

    <1> ------- <2>                   } <stable>
    /             \
  *0* --- *3* --- *4* ---- *7*        \
    \        \             /           \
     \       *5* ------- *6* -> bug     } *default*
      \                    \           /
      *8* ---------------- *9*        /
                            |
                         feature

Análise da Ramificação

Os apontadores são simples e flexíveis, podendo ser renomeados e removidos facilmente. A única deficiência é que não registram o histório real de cada ramo. São perfeitos para ramos de curta duração, cujos históricos individuais podem ser mesclados com os ramos de longa duração ao qual são subordinados sem perda de informação relevante. Mas os ramos de longa duração, como o stable e o master, têm o objetivo de manter separadas certas variações do projeto, e é importante, portanto, que sejam permanentes e tenham seus históricos corretos e isolados.

Ramos nomeados registram o histórico real, podem ser fechados, mas não podem ser renomeados ou apagados. São mais indicados para ramos de longa duração.

O ideal é que houvesse apenas um tipo de identificação de ramos no DAG que combinasse a agilidade e flexibilidade dos apontadores com o registro real dos ramos nomeados. Este tipo serviria tanto para ramos de curta quanto de longa duração. Enquanto isto não acontece, a solução mais adequada é usar ramos nomeados para os ramos de longa duração e apontadores para ramos de curta duração.

Resultado: A ramificação do Mercurial e do Git são melhores que a do Subversion, mas nenhum dos dois têm um modelo perfeito de ramificação. O Mercurial tem opções diferentes para atender cada caso, mas isto implica mais detalhes para se preocupar. O Git segue um modelo único, mas que só é adequado para projetos sem comprometimento com o registro fiel do histórico.

Mesclagem

Além da estratégia clássica de mesclagem em três vias usada pelo Mercurial, o Git também possui duas outras estratégias para atender a situações específicas de mesclagem cruzada e mesclagem simultânea de vários ramos.

Mesclagem Cruzada

O caso de conhecido como mesclagem cruzada (criss-cross merge), acontece quando há mais de um ancestral comum que pode ser usado na mesclagem (veja ref1, ref2 e ref3). No exemplo abaixo, a mesclagem das revisões (3) e (4) que resulta na revisão (5), tem dois ancestrais comuns possíveis (1) e (2):

      (1) --- (3)
     /   \   /   \
    /     \ /     \
  (0)      X      (5)
    \     / \     /
     \   /   \   /
      (2) --- (4)

A mesclagem convencional de três vias se baseia em um único ancestral comum. Quando encontra uma mesclagem cruzada, a mesclagem por três vias escolhe arbitrariamente um dos ancestrais comuns (revisão 1 ou revisão 2), e o resultado pode variar (ref4):

  • Conflitos que já foram resolvidos podem reaparecer;
  • Mudanças que haviam sido revertidas podem oscilar.

O Git usa uma estratégia padrão de mesclagem chamada de "mesclagem recursiva", que detecta automaticamente os casos de mesclagem cruzada, cria uma mesclagem intermediária dos ancestrais comuns e a usa como referência para a mesclagem tradicional de três vias (veja o texto de ajuda pelo git help merge). No entanto, é possível que também apareçam conflitos na mesclagem intermediária e que vários níveis de recursão sejam necessários, o que tornaria a resolução dos conflitos ainda mais complicada..

As informações sobre mesclagem cruzadas são escassas. Não foram encontradas estatísticas sobre a frequência com que ocorrem, nem a eficácia de estratégias alternativas de mesclagens nesses casos. No texto de ajuda do comando `git merge` há apenas uma menção:

… This has been reported to result in fewer merge conflicts without causing mis-merges by tests done on actual merge commits taken from Linux 2.6 kernel development history.

Observação

Já para a próxima versão do Mercurial (2.3), está prevista a implementação de uma estratégia chamada de consensus merge para atender aos casos de criss-cross merge (veja referência).

Mesclagem Simultânea de Vários Ramos

O Git tem uma outra estratégia de mesclagem conhecida como octopus, que é capaz de mesclar vários ramos simultaneamente, desde que não haja conflitos entre eles. É uma funcionalidade muito conveniente para mantenedores de projetos que precisam combinar ramos provenientes de vários subsistemas independentes, tal como acontece no Linux.

Para projetos baseados em um arranjo cliente-servidor, esta funcionalidade não é tão interessante pois cabe a cada desenvolvedor mesclar seu ramo individual com o ramo existente no repositório oficial. A mesclagem de dois ramos por vez é suficiente para esses casos.

Análise da Mesclagem

A mesclagem de qualquer DVCS já é melhor que a do Subversion. O Git oferece algumas estratégias adicionais para casos mais raros e específicos de mesclagem, mas sem complicação adicional ao procedimento.

Resultado: Git possui mais estratégias de mesclagem, mas a mesclagem do Mercurial é boa o suficiente.

Conclusão da Parte 3

As limitações de ramificação e mesclagem do Subversion não existem no DVCS, devido ao uso do DAG para registrar o histórico do projeto. Consequentemente, essas operações podem ser usadas frequentemente e sem restrições.

Embora existam algumas diferenças entre o Mercurial e o Git quanto à ramificação e à mesclagem, não houve um que tenha se destacado mais nos dois quesitos ao mesmo tempo, e por isso houve um empate técnico nesta rodada.

No próximo artigo desta série de comparações tratará das semelhanças e diferenças entre comandos e o modelo de operação do Subversion e os do Mercurial e do Git.

This entry was posted in controle de versão, gerência de configuração de software, git, mercurial, subversion and tagged , , , , . Bookmark the permalink.

6 Responses to Comparação entre Subversion, Mercurial e Git – Parte 3

  1. Leandro says:

    Bom dia André, muito bom seu artigo, muito bom mesmo. Técnico, com boa abordagem, fácil de entender. Realmente o tipo de comparação que eu estava querendo ver. Esse com certeza valeu mais que os outros dois juntos e ainda me deixou com uma grande expectativa pelo próximo . :-D

    [ ]‘s

  2. André Felipe Dias says:

    Obrigado, Leandro. Mas a parte do “valeu mais do que os outros dois juntos” quase estragou os elogios. hehe

    Parece que o pessoal gosta de um empate técnico. Acho que reforça a ideia de que a escolha que fizeram foi boa, independente de qual tenha sido. Ninguém leva para o lado pessoal e fica tudo na paz.

    Infelizmente, não é o que vai acontecer na próxima análise.

  3. Robson Peixoto says:

    Problema ao executar o ‘hg log’

    $ hg log -Gqr default-bookmark:0
    hg log: option -G not recognized

    $ hg –version
    Mercurial Distributed SCM (version 2.2.2)

    $ port installed mercurial
    The following ports are currently installed:
    mercurial @2.2.2_0+bash_completion (active)

  4. André Felipe Dias says:

    Obrigado pelo aviso, Robson. Tinha esquecido de pedir para habilitar a extensão. Consertei o artigo.

  5. Leandro says:

    Oi André, o elogio foi mais pelo “técnico” do que pelo “empate” ehehehe. Pra falar a verdade, a conclusão foi o que menos me interessou. O que gostei foi da forma como você colocou os problemas e como cada ferramenta os resolveu ou não. Com certeza uma informação bem mais preciosa do que o número de páginas do help ou o tempo em milisegundos para fazer um commit. Tomara que na próxima análise tenha mais comparativos assim, pois o resultado pode até ser um empate técnico, mas o leitor vai sair sempre ganhando. Mais uma vez, parabéns pelo ótimo artigo! ;-)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>