Por que você deveria usar git rebase

Uma receita para resolver conflitos e ser mais feliz

Há alguns 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: nós sofremos tentando resolver mega-conflitos 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.

Como surgem conflitos no Git

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 para 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:

Arquivo alterado por Maria:

Qual alteração deve prevalecer, a de João ou a de Maria? Não é possível, apenas com essas informações, decidir automaticamente qual é o resultado esperado.

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 de arquivos. Sendo assim, a melhor forma de lidar com conflitos é simplesmente… evitá-los! 😅

A melhor forma de lidar com conflitos é simplesmente… 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. 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 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 estão trabalhando no mesmo arquivo e fazem um commit em master cada um. Ao terminarem, ambos 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á push para o servidor primeiro, que é
    fast-forward;
  2. João tenta dar o push, que é rejeitado pelo servidor por não ser
    fast-forward;
  3. Ele então faz o pull (que compreende as operações de fetch + merge) para obter as últimas alterações do servidor remoto. Caso exista 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;
  4. Já com as últimas alterações do servidor remoto, João faz um novo Push,
    desta vez Fast-Forward;
  5. O servidor remoto agora contém as alterações de João e Maria.

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:

Histórico com muitos merges

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:

Histórico após um rebase

Usando o Rebase

O rebase é uma das funcionalidades mais incompreendidas do Git. Muita gente acha que ela é apenas uma alternativa ao merge e decide 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 é bem diferente.

No merge, as mudanças são trazidas por cima.

No merge, as mudanças são trazidas por cima, e um commit de merge é criado para adicionar todos os commits da branch que está sendo trazida.

Possíveis conflitos precisa ser resolvidos de uma só vez, e as alterações realizadas para a resolução dos conflitos ficam registradas neste merge commit.

Lidar com todos os conflitos entre duas branches de uma só vez pode ser bem complicado — o rebase torna essa resolução muito mais simples.

No rebase, as mudanças são trazidas por baixo.

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 dos commits da outra branch (que aqui estamos chamando master).

Os commits são, literalmente, realizados novamente sobre a nova base: daí o nome rebase (“troque a base”). Eles se tornam novos commits, com novos identificadores SHA1, porém com o mesmo conteúdo.

É possível que conflitos aconteçam durante a tentativa de aplicação de cada commit: nesse momento, o Git te dá a chance de resolver o conflito (escolhendo qual trechos de código deve prevalecer) e então retomar o
rebase.

Como os conflitos são resolvidos aos poucos, fica muito mais fácil de trabalhar. Ainda é importante não ficar muito atrás da branch master, porém, caso contrário você pode ter muitos conflitos para resolver… ainda que seja aos poucos, é um trabalho um pouco ingrato 😟

Terminado o rebase, os commits da branch atual já estarão contemplando as mudanças de master, como se o trabalho na branch tivesse sido feito depois! Em seguida, basta fazer o merge em master:

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 (refactor)

Para cada unidade de trabalho, deve ser criada uma feature branch, que irá
conter uma sequência de ações (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 Joana 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, master sofreu alterações desde quefeature/do-something foi criada. Sendo assim, é necessário fazer um rebase para aplicar as mudanças da feature branch em cima da nova realidade demaster:

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

Após o rebase, Joana volta master para fazer o merge:

$ 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:

Histórico após rebase (de novo!)

Não há nada de errado com essa visualização, mas há como aumentar ainda mais a legibilidade do histórico, usando a opção --no-ff na hora de fazer o merge:

$ 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. Neste caso, 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. Demos a essa visualização o apelido informal de barriguinha: um repositório com barriguinhas é um repositório saudável ✨.

Histórico com rebase e merge no-ff (barriguinhas!)

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 e 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 eles devem ser resolvidos.

Existe vida após o merge (e antes também!)

Hoje, temos mais de 150 repositórios Git da VTEX, que guardam desde o
código-fonte dos nossos sistemas até documentação dos times. É 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.

O rebase já se tornou a forma de trabalho padrão no dia-a-dia da VTEX. Considere usá-lo antes de realizar o seu próximo merge!


Este artigo foi publicado originalmente no blog do VTEX Lab