Git
Chapters ▾ 2nd Edition

3.6 Branches no Git - Rebase

Rebase

No Git, existem duas maneiras principais de integrar as mudanças de um branch para outro: o merge e o rebase. Nesta seção, você aprenderá o que é o rebase, como fazê-lo, por que é uma ferramenta incrível e em que casos não vai querer usá-la.

O básico do Rebase

Se você voltar a um exemplo anterior de Mesclagem Básica, você pode ver que o seu trabalho divergiu e fez commits em dois branches diferentes.

Simple divergent history.
Figure 35. Um simples histórico de divergência

A maneira mais fácil de integrar os branches, como já vimos, é o comando merge. Ele realiza uma fusão de três vias entre os dois últimos snapshots de branch (C3 e C4) e o ancestral comum mais recente dos dois (C2), criando um novo snapshot (e commit).

Merging to integrate diverged work history.
Figure 36. Fazendo um merge para integrar áreas de trabalho que divergiram

No entanto, há outra maneira: você pode pegar o patch da mudança que foi introduzida no C4 e reaplicá-lo em cima do C3. No Git, isso é chamado de rebasing. Com o comando rebase, você pode pegar todas as alterações que foram confirmadas em um branch e reproduzi-las em outro.

Neste exemplo, você executaria o seguinte:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

Ele funciona indo para o ancestral comum dos dois branches (aquele em que você está e aquele em que você está fazendo o rebase), obtendo o diff introduzido por cada commit do branch em que você está, salvando esses diffs em arquivos temporários, redefinindo o branch atual para o mesmo commit do branch no qual você está fazendo o rebase e, finalmente, aplicando cada mudança por vez.

Rebasing the change introduced in `C4` onto `C3`.
Figure 37. Fazendo o Rebase da mudança introduzida no C4 em C3

Neste ponto, você pode voltar ao branch master e fazer uma fusão rápida.

$ git checkout master
$ git merge experiment
Fast-forwarding the master branch.
Figure 38. Fazendo uma fusão rápida no branch master

Agora, o snapshot apontado por C4' é exatamente o mesmo que foi apontado por C5 no exemplo de merge. Não há diferença no produto final da integração, mas o rebase contribui para um histórico mais limpo. Se você examinar o log de um branch que foi feito rebase, parece uma registro linear: parece que todo o trabalho aconteceu em série, mesmo quando originalmente aconteceu em paralelo.

Frequentemente, você fará isso para garantir que seus commits sejam aplicados de forma limpa em um branch remoto - talvez em um projeto para o qual você está tentando contribuir, mas que não mantém. Neste caso, você faria seu trabalho em um branch e então realocaria seu trabalho em origin/master quando estivesse pronto para enviar seus patches para o projeto principal. Dessa forma, o mantenedor não precisa fazer nenhum trabalho de integração - apenas um fusão rápida ou uma aplicação limpa.

Observe que o snapshot apontado pelo commit final com o qual você termina, seja o último dos commits para um rebase ou o commit final de mesclagem após um merge, é o mesmo snapshot - é apenas o histórico que é diferente. O Rebase reproduz as alterações de uma linha de trabalho para outra na ordem em que foram introduzidas, enquanto a mesclagem pega os finais e os mescla.

Rebases mais interessantes

Você também pode fazer o replay do rebase em algo diferente do branch de destino. Pegue um histórico como Um histórico com um tópico de branch de outro branch, por exemplo. Você ramificou um branch de tópico (server) para adicionar alguma funcionalidade do lado do servidor ao seu projeto e fez um commit. Então, você ramificou isso para fazer as alterações do lado do cliente (client) e fez commit algumas vezes. Finalmente, você voltou ao seu branch de servidor e fez mais alguns commits.

A history with a topic branch off another topic branch.
Figure 39. Um histórico com um tópico de branch de outro branch

Suponha que você decida que deseja mesclar suas alterações do lado do cliente em sua linha principal para um lançamento, mas deseja adiar as alterações do lado do servidor até que seja testado mais profundamente. Você pode pegar as mudanças no cliente que não está no servidor (C8 e C9) e reproduzi-las em seu branch master usando a opção --onto do git rebase:

$ git rebase --onto master server client

