quinta-feira, 26 de maio de 2011

Subversion - Mesclar Revisões

Introdução


O uso de branches é uma técnica muito recomendada para auxiliar o gerenciamento da linha de produção de um software. No entanto é comum a não utilização desta técnica pelos usuários do repositório pela dificuldade de aplicar as alterações no branch de volta ao trunk. Esta dificuldade se explica pelo desconhecimento do usuário da técnica de patch ou da ferramenta merge do próprio subversion. Ao longo deste artigo vamos desmitificar estas duas técnicas e definitivamente convencer do quanto pode ser simples a utilização de branches.

O comando merge possui outras sintaxes e utilidades além da apresentada aqui mas vamos nos ater a especificamente aplicar as alterações do branches de volta ao trunk.

Notação


Para simplificar o entendimento dos comandos que serão apresentados neste artigo vamos adotar alguns padrões. Todos os comandos que devem ser digitados na linha de comando serão precedidos do sinal de dólar ($). Exemplo:
$ svn status

As linhas seguintes aos comandos que não começam com o sinal de dólar representam a informação impressa no console pelo comando. Exemplo:
$ svn status
?       patch.txt
M       trunk/Main.java
M       trunk/Janela.java

Os caminhos de diretórios serão mostrados em geral com a notação do Linux, que usa o sinal de barra (/) para separar pastas. Certifique-se de inverter esta barra se estiver usando o Windows.
Por exemplo, este diretório:
branches/correcao
deverá ser corrigido no windows para:
branches\correcao

Preparação


Para facilitar o entendimento vamos desenvolver um projeto ao longo do artigo exemplificando cada etapa do processo. Os exemplos serão demonstrados com base na linha de comando do próprio subversion, por se muito simples e para evitar particularidades de ferramentas e plugins de terceiros. Abra um console do seu sistema operacional e siga os comandos passo-a-passo.

Para este passo-a-passo vamos precisar de um repositório local e de uma pasta de trabalho com o 'checkout' desse repositório. Não vou entrar em detalhes sobre os comandos do subversion, os detalhes podem ser consultados na repositórios, mas a grosso modo, o repositório pode ser criado da forma apresentada abaixo.

Linux

$ cd /home/seu_usuario_aqui
$ svnadmin create fs-type fsfs repo-svn
$ svn checkout file:///home/seu_usuario_aqui/repo-svn trab

Windows

$ c:
$ cd c:\
$ svnadmin create fs-type fsfs repo-svn
$ svn checkout file:///c:/repo-svn trab

Nota: A execução de comandos no console do Linux em geral é trivial uma vez o subversion instalado, no Windows no entanto pode ser necessário acrescentar os binários à variável de ambiente PATH afim de habilitar os comandos svn e svnadmin no prompt de comando. O subversion mantido pela CollabNet já configura as variáveis de ambiente corretamente.

No restante deste documento iremos apresentar diversos comandos a serem executados na linha de comando. Para simplificar a execução destes comandos executaremos todos eles à partir da pasta trab que criamos mais acima, por isso, será importante notar que sempre acrescentaremos aos comandos executados um parâmetro informando em qual ou quais sub-pastas o comando deverá ser aplicado.

Acesse a pasta trab. Todos os comandos de agora em diante serão executados a partir desta pasta.

$ cd trab

Vamos criar a estrutura inicial do projeto contendo as três pastas básicas trunk, tags e branches:
$ svn mkdir branches tags trunk
$ svn commit -m"Estrutura inicial do projeto"

Crie os arquivos Main.java e Janela.java dentro de trunk com seus respectivos conteúdos:

trunk/Main.java

1  class Main {
 2    public static void main(String[] args) {
 3      new Janela().setVisible(true);
 4    }
 5  }

trunk/Janela.java

1  import java.awt.*;
 2  import java.awt.event.*;
 3  import javax.swing.*;
 4  
 5  class Janela extends JFrame {
 6    Janela() {
 7      setLayout(new BorderLayout());
 8      setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
 9      
10      JLabel label = new JLabel("Hello World!");
11      add(label, BorderLayout.CENTER);
12      
13      JButton button = new JButton("Fechar");
14      button.addActionListener(new ActionListener() {
15        public void actionPerformed(ActionEvent e) {
16          dispose();
17        }
18      });
19      add(button, BorderLayout.SOUTH);
20      
21      pack();
22    }
23  }

Quando executado, o programa acima exibe uma janela com a mensagem "Hello World" e um botão para fechar a janela. O programa pode ser compilado e executado da seguinte forma:

