quinta-feira, 31 de março de 2011

Mercurial Hg - Controlando as Versões do seu Software

Bom dia pessoal! Estou aqui escrevendo um pouco sobre sistemas de controle de versão de codigo fonte. Eu pessoalmente comecei a mexer com controle de versões há mais ou menos um ano, por conta dos vários trabalhos da faculdade. Comecei com o SVN e depois mudei para o GIT por um curto espaço de tempo e hoje estou no Mercurial. Antes disso, já tinha tido uma experiência maravilhosa com o Dropbox, no sentido que poderia facilmente recuperar versões de arquivos hospedados no serviço.

E aqui estamos. Hoje vou falar um pouco sobre a teoria por trás dos sistemas de controle de versão e então falar sobre o Mercurial, o sistema que atualmente estou utilizando. Espero que vocês gostem do material.

Controle de Versões de Software

Há muito tempo atrás, quando não existiam máquinas mais potentes que uma calculadora, o controle de versões de códigos-fonte era feito de uma maneira bem eficiente. Somente uma pessoa por vez podia acessar o código. Não havia problemas de versões diferentes de código. Até então...


Com a chegada das máquinas em rede, começaram os problemas com o código. Acho que até hoje ainda existem empresas que usam uma solução de compartilhamento de pasta pública para desenvolver projetos. Essa solução ad-hoc pode até funcionar para pequenas equipes, mas cresça a empresa e você criará um enorme problema, uma bomba relógio que uma hora ou outra irá explodir. E se você for o gerente de projetos, a única explicação é vc gostar de emoções.


Quantas vezes já aconteceu de você escrever um código, que depois foi sobrescrito por outra pessoa? Se seu arquivo sobreviveu à essa fase, você já deve ter percebido, também, como é difícil saber o que foi alterado depois de um dia de trabalho em vários arquivos. E depois de editar e ver que seu novo código não funcionou, a dificuldade que foi voltar para a versão estável? Ou pior ainda, a dificuldade que é manter duas versões do mesmo sistema, por exemplo, versão estável e versão de desenvolvimento.

Hoje, o uso de sistemas de controle de versões e de rastreamento de modificações é inevitável, se você quer manter o controle sobre o que você desenvolve. E não digo só para empresas, pois um desenvolvedor freela pode tirar vantagem de recursos como registro de alterações ou criação de ramos para teste de novas funcionalidades.

Enfim, o controle de versões apoia o desenvolvimento nos seguintes aspectos:

Histórico. Registrando toda a evolução do projeto, cada alteração sobre cada arquivo, sabe-se quem fez o que, quando e onde. Além disso, permite reconstruir uma revisão específica do arquivo sempre que desejado;

Colaboração. O controle de versão possibilita que vários desenvolvedores trabalhem em paralelo sobre os mesmo arquivos sem que um sobrescreva o código de outro, o que faria com que defeitos reaparecimento e funcionalidades fossem perdidas;

Variações no Projeto. Mantém linhas diferentes de evolução do mesmo projeto. Por exemplo, mantendo uma versão 1.0 enquanto a equipe de desenvolvimento prepara uma versão 2.0.

Como funciona?

Basicamente, um sistema de controle de versões funciona utilizando o conceito de repositório. O repositório é um arquivo (ou local, dependendo do sistema utilizado) onde todo o histórico de evolução do projeto é armazenado, para cada item que é monitorado pelo sistema.

O desenvolvedor não trabalha diretamente no repositório. Ao invés disso ele usa uma área que possui uma cópia de todos os arquivos que são monitorados pelo sistema de controle de versões. Nesta área, que podemos chamar de "área de trabalho", é que fazemos as alterações nos arquivos e é nesta mesma área que o sistema monitorará os arquivos para verificar alterações.

O monitoramento do sistema de controle de versões se dá através de duas operações básicas: commit e update. O update é feito para atualizar a área de trabalho com a versão mais recente dos arquivos do repositório enquanto o commit é feito para enviar as alterações na área de trabalho para o repositório. A sintaxe de uso dessas duas operações varia de sistema para sistema, mas a idéia é essencialmente esta. Outras operações como carregar uma versão anterior de um arquivo ou verificar as alterações em um arquivo podem estar disponíveis.

Existe uma diferenciação em relação a forma como o repositório e a área de trabalho estão dispostos. Existem duas formas de gerenciar as alterações da área de trabalho. Uma delas é utilizar um repositório central e acessível por todos os desenvolvedores, enquanto a outra forma é a utilização de repositórios locais, que podem ser sincronizados com outros repositórios remotos, podendo ser estes quaisquer outros repositórios, inclusive de outros desenvolvedores.

