Como usamos o Git: Rebase

por: Alex Tercete   July 3, 2014 13:00 em Engineering

Há cerca de dois anos, nós migramos o código-fonte de todos os nossos sistemas
do TFS para o Git.

Como era de se esperar, a migração não ocorreu de forma suave. Demorou um tempo até todos se acostumarem com a nova ferramenta — e mais ainda até começarem a tirar proveito do que ela tem a oferecer. Nós sofremos tentando resolver
megaconflitos de Merge, e aprendemos — do jeito difícil — a importância das Feature Branches.

Um dos maiores benefícios de um Sistema de Controle de Versão (SCV) é permitir que mais de uma pessoa trabalhe em um mesmo conjunto de arquivos, ao mesmo tempo. Por outro lado, o trabalho colaborativo é o causador da principal
dor de cabeça ao se trabalhar com SCVs: os conflitos.

Conflitos, muitos conflitos

No início, com a intenção de facilitar a adoção da ferramenta, tentamos usar o Git da mesma forma que usávamos o TFS. Essa experiência foi equivalente a usar um martelo para colocar um parafuso na parede: funcionava, mas não parecia certo.

Em um dos nossos projetos, tínhamos duas equipes trabalhando em Branches diferentes, que correspondiam às versões de Produção e de Desenvolvimento do sistema em questão, que estava sendo redesenhado. Com alguma frequência, precisávamos trazer as modificações feitas na Branch de Produção para a Branch de Desenvolvimento. E então aconteciam os conflitos. Muitos conflitos.

Como surgem os conflitos

Os conflitos surgem quando duas pessoas fazem alterações em um mesmo arquivo. No caso de arquivos de texto, esses conflitos ocorrem quando as duas partes alteram a mesma linha do arquivo. Existem ferramentas que têm o intuito de resolver esses conflitos de forma automática, mas em alguns casos, isso não é possível.

Por exemplo, suponha que João e Maria fizeram as seguintes alterações no mesmo arquivo:

Arquivo alterado por João:

Essa é uma linha
-Essa é outra linha
+Essa é outra linha, alterada

Arquivo alterado por Maria:

Essa é uma linha
-Essa é outra linha
+Essa é mais uma linha

Não é possível, apenas com essas informações, decidir qual dos resultados a seguir é esperado para a combinação (também conhecida como Merge) das alterações:

  • Essa é outra linha, alterada
  • Essa é mais uma linha
  • Essa é mais uma linha, alterada

Quando isso acontece, é necessário que o usuário resolva os conflitos um a um — manualmente, ou através de uma ferramenta específica pra esse fim. Essa resolução de conflitos é um processo delicado e tedioso, principalmente quando o conflito ocorre em dezenas — no nosso caso eram centenas — de arquivos.

Sendo assim, a melhor forma de lidar com conflitos é evitá-los.

Como evitar os conflitos no Git

A má notícia é que os conflitos não podem ser evitados. A boa notícia é que é possível melhorar a qualidade dos conflitos e diminuir a frequência com que eles ocorrem.

Não faça Pull

O maior causador de conflitos no Git é o git pull. Infelizmente, esse é um dos primeiros comandos ensinados na maioria dos tutoriais básicos sobre Git, por se encaixar mais no fluxo de trabalho com o qual os iniciantes (que geralmente usam CVS, SVN ou TFS) estão familiarizados.

Sendo assim, a tendência de quem chega ao Git é fazer Commits diretamente na Branch master.

Por exemplo, João e Maria fazem um Commit em master cada um, e decidem fazer o Push para o servidor remoto. A sequência de ações que ocorre em seguida é:

  1. Maria é mais rápida, e dá logo o Push para o servidor, que é Fast-Forward;
  2. João tenta dar o Push, que é rejeitado pelo servidor por não ser Fast-Forward. Ele, então, faz o Pull (que compreende as operações Fetch e Merge), para obter as últimas alterações do servidor remoto. Caso haja conflito entre suas alterações e as de Maria, ele deverá resolvê-los. Independente da existência de conflitos, um Commit de Merge será gerado;
  3. Já com as últimas alterações do servidor remoto, João faz um novo Push, desta vez Fast-Forward;
  4. O servidor remoto agora contém as alterações de João e Maria.

1-steps

Com o tempo, esse fluxo de trabalho deixa o histórico do repositório difícil de
entender, principalmente se tiver mais gente trabalhando nele:

2-confusing-history

Uma forma de evitar esse caos é usar git pull --rebase (que executa Rebase
ao invés de Merge após realizar o Fetch). Isso fará com que o histórico
fique linear (sem Commits de Merge), agrupando as alterações por autor:

3-rebased

Use o Rebase

O Rebase é uma das funcionalidades mais incompreendidas do Git. Muita gente
acha que ela é apenas uma alternativa ao Merge, e opta por não usá-la.

É verdade que o Rebase tem a mesma função do Merge: trazer as mudanças
de outra Branch para a Branch atual. No entanto, a forma como as mudanças
são trazidas é bastante diferente.