Isso basicamente diz: “Pegue o branch client, descubra os patches, desde que divergiu do branch server, e repita esses patches no branch client como se fosse baseado diretamente no branch master.” É um pouco complexo, mas o resultado é bem legal.

Rebasing a topic branch off another topic branch.
Figure 40. Rebase o tópico de um branch de outro branch

Agora, você pode fazer uma fusão rápida no branch master (veja Avanço rápido de seu branch principal para incluir as alterações da branch do cliente):

$ git checkout master
$ git merge client
Fast-forwarding your master branch to include the client branch changes.
Figure 41. Avanço rápido de seu branch principal para incluir as alterações da branch do cliente

Digamos que você decida puxar seu branch de servidor também. Você pode realocar o branch do servidor no branch master sem ter que verificá-lo primeiro executando git rebase [basebranch] [topicbranch] - que verifica o branch do tópico (neste caso, server) para você e repete no branch base (master):

$ git rebase master server

Isso reproduz o trabalho do server em cima do trabalho do master, como mostrado em Rebase o branch server por cima do branch master.

Rebasing your server branch on top of your master branch.
Figure 42. Rebase o branch server por cima do branch master

Então, você pode avançar o branch base (master):

$ git checkout master
$ git merge server

Você pode remover os branches client e server porque todo o trabalho foi integrado e você não precisa mais deles, deixando seu histórico para todo o processo parecido com Histórico final de commits:

$ git branch -d client
$ git branch -d server
Final commit history.
Figure 43. Histórico final de commits

Os perigos do Rebase

Ahh, mas a felicidade do rebase não vem sem suas desvantagens, que podem ser resumidas em uma única linha:

Não faça rebase de commits que existam fora do seu repositório.

Se você seguir essa diretriz, ficará bem. Do contrário, as pessoas irão odiá-lo e você será desprezado por amigos e familiares.

Quando você faz o rebase, você está abandonando commits existentes e criando novos que são semelhantes, mas diferentes. Se você enviar commits para algum lugar e outros puxá-los para baixo e trabalhar com base neles, e então você reescrever esses commits com git rebase e colocá-los novamente, seus colaboradores terão que fazer um novo merge em seus trabalhos e as coisas ficarão complicadas quando você tentar puxar o trabalho deles de volta para o seu.

Vejamos um exemplo de como o rebase de um trabalho que você tornou público pode causar problemas. Suponha que você clone de um servidor central e faça algum trabalho a partir dele. Seu histórico de commits é parecido com este:

Clone a repository, and base some work on it.
Figure 44. Fazendo o clone de um repositório e trabalhando com ele

Agora, outra pessoa faz mais alterações que inclui um merge e envia esse trabalho para o servidor central. Você o busca e mescla o novo branch remoto em seu trabalho, fazendo com que seu histórico se pareça com isto:

Fetch more commits, and merge them into your work.
Figure 45. Buscar mais commits e fazer merge em seu trabalho

Em seguida, a pessoa que empurrou o trabalho mesclado decide voltar e realocar seu trabalho; eles fazem um git push --force para sobrescrever o histórico no servidor. Você então busca daquele servidor, derrubando os novos commits.

Someone pushes rebased commits, abandoning commits you’ve based your work on.
Figure 46. Alguém empurra commits que foram feitos rebase, abandonando commits nos quais você baseou seu trabalho

Agora você está em apuros. Se você fizer um git pull, você criará um commit de merge que inclui as duas linhas do histórico, e seu repositório ficará assim:

You merge in the same work again into a new merge commit.
Figure 47. Você faz o merge do mesmo trabalho novamente em um novo commit de merge

Se você executar um git log quando seu histórico estiver assim, você verá dois commits com o mesmo autor, data e mensagem, o que será confuso. Além disso, se você enviar esse histórico de volta ao servidor, reintroduzirá todos os commits realocados no servidor central, o que pode confundir ainda mais as pessoas. É bastante seguro assumir que o outro desenvolvedor não quer que C4 e C6 apareçam na história; é por isso que eles fizeram um rebase antes.

Rebase quando vocês faz Rebase

Se você realmente se encontrar em uma situação como essa, o Git tem mais alguma mágica que pode te ajudar. Se alguém em sua equipe forçar mudanças que substituam o trabalho no qual você se baseou, seu desafio é descobrir o que é seu e o que eles reescreveram.