Isto nos leva aos dois modelos de gerenciamento de versões mais comuns, o controle de versões centralizado e o descentralizado.

O controle de versões centralizado segue uma topologia em estrela, havendo apenas um único repositório central mas várias cópias de trabalho, uma para cada desenvolvedor. A comunicação entre uma área de trabalho e outra passa obrigatoriamente pelo repositório central.

O controle de versões distribuído segue uma arquitetura peer-to-peer, onde temos vários repositórios autônomos e independentes, um para cada desenvolvedor. Cada repositório possui uma área de trabalho acoplada e as operações commit e update acontecem localmente entre os dois.

Uma diferença estrutural do sistema distribuído em relação ao sistema centralizado é a existência de duas operações adicionais, responsáveis por atualizar o repositório local ou o remoto. Os comandos são o push e o pull:

Pull (Puxar). Atualiza o repositório local (destino) com todas as alterações feitas em outro repositório (origem).
Push (Empurrar). Envia as alterações do repositório local (origem) para um outro repositório (destino).

A sincronização entre os desenvolvedores acontece de repositório a repositório e não existe, em princípio, um repositório mais importante que o outro, embora o papel de um repositório central possa ser usado para convencionar o fluxo de trabalho.

Mercurial

Assim como o GIT, o Mercurial é um sistema distribuído de controle de versões, ao passo que o CVS e do SVN são sistemas centralizados de controle de versões.

Trabalhando com repositórios locais

Para se trabalhar com o Mercurial, primeiramente precisamos configurar um nome de usuário que o Mercurial usará para salvar as alterações no código fonte. Para isso, é necessário criar um arquivo que deverá conter a seguinte informação:

[ui]
username = Seu Nome <seu@email.com.br>

Este arquivo pode ser criado em vários lugares diferentes. Para criar um arquivo global para todos os projetos, o mesmo pode ser criado em /home/usuario/.hgrc e caso se queira personalizar o repositório, a informação pode ser adicionada no arquivo .hg/hgrc que é criado dentro da pasta do repositório.

Depois de salvo este arquivo, poderemos fazer nossos commits normalmente. Se você não criar este arquivo, ao chamar o comando hg commit a seguinte mensagem aparecerá em sua tela: "abortado: nome de usuário não fornecido (veja "hg help config")". Então, caso esta apareça, já sabe como proceder.

Dando prosseguimento ao nosso tutorial, para iniciar o uso do Mercurial, existem duas ações que você pode executar: criar um novo repositório ou criar uma copia local de um outro repositório existente.

Criando um repositório

Supondo que você já tenha algum código fonte que você queira controlar, você pode criar o repositório local usando os seguintes comandos:

hg init
hg add *
hg commit

Carregando um repositório a partir de uma outra cópia

Se um repositório já existe, é possível fazer uma cópia local deste, que utilizaremos para trabalhar. Para isto, usamos o seguinte comando:

hg clone <origem> <destino>

Onde <origem> indica o repositório que estamos querendo copiar e <destino> a pasta onde esse repositório será colocado. Ao executar este comando, nosso repositório será idêntico ao repositório copiado.

Verificando o status do repositório

Depois de criar ou copiar o repositório, nós temos três comandos que podem dar informações a respeito do status e das operações realizadas no mesmo. Os comandos são os seguintes:

hg log

Mostra todas as revisões já efetuadas naquele repositório.

hg tip

Mostra a última revisão efetuada no repositório

hg status

Mostra pendências que ainda não foram resolvidas (arquivos adicionados/excluídos/alterados dentro da área de trabalho que ainda não foram commitados)

Brincando de programar

Agora que falamos um pouquinho do Mercurial e mostramos alguns comandos básicos, vamos brincar um pouco com a ferramenta. Esta é a melhor forma de se aprender a trabalhar com ela, já que não é necessário levar nada a sério neste ponto e experiências podem ser feitas sem que haja um compromentimento de nenhum código de produção real.

Então vamos lá. Primeiramente vamos criar uma pasta onde ficarão nossos repositórios. Vamos supor que temos dois desenvolvedores (eduardo e tatiane) e um repositório principal (veja bem, ele não é um repositório "central" e sim um respositório comum para o qual foi delegada a função de ser o repositório oficial da equipe de desenvolvimento). Vamos então criar os mesmos e começar a operar nestes.