No Merge, as mudanças são trazidas por cima. Isso significa que, caso haja
Commits na outra Branch que não existam na Branch atual, um Commit de
Merge será criado. Os possíveis conflitos devem ser resolvidos de uma só vez,
e as alterações realizadas para a resolução dos conflitos ficam registradas no
Commit de Merge.

Lidar com todos os conflitos entre duas Branches de uma só vez é mais
complicado do que parece. No nosso caso, não era raro um Merge malsucedido
causar a perda de mudanças feitas em uma das Branches envolvidas. Para ser
justo, o processo de Merge do Git requer que alguns conceitos básicos estejam
claros, como o uso da área de Staging para preparação do Commit, mas isso já
é assunto pra outro Post.

No Rebase, as mudanças são trazidas por baixo. Isso quer dizer que os
Commits realizados na Branch atual são aplicados um a um em cima do último
Commit da outra Branch. Os Commits são, literalmente, realizados novamente
sobre a nova base (daí o nome Rebase: “troque a base”); isto é, são novos
Commits (com novos identificadores SHA1), porém com o mesmo conteúdo. Os
possíveis conflitos irão ocorrer durante a tentativa de aplicação de cada
Commit, quando será dada a chance de resolver o conflito, e então retomar o
Rebase; ou seja, os conflitos são resolvidos aos poucos. Terminado o Rebase,
os Commits da Branch atual já estarão contemplando as mudanças da outra
Branch, como se o trabalho na Branch atual tivesse começado após o
trabalho na outra Branch ter terminado.

4-rebase-and-merge

Usar Rebase faz com a história seja mais legível, mantendo os Commits
agrupados por autor. No entanto, existe uma forma melhor de agrupar os Commits
para permitir ainda mais contexto para quem está lendo a história: o agrupamento
por finalidade.

Crie Feature Branches

Independente da natureza de um sistema, sempre é possível dividir as tarefas que
nele precisam ser desempenhadas em pequenas unidades de trabalho. Essas unidades
podem ter três finalidades básicas:

  1. Introduzir novas funcionalidades
  2. Corrigir comportamento inesperado
  3. Alterar a forma, mantendo o comportamento

Para cada unidade de trabalho, deve ser criada uma Feature Branch, que irá
conter uma sequência de ações (na forma de Commits) necessárias para que a
finalidade seja atingida. O nome da Branch deve ser prefixado com o tipo de
alteração que será realizada: feature, bug ou refactor.

Ao combinar o uso de Feature Branches com a prática do Rebase, é possível
agrupar os Commits por finalidade.

Por exemplo, suponha que João tenha criado a Branch feature/do-something a
partir de master. Depois de alguns Commits, a funcionalidade já está pronta
para ser entregue. No entanto, a Branch master sofreu alterações desde que
feature/do-something foi criada. Sendo assim, é necessário fazer um Rebase
para aplicar as mudanças da Feature Branch em cima da nova realidade de
master:

$ git checkout feature/do-something
$ git rebase master

Após o Rebase, o Merge pode ser realizado:

$ git checkout master
$ git merge feature/do-something

Como o Merge é Fast-Forward, nenhum Commit de Merge é criado. Após
repetir o processo algumas vezes, o histórico fica parecido com isso:

3-rebased

Apesar do agrupamento por finalidade ser melhor do que o agrupamento por autor,
há como aumentar ainda mais a legibilidade do histórico, usando a opção
--no-ff na hora de fazer o Merge:

$ git checkout feature/do-something
$ git rebase master
$ git checkout master
$ git merge --no-ff feature/do-something

Isso irá forçar a criação do Commit de Merge, mesmo que ele não seja
necessário (já que o Merge pode ser Fast-Forward). O Commit de Merge
serve para formalizar a existência [e o fim] da Feature Branch.

O resultado é um repositório fácil de ler, independente da quantidade de pessoas
trabalhando nele:

5-rebased-history-noff

Fazer o Rebase com frequência torna o processo de resolução de conflitos mais
simples. Quantas vezes perdemos dias inteiros resolvendo conflitos de Merge!
Para piorar, o fardo do Merge ficava a cargo de quem tinha mais
coragem paciência para fazê-lo. Hoje, esse esforço é diluído: quem
cria a Feature Branch tem a responsabilidade de mantê-la atualizada em relação
à master. Lidamos com conflitos muito menores, e quem os resolve é quem tem
mais condições de julgar como esses conflitos devem ser resolvidos.

Existe vida após [e antes d]o Merge

Hoje, temos mais de 150 repositórios Git no VTEX Lab, que guardam desde o
código-fonte dos nossos sistemas até os Posts deste Blog. É comum que uma só
pessoa interaja diariamente com diversos repositórios diferentes. Sendo assim, é
imprescindível que o histórico de mudanças seja fácil de ler e entender.

Muitas das nossas equipes já adotaram o Rebase no dia-a-dia. Considere usá-lo
antes de realizar o seu próximo Merge!

Gostou? Estamos contratando!