Acontece que, além da soma de verificação SHA-1 de commit, o Git também calcula uma soma de verificação que é baseada apenas no patch introduzido com a confirmação. Isso é chamado de “patch-id”.

Se você puxar o trabalho que foi reescrito e fazer o rebase sobre os novos commits de seu parceiro, o Git pode muitas vezes descobrir o que é exclusivamente seu e aplicá-lo de volta ao novo branch.

Por exemplo, no cenário anterior, se em vez de fazer uma fusão quando estamos em Alguém empurra commits que foram feitos rebase, abandonando commits nos quais você baseou seu trabalho executarmos git rebase teamone/master, Git irá:

  • Determine qual trabalho é exclusivo para nosso branch (C2, C3, C4, C6, C7)

  • Determine quais não são confirmações de merge (C2, C3, C4)

  • Determine quais não foram reescritos no branch de destino (apenas C2 e C3, uma vez que C4 é o mesmo patch que C4')

  • Aplique esses commits no topo de teamone/master

Então, em vez do resultado que vemos em Você faz o merge do mesmo trabalho novamente em um novo commit de merge, acabaríamos com algo mais parecido com Rebase on top of force-pushed rebase work..

Rebase on top of force-pushed rebase work.
Figure 48. Rebase on top of force-pushed rebase work.

Isso só funciona se C4 e C4' que seu parceiro fez forem quase exatamente o mesmo patch. Caso contrário, o rebase não será capaz de dizer o que é uma duplicata e adicionará outro patch semelhante ao C4 (que provavelmente não será aplicado, uma vez que as alterações já estariam pelo menos um pouco lá).

Você também pode simplificar isso executando um git pull --rebase em vez de um git pull normal. Ou você poderia fazer isso manualmente com um git fetch seguido por um git rebase teamone/master neste caso.

Se você estiver usando git pull e quiser tornar --rebase o padrão, você pode definir o valor de configuração pull.rebase com algo como git config --global pull.rebase true.

Se você tratar o rebase como uma forma de limpar e trabalhar com commits antes de enviá-los, e se você apenas fazer o rebase dos commits que nunca estiveram disponíveis publicamente, então você ficará bem. Se você fizer o rebase dos commits que já foram enviados publicamente, e as pessoas podem ter baseado o trabalho nesses commits, então você pode ter alguns problemas frustrantes e o desprezo de seus companheiros de equipe.

Se você ou um parceiro achar necessário em algum ponto, certifique-se de que todos saibam executar git pull --rebase para tentar tornar a dor depois que ela acontecer um pouco mais simples.

Rebase vs. Merge

Agora que você viu o rebase e o merge em ação, pode estar se perguntando qual é o melhor. Antes de respondermos, vamos voltar um pouco e falar sobre o que a história significa.

Um ponto de vista sobre isso é que o histórico de commit do seu repositório é um registro do que realmente aconteceu. É um documento histórico, valioso por si só, e não deve ser alterado. Desse ângulo, mudar o histórico de commits é quase uma blasfêmia; você está mentindo sobre o que realmente aconteceu. E daí se houvesse uma série confusa de commits de merge? Foi assim que aconteceu, e o repositório deve preservar isso para a posteridade.

O ponto de vista oposto é que o histórico de commits é a história de como seu projeto foi feito. Você não publicaria o primeiro rascunho de um livro, e o manual de como manter seu software merece uma edição cuidadosa. Este é o campo que usa ferramentas como rebase e filter-branch para contar a história da maneira que for melhor para futuros leitores.

Agora, à questão de saber se merge ou rebase é melhor: espero que você veja que não é tão simples. O Git é uma ferramenta poderosa e permite que você faça muitas coisas para e com sua história, mas cada equipe e cada projeto são diferentes. Agora que você sabe como essas duas coisas funcionam, cabe a você decidir qual é a melhor para sua situação específica.

Em geral, a maneira de obter o melhor dos dois mundos é fazer o rebase nas mudanças locais que você fez, mas não compartilhou ainda antes de empurrá-las para limpar seu histórico, mas nunca faça rebase em algo que você empurrou em algum lugar.