Primeiramente vamos criar o nosso repositório principal:

$ mkdir repositorio
$ cd repositorio

Em seguida vamos criar um arquivo qualquer, em Python, para ser nosso código fonte primário:

$ echo 'print ("Haro, sekai!")' > programa.py

Depois de criado nosso primeiro arquivo, vamos transformar esta pasta em um repositório Mercurial e vamos começar a controlar o arquivo que criamos:

$ hg init
$ hg add programa.py

Depois, vamos editar o arquivo .hg/hgrc (crie, caso ainda não exista) e adicionar as seguintes informações e depois salve o arquivo:

[ui]
username = "José (Mantenedor)" <jose@dev.com>

[extensions]
hgext.graphlog =
color=

Peraí, essa extensions é nova! O Mercurial permite você adicionar extensões que auxiliem na tarefa de gerenciar o repositório. Entre as várias ferramentas, a graphlog permite uma identificação visual de como estão organizadas as alterações no repositório e a color adiciona cores na exibição dos comandos no terminal. Existem várias outras extensões que vocês podem consultar no site mercurial.selenic.com

Depois de criado nosso arquivo, podemos fazer nosso primeiro commit, que gerará a primeira revisão do nosso repositório.

hg commit -m "Commit inicial. Adicionado o arquivo programa.py"

Agora, vamos sair do nosso repositório principal e criar nosso primeiro repositório de desenvolvimento.

$ cd ..
$ mkdir eduardo
$ cd eduardo/

Agora, nós queremos trabalhar com os arquivos que estão no repositório. Então, vamos copiar o mesmo para nossa pasta recém criada usando o comando clone do Mercurial:

hg clone ../repositorio .

Lembre-se que o repositório é a pasta onde está contida uma outra pasta, chamada .hg. Esta pasta armazena todas as informações do repositório que estamos trabalhando.

Agora que fizemos uma cópia do repositório principal, vamos editar o arquivo hgrc em .hg com as informações do programador eduardo, da seguinte maneira:

[ui]
username = "Eduardo (Programador)" <edu@dev.com>

[extensions]
hgext.graphlog =
color=

Agora, podemos editar nossos arquivos à vontade. Vamos supor que eu fiz uma alteração no arquivo que criamos, e adicionei uma linha.

$ echo 'print "Alteração do Eduardo Rolim"' >> programa.py
$ hg commit -m "Adicionada nova linha no arquivo programa.py"

Se eu rodar o comando hg log aqui, aparecerá a revisão inicial, criada pelo José quando criou o repositório e a revisão relacionada à alteração do arquivo programa.py. No entanto, esta alteração ainda não foi enviada para o servidor principal. Para saber o que estaremos enviando para o repositório principal podemos usar o comando:

$ hg outgoing ../repositorio

Este comando irá mostrar todos os changesets que devem ser enviados para o servidor para manter este atualizado. Para enviar todos os changesets, pode ser usado o comando:

$ hg push ../repositorio

Agora, vamos criar um novo programador, que irá trabalhar em conjunto com o eduardo no desenvolvimento da aplicação. Para isso, vamos sair da pasta atual e criar a pasta onde ficará o repositório do outro programador:

$ cd ..
$ mkdir tatiane
$ cd tatiane
$ hg clone ../repositorio .

Da mesma forma que fizemos com o repositório do eduardo, vamos editar o arquivo hgrc que está dentro da pasta recém criada do repositório, .hg e configurar as informações de usuário do novo programador:

[ui]
username = "Tatiane (Programadora)" <tati@dev.com>

[extensions]
hgext.graphlog =
color=

Salve o arquivo. Agora, vamos fazer mais algumas alterações.

$ echo 'print "Olá, sou a Tati!"' >> programa.py
$ hg commit -m "Adicionado print da Tatiane"

Vamos agora enviar a alteração da Tatiane para o repositório principal. Lembrando que o repositório principal não é o central. Todos os repositórios têm a mesma hierarquia. O mesmo é chamado de principal por convenção, de forma a poder centralizar o desenvolvimento.

$ hg push ../repositorio

Pronto. Nosso repositório agora possui vários updates de vários programadores. Agora, suponhamos que enquanto a tatiane estava desenvolvendo suas alterações, o eduardo adicionou mais funcionalidades e tentou enviar suas alterações depois que a tatiane enviou as dela:

$ cd ../eduardo
$ echo 'print "Linha extra"' >> programa.py
$ hg commit -m "Adicionada uma linha extra"

