O comando git rebase tem uma reputação de ser o vodu mágico do Git do qual os iniciantes devem ficar longe, mas ele, na verdade, pode facilitar muito a vida para uma equipe de desenvolvimento quando usado com cuidado. Neste artigo, vamos comparar o git rebase com o comando git merge relacionado e identificar todas as oportunidades potenciais para incorporar o rebase ao fluxo de trabalho típico do Git.

Visão geral conceitual

A primeira coisa a ser entendida sobre o git rebase é que resolve o mesmo problema que o git merge. Ambos os comandos são desenvolvidos para integrar alterações de uma ramificação para outra—eles só fazem isto de formas muito diferentes.

Considere o que acontece quando você começa a trabalhar em uma nova funcionalidade em uma ramificação dedicada e, em seguida, outro membro da equipe atualiza a ramificação mestre com novos commits. Isto resulta em um histórico bifurcado, o que deve ser familiar a qualquer pessoa que já usou o Git como ferramenta de colaboração.

Um histórico de confirmação bifurcada

Agora, digamos que os novos commits no mestre sejam relevantes à funcionalidade na qual você está trabalhando. Para incorporar os novos commits na sua ramificação de funcionalidade, você tem duas opções: mesclagem ou rebase.

A opção Mesclar

A opção mais fácil é realizar o merge do branch mestre com o branch de recursos usando algo parecido com o seguinte:

git checkout feature
git merge master

Ou você pode condensar isso em uma linha de comando:

git merge feature master

Assim você cria uma nova “confirmação de mesclagem” no branch de recursos que se une aos históricos de ambos os branches, dando a você uma estrutura de ramificação semelhante ao seguinte:

Como mesclar mestre na ramificação de recurso

A mesclagem é legal porque é uma operação não destrutiva. As ramificações existentes não são alteradas de forma alguma. Isso evita todas as possíveis armadilhas do rebase (discutidas abaixo).

Por outro lado, isso também significa que a ramificação de funcionalidade terá um commit de mesclagem estranho toda vez que você precisar incorporar alterações upstream. Se o mestre for muito ativo, isto pode poluir um pouco o histórico da sua ramificação de funcionalidade. Embora seja possível mitigar este problema com opções avançadas de git log, isso pode dificultar para outros desenvolvedores entenderem o histórico do projeto.

A opção Rebase

Como uma alternativa ao merge, você pode fazer o rebase do branch de recursos no branch principal usando os comandos a seguir:

git checkout feature
git rebase master

Isso move a ramificação de funcionalidade inteira para começar na ponta da ramificação mestre, incorporando efetivamente todos os novos commits no mestre. Porém, em vez de usar um commit de mesclagem, o rebase reescreve o histórico do projeto criando novos commits para cada commit na ramificação original.

Como fazer o rebase da ramificação de recurso em mestre

O maior benefício do rebase é que você obtém um histórico de projeto muito mais limpo. Primeiro, ele elimina commits de mesclagem desnecessários exigidos pelo git merge. Segundo, como você pode ver no diagrama acima, o rebase também resulta em um histórico de projeto perfeitamente linear—você pode seguir a ponta da funcionalidade até o começo do projeto sem quaisquer bifurcações. Isso facilita a navegação pelo projeto com comandos como git log, git bisect e gitk.

Porém, há duas desvantagens para este histórico puro de commit: segurança e rastreabilidade. Se você não seguir a Regra de ouro do rebase, reescreve o histórico do projeto pode ser potencialmente catastrófico para o seu fluxo de trabalho colaborativo. E, de modo menos importante, o rebase perde o contexto fornecido por um commit de mesclagem—você não consegue ver quando as alterações upstream foram incorporadas na funcionalidade.

Rebase interativo

O rebase interativo dá a você a oportunidade de alterar commits à medida que eles são movidos para a nova ramificação. Isso é ainda mais poderoso do que um rebase automatizado, já que oferece o controle completo sobre o histórico de commit da ramificação. Normalmente, isso é usado para limpar um histórico bagunçado antes de mesclar uma ramificação de funcionalidade no mestre.

Para iniciar uma sessão interativa de rebase, passe a opção i para o comando git rebase:

git checkout feature
git rebase -i master

Isso vai abrir um editor de texto, listando todas as confirmações que estão prestes a serem movidas:

pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

Essa listagem traz a definição exata de qual vai ser o aspecto do branch depois da execução do rebase. Ao alterar o comando pick e/ou reordenar as entradas, é possível fazer com que o histórico do branch se pareça com o que você quiser. Por exemplo, se o 2º commit corrigir um problema pequeno no 1º commit, você pode condensar os dois em um único commit com o comando fixup:

pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