$ javac trunk/*.java
$ java -cp trunk Main

Versione as classes e submeta as alterações ao repositório.

$ svn add trunk/*.java
$ svn commit -m"Criação da janela de mensagens"
$ svn update

Exemplo de aplicação da técnica de patch


Existe um clássico processo para a distribuição de correções de programas conhecido como patch. Este processo se encaixa perfeitamente na aplicação das alterações de branches para o trunk como veremos nessa seção.

Alterando o projeto no branch


Vamos começar realizando uma pequena mudança nos arquivos do nosso projeto. Estas mudanças serão feitas num branch e ao final aplicadas de volta ao trunk. Essa aplicação das alterações no trunk será feita através de patch e ao final submetidas a repositório. Para um estudo um pouco mais abrangente vamos simular uma segunda alteração diretamente na pasta trunk, que acontecerá durante a nossa primeira alteração, ou seja, ela será submetida ao repositório depois da criação do nosso branch porém antes da aplicação do nosso patch. O resultado esperado com esta segunda alteração é que a aplicação do patch não sobreponha esta segunda alteração. Ambas devem prevalecer após a submissão ao repositório. Este risco de perder a segunda alteração não existe de fato, vamos apenas mostrar que o patch é seguro.

De início vamos copiar a pasta trunk para branches/correcao.

$ svn copy trunk branches/correcao
$ svn commit -m"Preparação para a correção do projeto" 
$ svn update

No nosso código fonte criado mais acima a mensagem exibida na janela está fixa como "Hello World". Vamos alterar o programa para suportar uma mensagem personalizada, e vamos passar uma mensagem diferente no construtor da janela dentro do método main.

Aplique nos arquivos dentro da pasta branches/correcao as alterações listadas abaixo. Note que destacamos o número de cada linha para facilitar a localização da porção a ser alterada.

branches/correcao/Main.java

3      new Janela("Olá mundo!").setVisible(true);

branches/correcao/Janela.java

6    Janela(String mensagem) {
10      JLabel label = new JLabel(mensagem);

A alteração acima faz com que seja exibida uma mensagem diferente na janela. O programa pode ser executado da seguinte forma:
$ javac branches/correcao/*.java
$ java -cp branches/correcao Main

Ao final submeta as alterações ao repositório.
$ svn commit -m"Tornando a mensagem personalizável"
$ svn update

Simulando uma segunda alteração direto no trunk


Como dito anteriormente, vamos realizar uma segunda alteração, dessa vez diretamente na pasta trunk. Nossa alteração será centralizar a janela na tela. Esta alteração será feita em duas linhas com um cálculo bem tradicional.

Acrescente no arquivo Janela.java as duas linhas abaixo antes da instrução pack();.

trunk/Janela.java
26        Dimension tamanhoTela = Toolkit.getDefaultToolkit().getScreenSize();
27        setLocation((tamanhoTela.width-getSize().width)/2, (tamanhoTela.height-getSize().height)/2);

Submeta as alterações ao repositório.
$ svn commit -m"Centralizando a janela"
$ svn update

Gerando o patch com as alterações


Vamos agora gerar o patch para as nossas alterações realizadas dentro da pasta branches/correcao. O patch nada mais é que um arquivo texto contendo as diferenças das versões de cada arquivo antes e depois da alteração do projeto. Este arquivo com as diferenças pode ser gerado pela ferramenta svn diff. Note que o importante é informar para a ferramenta a revisão exata de quando a pasta branches/correcao foi criada e a revisão referente à submissão ao repositório da nossa última alteração, de forma a conseguir um patch contendo todas as nossas alterações feitas nessa pasta. Note que no nosso caso foi apenas uma alteração, tornar a mensagem personalizável, mas numa situação real provavelmente teríamos submetido mais de uma alteração neste mesmo branch. Informando as duas versões limites para o svn diff temos a certeza de todos as nossas alterações constarão no patch.

Nem sempre saberemos de cor o número da revisão no momento da cópia da pasta trunk para o branch, no entanto podemos usar a ferramenta svn log -v para consultar o histórico de submissões dessa pasta e identificar o momento exato da sua cópia. Observe o parâmetro -v passado para a ferramenta, com este parâmetro a ferramenta lista as entradas do histórico da pasta em modo detalhado, no nosso caso isto ajudará a identificar o momento da cópia da pasta. O log do nosso programa até este momento será parecido com o seguinte:

$ svn log -v branches/correcao

r4 | fernando | 2010-03-15 08:51:08 -0300 (Seg, 15 Mar 2010) | 1 linha
Caminhos mudados:
   M /branches/correcao/Janela.java
   M /branches/correcao/Main.java

Tornando a mensagem personalizável

r3 | fernando | 2010-03-15 08:46:39 -0300 (Seg, 15 Mar 2010) | 1 linha
Caminhos mudados:
   A /branches/correcao (de /trunk:2)

Preparação para a correção do projeto

r2 | fernando | 2010-03-15 08:44:33 -0300 (Seg, 15 Mar 2010) | 1 linha
Caminhos mudados:
   A /trunk/Janela.java
   A /trunk/Main.java

Criação ja janela de mensagens

r1 | fernando | 2010-03-15 08:39:16 -0300 (Seg, 15 Mar 2010) | 1 linha
Caminhos mudados:
   A /branches
   A /tags
   A /trunk

Estrutura inicial do projeto


Analisando esta saída temos a revisão 4 representando a nossa mais recente alteração na pasta branches/correcao. Note na revisão 3 a porção /branches/correcao (de /trunk:2), esta linha destaca exatamente o momento da cópia da pasta trunk para o branches. As nossas alterações na pasta branch estão portanto entre as revisões 3 e 4.

Note que numa situação real, será provavelmente listado um histórico muito mais longo do que este do nosso exemplo, dificultando imensamente a localização da revisão da cópia da pasta, no entanto, acrescentando-se o parâmetro stop-on-copy ao comando svn log serão listados apenas as entradas até o momento da cópia da pasta. Esta é certamente a forma mais fácil de se obter a revisão exata já que a última ou uma das últimas entradas listadas será provavelmente aquela que estamos procurando. No nosso exemplo a saída seria algo como:

$ svn log -v stop-on-copy branches/correcao

r4 | fernando | 2010-03-15 08:51:08 -0300 (Seg, 15 Mar 2010) | 1 linha
Caminhos mudados:
   M /branches/correcao/Janela.java
   M /branches/correcao/Main.java

Tornando a mensagem personalizável

r3 | fernando | 2010-03-15 08:46:39 -0300 (Seg, 15 Mar 2010) | 1 linha
Caminhos mudados:
   A /branches/correcao (de /trunk:2)

Preparação para a correção do projeto


Agora que conhecemos as revisões limite, 3 e 4, podemos gerar o arquivo patch com o comando abaixo. Nele será criado o arquivo patch.txt na raiz do nosso projeto contendo todas as alterações que fizemos na pasta branches/correcao.
$ svn diff -r 3:4 branches/correcao > patch.txt

Dica: Abra o arquivo patch.txt num editor de texto de sua preferência e examine o seu conteúdo para conhecer um pouco mais sobre este tipo de arquivo.

Aplicando o patch à pasta trunk


Agora que temos o patch contendo todas as nossas alterações na pasta branch é hora de aplicá-lo à pasta trunk. Neste processo usaremos uma ferramenta chamada patch. Esta ferramenta é capaz de ler as diferenças descritas no arquivo patch e aplicá-las aos respectivos arquivos numa pasta destino indicada, no nosso caso trunk/.

As distribuições Linux já costumam trazer esta ferramenta por padrão. Usuários do Windows podem instalá-la a partir do site GnuWin32. Provavelmente será necessário adicionar os binários do comando patch na variável de ambiente PATH. Em geral os binários são instalados em C:\Arquivos de programas\GnuWin32\bin.

Nota: O prompt de comando do Windows não é capaz de identificar automaticamente alterações em variáveis de ambiente, por isso, depois de alterar uma variável de ambiente lembre-se de fechar e abrir novamente o prompt de comando, ou simplesmente digite cmd.

Importante: Para uma aplicação de patches sem maiores problemas certifique-se de que não existam alterações pendentes na sua pasta trunk, caso existam, submeta-as ao repositório, e lembre-se de atualizar sua pasta trunk com o comando svn update antes de aplicar o patch. Estes dois passos evitam mensagens de erros durante a submissão das alterações ao repositório.

O comando patch lê e aplica o arquivo patch com o seguinte comando:
$ patch -d trunk < patch.txt
Note que agora consultando o estado da pasta de trabalho serão listados os dois arquivos alterados em trunk.
$ svn status
?       patch.txt
M       trunk/Main.java
M       trunk/Janela.java
Confira o conteúdo dos arquivos para ver como o comando patch aplicou as alterções lidas do patch.txt. Note que aquelas alterações que fizemos diretamente na pasta trunk foram mantidas. Submeta as alterações ao repositório.
$ svn commit -m"Aplicando as alterações de branches/correcao/ entre as revisões r3:r4 para o trunk/"
$ svn update
Como visto, a aplicação de patch é um processo bastante simples e prática. No entanto tente manter as alterações no branches o menor tempo possível. Quanto mais rápido as alterações no branch forem aplicadas ao trunk menor será a chance de ocorrem conflitos. Nota: Conflitos neste contexto são aquelas situações onde a mesma linha de um arquivo foi alterada por dois usuários diferentes. Conflitos levam o comando commit a falhar durante a submissão. Nestes casos cabe ao usuário que recebeu a falha corrigir o conflito, em geral consultando os demais usuários envolvidos no conflito, e submeter novamente as alterações.

Svn merge, mais simples que o patch

Apenas seguindo o procedimento de patch visto até agora já poderíamos trabalhar com pastas branches à vontade com a garantia de conseguirmos facilmente devolver as alterações para o trunk, no entanto, o comando svn merge simplifica ainda mais este procedimento. Na verdade, ele é capaz de fundir as duas etapas de um patch em uma só, ele gera a diferença e aplica o patch ao mesmo tempo. A sintaxe é bastante similar ao comando svn diff apresentado mais acima, acrescentando-se apenas a pasta destino. Para apresentar o merge na prática vamos criar uma nova alteração em branches. A este ponto do artigo os comandos utilizados pra manipulação do branch já devem ser familiares, por isso não vamos entrar em detalhes sobre eles. Crie o branch branches/nova_fonte:

$ svn copy trunk branches/nova_fonte
$ svn commit -m"Preparação para a troca da fonte"
$ svn update
Nossa alteração consiste em trocar a fonte do label onde a mensagem é exibida. Esta alteração será feita apenas no arquivo Janela.java da pasta branches/nova_fonte. branches/nova_fonte/Janela.java
11      label.setFont(new Font("Serif", 0, 36));
Submeta a alteração ao repositório.
$ svn commit -m"Trocando a fonte da mensagem exibida"
$ svn update
A este ponto já estamos pronto para aplicar a alteração da fonte feita em branches/nova_fonte de volta ao trunk. Vamos seguir o passo já apresentado para encontrar os números das revisões limites do nosso branch, que é basicamente investigar o histórico do branch com a ferramenta svn log -v stop-on-copy.
$ svn log -v stop-on-copy branches/nova_fonte

r8 | fernando | 2010-03-15 09:24:28 -0300 (Seg, 15 Mar 2010) | 1 linha
Caminhos mudados:
   M /branches/nova_fonte/Janela.java

Trocando a fonte da mensagem exibida

r7 | fernando | 2010-03-15 09:20:57 -0300 (Seg, 15 Mar 2010) | 1 linha
Caminhos mudados:
   A /branches/nova_fonte (de /trunk:6)

Preparação para a troca de fonte

Temos portanto as revisões limites 7 e 8. Entre elas está nossa alteração. Como dito, a montagem do comando svn merge é similar à do svn diff, acrescentando-se apenas a pasta destino, que no nosso caso é trunk. Vamos ao comando:
$ svn merge -r 7:8 branches/nova_fonte trunk
O merge será capaz de pegar as alterações em branches/nova_fonte entre as revisões 7 e 8 e aplicá-las à pasta trunk, mesclando os arquivos conforme necessário. Confira o arquivo alterado dentro da pasta trunk depois da execução desse merge, você vai notar que a troca da fonte que fizemos no branch já está no trunk. Nota: Para que o merge seja feito com a revisão mais recente do repositório é preciso informa HEAD em vez do número da revisão. Ex.:
$ svn merge -r 7:HEAD branches/nova_fonte trunk
Mescla as alteraçoes que foram feitas no branches da revisão 7 ate a revisão mais atual (HEAD) com a pasta trunk Há ainda mais um benefício em se usar o comando svn merge. O comando acrescenta à pasta destino, no nosso caso trunk, uma informação sobre a mescla realizada. Esta informação pode ser consultada posteriormente com o comando svn mergeinfo. Este comando exibe mesclas ocorridas ou que podem vir a ocorrer entre quaisquer pares de pastas. Note na saída do comando svn status a alteração na pasta trunk. Justamente nesta pasta foram acrescidas informações sobre a mescla:
$ svn status
?       patch.txt
 M      trunk
M       trunk/Main.java
M       trunk/Janela.java
Submeta as alterações ao repositório.
$ svn commit -m"Aplicando as alterações de branches/nova_fonte/ entre as revisões r7:r8 para o trunk/"
$ svn update

Conclusão

O uso de branch é uma importante prática na construção de softwares, mas costuma ser descartado pela falta de conhecimento da equipe no momento de retornar as alterações para o trunk. Como vimos neste artigo as práticas de patch e svn merge são triviais, facilmente implementáveis, e resolve com muita qualidade a devolução de alterações para o trunk. No entanto, estas técnicas não se limitam a devolver alterações para o trunk. É importante conhecer e praticar estes conceitos para tê-los como alternativa na elaboração dos processos de construção de software.

Referências