Até aqui tudo bem. Nosso repositório local é desvinculado do repositório principal. São dois repositórios diferentes e independentes. Agora, se tentarmos enviar as revisões de eduardo, elas falharão pois a revisão que está sendo enviada é baseada em uma revisão não mais ativa. Podemos verificar isso com o comando hg outgoing:

$ hg out ../repositorio

O comando informará que há somente uma revisão para enviar. Então vamos enviar:

$ hg push ../repositorio

Se você olhar, não foi possível enviar nossa alteração para o servidor pois há uma alteração mais nova no repositório principal. Em resumo, temos duas versões de nosso arquivo. Uma que está no repositório principal e outra que se encontra no nosso repositório. O que faremos agora é trazer esta alteração para o repositório local e vamos tentar resolver o conflito:

$ hg pull ../repositorio

Este comando irá trazer os changesets que foram criados depois da última vez que enviamos algo para o repositório principal. Este comando é a contra-partida do comando push. O comando push envia as alterações do local para o remoto enquanto o pull puxa as alterações do remoto para o local.

Existe uma regrinha em inglês pra não misturar estes comandos:
Se está escrito "pull", não pule, puxe;
Se está escrito "push", não puxe, empurre.


Agora, temos tanto os nossos changesets quanto os changesets do outro programador. Podemos verificar quais changesets estão em conflito usando o comando:

$ hg heads

Ao contrário o CVS e do SVN, o Mercurial controla todas as alterações feitas no código fonte e os armazena no changeset, que é trocado entre os repositórios. Desta forma, caso os conflitos ocorram em partes diferentes do nosso arquivo, o Mercurial irá resolver a dependência automaticamente, através de um comando que une as duas vertentes do código:

$ hg merge

Infelizmente no nosso código, a operação de merge não foi bem-sucedida pois na mesma linha há duas informações diferentes nas duas vertentes do arquivo.

Se abrirmos o arquivo agora, você vera que linhas foram adicionadas dentro do arquivo. Se você está seguindo exatamente este tutorial, o arquivo deve estar com a seguinte configuração:

print ("Haro, sekai!")
print "Alteração do Eduardo Rolim"
<<<<<<< local
print "Mais uma alteração"
=======
print "Olá, sou a Tati!"
>>>>>>> other

Nós podemos verificar que o arquivo foi alterado e está aguardando para ter as dependências resolvidas ao usar o comando hg status.

Para resolver o conflito, nós vamos editar o arquivo, remover as marcas indicativas de conflito do código. Caso seja necessário acessar o arquivo original, o Mercurial cria um arquivo com o mesmo nome do arquivo em conflito, com a adição de .orig. Vamos resolver o conflito. Editemos o arquivo de forma que este fique com a seguinte configuração:

print ("Haro, sekai!")
print "Alteração do Eduardo Rolim"
print "Mais uma alteração"
print "Olá, sou a Tati!"

E vamos informar para o Mercurial que o conflito foi resolvido:

$ hg resolve -m programa.py

Conflito resolvido, vamos então commitar a alteração em nosso repositório local:

$ hg commit -m "Resolvendo conflito de versões do arquivo programa.py"

Se dermos o comando hg log, veremos que a última revisão possui dois pais. Isso significa que juntamos os dois ramos conflitantes em um único changeset com todos os conflitos resolvidos. Se executarmos o comando hg glog em nosso repositório, veremos uma estrutura como a seguinte:

@    revisão:   4:edd0e8850a4d
|\   etiqueta:    tip
| |  pai:         2:c8a123af6e2b
| |  pai:         3:281247201add
| |  usuário:     "Eduardo (Programador)" <edu@dev.com>
| |  data:        Thu Mar 31 20:03:34 2011 -0300
| |  sumário:     Resolvendo conflito de versões do arquivo programa.py
| |
| o  revisão:   3:281247201add
| |  pai:         1:b5b31e53b893
| |  usuário:     "Tatiane (Programadora)" <tati@dev.com>
| |  data:        Thu Mar 31 19:21:01 2011 -0300
| |  sumário:     Adicionado print da Tatiane
| |
o |  revisão:   2:c8a123af6e2b
|/   usuário:     "Eduardo (Programador)" <edu@dev.com>
|    data:        Thu Mar 31 19:34:05 2011 -0300
|    sumário:     Adicionada uma linha extra
|
o  revisão:   1:b5b31e53b893
|  usuário:     "Eduardo (Programador)" <edu@dev.com>
|  data:        Thu Mar 31 19:09:01 2011 -0300
|  sumário:     Adicionada nova linha no arquivo programa.py
|
o  revisão:   0:198b1491804b
usuário:     "José (Mantenedor)" <jose@dev.com>
data:        Thu Mar 31 18:53:21 2011 -0300
sumário:     Primeiro commit. Adicionado arquivo programa.py