Quando você salva e fecha o arquivo, o Git vai executar o rebase de acordo com suas instruções, resultando em um histórico de projeto parecido com o seguinte:

Como suprimir uma confirmação com um rebase interativo

Eliminar commits insignificantes como estes faz com que o seu histórico de funcionalidades fique mais fácil de entender. Isso é algo que o git merge simplesmente não consegue fazer.

A regra de ouro da troca de base

Depois que você entende o que é o rebase, a coisa mais importante a ser aprendida é quando não fazê-lo. A regra de ouro do git rebase é nunca usá-lo em ramificações públicas.

Por exemplo, pense no que aconteceria se você fizesse rebase do mestre em sua ramificação de funcionalidade:

Como fazer o rebase da ramificação mestre

O rebase move todos os commits no branch mestre para a ponta do recurso. O problema é que essa ação só aconteceu no seu repositório. Todos os outros desenvolvedores ainda estão trabalhando com o branch mestre original. Como o rebase resulta em commits novos, o Git vai pensar que o histórico do seu branch mestre divergiu do de todas as outras pessoas.

A única forma de sincronizar as duas ramificações mestre é mesclá-las de volta juntas, resultando em um commit de mesclagem extra e em dois conjuntos de commits que contêm as mesmas alterações (as originais, e aquelas da sua ramificação de rebase). Não é nem preciso dizer que esta é uma situação muito confusa.

Então, antes de executar o git rebase, sempre pergunte a si mesmo: “Mais alguém está trabalhando com este branch?” Se a resposta for sim, tire suas mãos do teclado e comece a pensar sobre uma forma não destrutiva de fazer suas alterações (por exemplo, o comando git revert). Caso contrário, você pode reescrever o histórico como quiser.

Forçar a colocação

Se você tentar colocar a ramificação mestre do rebase em um repositório remoto, o Git não deixará você fazer isso, porque ele entra em conflito com a ramificação mestre remota. Entretanto, você pode forçar a colocação, com o sinalizador --force, desta forma:

# Muito cuidado com este comando! git push --force

Isso substitui a ramificação mestre remota para corresponder à de rebase do seu repositório e torna as coisas muito confusas para o resto da sua equipe. Então, tome muito cuidado para usar este comando somente quando souber exatamente o que está fazendo.

Uma das únicas vezes que você deve forçar a colocação é quando você executou uma limpeza local depois de colocar uma ramificação de funcionalidade privada em um repositório remoto (por exemplo, para fins de backup). Isso é como dizer "Opa, eu não queria de fato colocar aquela versão original da ramificação de funcionalidade. Use a atual no lugar dela". Novamente, é importante que ninguém esteja trabalhando fora dos commits da versão original da ramificação de funcionalidade.

Passo a passo do fluxo de trabalho

O rebase pode ser incorporado ao seu fluxo de trabalho existente do Git na medida em que isso for confortável para sua equipe. Nesta seção, vamos dar uma olhada nos benefícios que o rebase pode oferecer nos vários estágios de desenvolvimento de um recurso.

A primeira etapa em qualquer fluxo de trabalho que utilize o git rebase é criar um branch dedicado para cada recurso. Assim você tem a estrutura de ramificação necessária para utilizar o rebase com segurança:

Como desenvolver um recurso em uma ramificação dedicada

Limpeza local

Uma das melhores maneiras de incorporar o rebase em seu fluxo de trabalho é limpar os recursos locais, em andamento. Ao executar periodicamente um rebase interativo, você pode assegurar que cada confirmação no recurso seja focada e útil. Isso permite escrever seu código sem se preocupar em quebrá-lo em confirmações isoladas—é possível corrigi-lo depois do fato.

Ao chamar git rebase, você tem duas opções para a nova base: a ramificação pai da funcionalidade (por exemplo, mestre), ou um commit anterior na sua funcionalidade. Nós vimos um exemplo da primeira opção na seção Rebase interativo. A última opção é boa quando você somente precisa corrigir os últimos commits. Por exemplo, o comando a seguir começa em um rebase interativo dos últimos 3 commits somente.

git checkout feature git rebase -i HEAD~3

Ao especificar HEAD~3 como a nova base, você não está movendo de fato a ramificação—você apenas está reescreve interativamente os 3 commits que a seguem. Observe que isso não incorporará alterações upstream na ramificação de funcionalidade.

Como fazer o rebase em Head~3

Se você deseja reescrever a funcionalidade inteira usando este método, o comando git merge-base pode ser útil para encontrar a base original da ramificação de funcionalidade. O seguinte retorna o ID de commit da base original, que você pode, então, passar para o git rebase:

git merge-base feature master