Aqui podemos ver exatamente onde houve o fork e onde houve o merge em nosso código. Para finalizarmos, vamos enviar nossa alteração para o repositório principal:

$ hg push ../repositorio

E finalmente, a tatiane vai atualizar seu repositório local com as alterações promovidas pelo eduardo:

$ cd ../tatiane
$ hg pull ../repositorio
$ hg update

Aqui entra um novo comando. O comando push trouxe as alterações, mas elas ainda não serão aplicadas na nossa área de trabalho. O comando update faz isso: ele verifica os changesets e caso houvesse algum arquivo alterado pela tatiane que estivesse em conflito com o changeset enviado por eduardo, iríamos ter de fazer o mesmo processo de corrigir os possíveis conflitos que surgissem e em seguida fazer o merge.

Visualizando o histórico de alterações de um arquivo

Bom, falamos de todo o processo de gerenciamento de versões do código e como tratar conflitos em versões diferentes do mesmo arquivo. No entanto, depois de um bom tempo de desenvolvimento, talvez você tivesse interesse em saber qual era o conteúdo de um arquivo em uma determinada revisão. Para isso vamos usar o comando hg cat:

$ hg cat -r 4 programa.py 
print ("Haro, sekai!")
print "Alteracao do Eduardo Rolim"
print "Mais uma alteracao"
print "Ola, sou a Tati!"

$ hg cat -r 3 programa.py 
print ("Haro, sekai!")
print "Alteracao do Eduardo Rolim"
print "Mais uma alteracao"

$ hg cat -r 2 programa.py 
print ("Haro, sekai!")
print "Alteracao do Eduardo Rolim"
print "Ola, sou a Tati!"

$ hg cat -r 1 programa.py 
print ("Haro, sekai!")
print "Alteracao do Eduardo Rolim"

$ hg cat -r 0 programa.py 
print ("Haro, sekai!")

Como você pode ver, o comando vai listar o conteúdo do arquivo na revisão especificada. Também é possível verificar o que foi alterado em um arquivo entre duas revisões ou o que uma revisão alterou em relação à revisão anterior.

Para verificar o que foi alterado em uma determinada revisão podemos usar o seguinte comando:

$ hg diff -c 2

Este comando irá mostrar as alterações que foram feitas na revisão 2, em formato específico do comando diff. Podemos também comparar duas revisões e as diferenças que há entre elas, com o seguinte comando:

$ hg diff -r 2 -r 4 programa.py

O comando acima comparou as revisões 2 e 4 e o que há de diferente entre as duas.

Conclusão

Como vocês podem ver o Mercurial, assim como outros sistemas de controle de versão, permitem que você gerencie o seu código, evitando que desastres possam vir a ocorrer por algum programador descuidado ter, por exemplo, perdido todas as alterações que ele fez porque o arquivo estava sendo editado por dois programadores ao mesmo tempo.

Estes são os recursos básicos da ferramenta e um ciclo simples de desenvolvimento. Há muito mais a ser explorado no Mercurial, tanto em comandos básicos da ferramenta quanto através de plugins especiais para as mais diversas tarefas, como gerenciamento de patches, rebaseamento de código, integração com ferramentas de controle de alterações do código como o Trac e o Bugzilla, e outros por aí.

Como eu disse, não há porque hoje não utilizar um sistema comoo Mercurial, mesmo que você esteja somente desenvolvendo individualmente. Um exemplo de uso para o programador freela é o de poder criar diferentes versões da aplicação em desenvolvimento, para testar novas funcionalidades sem prejudicar o desenvolvimento da aplicação. Caso as novas funcionalidades "funcionem", você pode enviá-las para o repositório de desenvolvimento sem nenhum problema. Caso não funcionem, é só descartar a versão.

Espero que esse post tenha ajudado a elucidar um pouco mais sobre esse mundo novo que é o do controle de versões. Espero num futuro próximo explicar um outro recurso, que é o controle de alterações de código fonte e mapeamento de bugs: o Trac ou o Bugzilla. Até mais!

Fontes:

Mercurial Wiki
Trabalhando com Mercurial - Hg
Mercurial - The Definitive Guide
Conceitos Básicos de Controle de Versão de Software — Centralizado e Distribuído