Este uso do rebase interativo é uma ótima maneira de introduzir o git rebase no seu fluxo de trabalho, já que ele afeta somente as ramificações locais. A única coisa que os outro desenvolvedores verão é o seu produto final, que deve ser um histórico de ramificação de funcionalidade limpo e fácil de seguir.

Mas, novamente, isso apenas funciona para ramificações de funcionalidade privadas. Se você está colaborando com outros desenvolvedores pela mesma ramificação de funcionalidade, essa ramificação é pública, e você não tem permissão para reescrever seu histórico.

Não existe uma alternativa de git merge para limpar commits locais com um rebase interativo.

Como incorporar alterações de upstream em um recurso

Na seção Visão geral conceitual, vimos como um branch de recursos pode incorporar alterações de upstream do branch mestre usando o git merge ou o git rebase. O merge é uma opção segura que preserva o histórico inteiro do seu repositório, enquanto o rebase cria um histórico linear movendo o branch de recursos para a ponta do branch principal.

Este uso de git rebase é semelhante a uma limpeza local (e pode ser realizado ao mesmo tempo), mas, no processo, incorpora esses commits de upstream do branch principal.

Saiba que é perfeitamente legal fazer rebase em uma ramificação remota, em vez da mestre. Isso pode acontecer ao colaborar na mesma funcionalidade com outro desenvolvedor e você precisar incorporar as mudanças dele no seu repositório.

Por exemplo, se você e outro desenvolvedor chamado John adicionaram commits ao branch de recursos, o aspecto do seu repositório pode ser semelhante ao seguinte depois de procurar o branch de recursos remoto do repositório de John:

Como colaborar na mesma ramificação de recurso

Você pode resolver esta bifurcação da mesma forma que integra alterações upstream da mestre: mescle sua funcionalidade local com john/funcionalidade ou faça rebase da sua funcionalidade local na ponta de john/funcionalidade.

Mesclagem vs. rebase em uma ramificação remota

Observe que esse rebase não viola a Regra primordial de rebase, pois apenas os commits de recurso locais estão sendo movidos — tudo antes deles está intocado. É como se dissesse: “adicione minhas alterações ao que John já fez”. Na maioria das circunstâncias, fazer assim é mais intuitivo do que sincronizar com o branch remoto por meio de uma confirmação de mesclagem.

Por padrão, o comando git pull executa um merge, mas você pode forçar a integração do branch remoto a um rebase passando para ele a opção --rebase.

Como revisar um recurso com uma solicitação pull

Se você usar solicitações pull como parte do seu processo de revisão de código, precisará evitar usar git rebase depois de criar a solicitação pull. Assim que fizer a solicitação pull, outros desenvolvedores olharão para os seus commits, o que significa que eles são uma ramificação pública. Reescrever o histórico dele impossibilitaria que o Git e seus companheiros de equipe rastreassem qualquer commit de acompanhamento adicionado à funcionalidade.

Quaisquer alterações de outros desenvolvedores precisam ser incorporadas com git merge em vez de com git rebase.

Por este motivo, geralmente é uma boa ideia limpar seu código com um rebase interativo antes de enviar sua solicitação pull.

Como integrar um recurso aprovado

Depois que uma funcionalidade for aprovada pela sua equipe, você tem a opção de fazer rebase dela no topo da ramificação mestre antes de usar git merge para integrar a funcionalidade à base do código principal.

Esta é uma situação semelhante a incorporar alterações upstream em uma ramificação de funcionalidade, mas já que você não tem permissão para reescrever commits na ramificação mestre, você precisa usar eventualmente o git merge para integrar a funcionalidade. No entanto, ao executar um rebase antes da mesclagem, você terá a certeza de que a mesclagem será de avanço rápido, resultando em um histórico perfeitamente linear. Isso também dá a você a chance de fazer squash em qualquer commit de acompanhamento adicionado durante uma solicitação pull.

Como integrar um recurso a mestre com e sem um rebase

Se você não estiver totalmente confortável com o git rebase, você pode sempre executar o rebase em uma ramificação temporária. Desta forma, se você bagunçar acidentalmente o histórico da funcionalidade, você poderá verificar a ramificação original e tentar novamente. Por exemplo:

git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Clean up the history]
git checkout master
git merge temporary-branch

Resumo

E isto é tudo o que você precisa saber para começar a usar rebase nas suas ramificações. Se preferir um histórico limpo, linear e sem commits de mesclagem desnecessários, você deve usar git rebase em vez de git merge ao integrar alterações de outra ramificação.

Por outro lado, se quiser preservar o histórico completo do seu projeto e evitar o risco de reescrever commits públicos, você pode inserir o git merge. Qualquer uma das opções é válida, mas pelo menos agora você tem a opção de aproveitar os benefícios do git rebase.