Chapters ▾ 2nd Edition

3.6 Ramificação (Branching) no Git - Rebase (Rebasing)

Rebase (Rebasing)

No Git, existem duas maneiras principais de integrar as alterações de um branch em outro: o merge (mesclagem) e o rebase (rebaseamento). Nesta seção, você aprenderá o que é o rebase, como fazê-lo, por que é uma ferramenta tão incrível e em quais casos você não vai querer usá-lo.

O Rebase Básico

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

Histórico divergente simples
Figure 35. Histórico divergente simples

A maneira mais fácil de integrar os branches, como já abordamos, é com o comando merge. Ele executa uma mesclagem de três vias (three-way merge) entre os dois snapshots (instantâneos) de branch mais recentes (C3 e C4) e o ancestral comum mais recente dos dois (C2), criando um novo snapshot (e commit).

Mesclando (Merging) para integrar histórico de trabalho divergente
Figure 36. Mesclando (Merging) para integrar histórico de trabalho divergente

No entanto, há outra maneira: você pode pegar o patch da alteração que foi introduzida no C4 e reaplicá-lo no topo do C3. No Git, isso é chamado de rebasing (rebaseamento). Com o comando rebase, você pode pegar todas as alterações que foram comitadas em um branch e repeti-las (replay) em um branch diferente.

Para este exemplo, você faria o checkout do branch experiment e então faria o rebase dele no branch master da seguinte maneira:

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

Essa operação funciona indo ao ancestral comum dos dois branches (aquele em que você está e aquele para o qual 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 (resetting) o branch atual para o mesmo commit do branch para o qual você está fazendo o rebase e, finalmente, aplicando cada alteração uma por uma.

Fazendo o rebase da alteração introduzida em `C4` no `C3`
Figure 37. Fazendo o rebase da alteração introduzida em C4 no C3

Neste ponto, você pode voltar ao branch master e fazer uma mesclagem de avanço rápido (fast-forward merge).

$ git checkout master
$ git merge experiment
Avanço rápido (Fast-forwarding) do branch `master`
Figure 38. Avanço rápido (Fast-forwarding) do branch master

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

Geralmente, você fará isso para garantir que os seus commits sejam aplicados de forma limpa em um branch remoto — talvez em um projeto para o qual você esteja tentando contribuir, mas que não mantém. Neste caso, você faria o seu trabalho em um branch e então faria o rebase do seu trabalho em origin/master quando estivesse pronto para enviar os seus patches para o projeto principal. Dessa forma, o mantenedor não precisa fazer nenhum trabalho de integração — apenas um avanço rápido (fast-forward) ou uma aplicação limpa.

Observe que o snapshot apontado pelo commit final que você obtém, seja ele o último dos commits rebaseados para um rebase ou o commit de mesclagem (merge) final após um merge, é o mesmo snapshot — apenas o histórico é diferente. O rebase repete (replays) as alterações de uma linha de trabalho para outra na ordem em que foram introduzidas, ao passo que a mesclagem (merging) pega os pontos finais e os mescla.

Rebases Mais Interessantes

Você também pode fazer com que o seu rebase seja repetido (replay) em algo diferente do branch de destino do rebase. Pegue um histórico como o Um histórico com um branch de tópico ramificado de outro branch de tópico, por exemplo. Você ramificou (branched) um branch de tópico (server) para adicionar algumas funcionalidades do lado do servidor ao seu projeto e fez um commit. Em seguida, você ramificou a partir dele para fazer as alterações no lado do cliente (client) e fez alguns commits. Por fim, você voltou ao seu branch server e fez mais alguns commits.

Um histórico com um branch de tópico ramificado de outro branch de tópico
Figure 39. Um histórico com um branch de tópico ramificado de outro branch de tópico

Suponha que você decida que deseja mesclar as suas alterações no lado do cliente na sua linha principal (mainline) para um lançamento (release), mas deseja adiar as alterações no lado do servidor até que elas sejam mais testadas. Você pode pegar as alterações no client que não estão no server (C8 e C9) e repeti-las (replay) no 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 ele divergiu do branch server e repita (replay) esses patches no branch client como se ele fosse baseado diretamente no branch master.” É um pouco complexo, mas o resultado é muito legal.

Fazendo o rebase de um branch de tópico a partir de outro branch de tópico
Figure 40. Fazendo o rebase de um branch de tópico a partir de outro branch de tópico

Agora você pode fazer o avanço rápido (fast-forward) do seu branch master (consulte Avanço rápido (Fast-forwarding) do seu branch master para incluir as alterações do branch client):

$ git checkout master
$ git merge client
Avanço rápido (Fast-forwarding) do seu branch `master` para incluir as alterações do branch `client`
Figure 41. Avanço rápido (Fast-forwarding) do seu branch master para incluir as alterações do branch client

Digamos que você decida fazer o pull do seu branch server também. Você pode fazer o rebase do branch server no branch master sem precisar fazer o checkout dele primeiro executando git rebase <basebranch> <topicbranch> — que faz o checkout do branch de tópico (neste caso, server) para você e o repete (replays) no branch base (master):

$ git rebase master server

Isso repete (replays) o seu trabalho do server sobre o seu trabalho do master, como mostrado em Fazendo o rebase do seu branch server sobre o seu branch master.

Fazendo o rebase do seu branch `server` sobre o seu branch `master`
Figure 42. Fazendo o rebase do seu branch server sobre o seu branch master

Em seguida, você pode fazer o avanço rápido (fast-forward) do branch base (master):

$ git checkout master
$ git merge server

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

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

Os Perigos do Rebase (Perils of Rebasing)

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

Não faça rebase de commits que existem fora do seu repositório e nos quais as pessoas possam ter baseado o trabalho delas.

Se você seguir essa diretriz, ficará tudo bem. Se não o fizer, as pessoas o odiarão, e você será desprezado por amigos e familiares.

Quando você faz rebase de coisas, você está abandonando os commits existentes e criando novos que são parecidos, mas diferentes. Se você fizer o push de commits para algum lugar e outras pessoas fizerem o pull deles (pull down) e basearem o trabalho neles, e então você reescrever esses commits com o git rebase e fizer o push deles novamente, os seus colaboradores terão que mesclar (re-merge) o trabalho deles novamente e as coisas ficarão bagunçadas quando você tentar fazer o pull do trabalho deles de volta para o seu.

Vejamos um exemplo de como fazer 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. O seu histórico de commits fica assim:

Clonar um repositório e basear algum trabalho nele
Figure 44. Clonar um repositório e basear algum trabalho nele

Agora, outra pessoa faz mais trabalho que inclui uma mesclagem (merge) e faz o push desse trabalho para o servidor central. Você faz um fetch e mescla (merge) o novo branch remoto no seu trabalho, fazendo com que o seu histórico fique parecido com isto:

Buscar (Fetch) mais commits e mesclá-los (merge) no seu trabalho
Figure 45. Buscar (Fetch) mais commits e mesclá-los (merge) no seu trabalho

A seguir, a pessoa que fez o push do trabalho mesclado decide voltar e, em vez disso, fazer o rebase do trabalho dela; ela faz um git push --force para substituir o histórico no servidor. Você então faz um fetch desse servidor, baixando os novos commits.

Alguém envia (pushes) commits em rebase, abandonando os commits nos quais você baseou o seu trabalho
Figure 46. Alguém envia (pushes) commits em rebase, abandonando os commits nos quais você baseou o seu trabalho

Agora vocês dois estão em apuros. Se você fizer um git pull, você criará um commit de mesclagem (merge) que inclui ambas as linhas de histórico e o seu repositório ficará assim:

Você mescla (merge in) o mesmo trabalho novamente em um novo commit de mesclagem (merge commit)
Figure 47. Você mescla (merge in) o mesmo trabalho novamente em um novo commit de mesclagem (merge commit)

Se você executar um git log quando o seu histórico estiver assim, você verá dois commits que têm o mesmo autor, data e mensagem, o que será confuso. Além disso, se você fizer o push desse histórico de volta para o servidor, você reintroduzirá todos esses commits com rebase no servidor central, o que pode confundir ainda mais as pessoas. É bastante seguro supor que o outro desenvolvedor não deseja que C4 e C6 estejam no histórico; foi por isso que ele fez o rebase em primeiro lugar.

Rebase Quando Você Fizer Rebase

Se você se encontrar em uma situação como esta, o Git tem um pouco mais de mágica que pode ajudá-lo. Se alguém na sua equipe forçar o push de alterações que substituem um trabalho no qual você baseou o seu trabalho, o seu desafio é descobrir o que é seu e o que foi reescrito.

Acontece que, além da soma de verificação (checksum) SHA-1 do commit, o Git também calcula um checksum baseado apenas no patch introduzido com o commit. Isso é chamado de “patch-id”.

Se você fizer o pull de um trabalho que foi reescrito e fizer o rebase no topo dos novos commits do seu parceiro, o Git geralmente consegue descobrir com sucesso o que é exclusivamente seu e aplicá-lo novamente no topo do novo branch.

Por exemplo, no cenário anterior, se, em vez de fazer uma mesclagem (merge) quando estamos em Alguém envia (pushes) commits em rebase, abandonando os commits nos quais você baseou o seu trabalho, executarmos git rebase teamone/master, o Git irá:

  • Determinar qual trabalho é exclusivo do nosso branch (C2, C3, C4, C6, C7)

  • Determinar quais não são commits de mesclagem (merge commits) (C2, C3, C4)

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

  • Aplicar esses commits no topo de teamone/master

Rebase no topo do trabalho com rebase e push forçado
Figure 48. Rebase no topo do trabalho com rebase e push forçado

Isso só funciona se o C4 e o C4' que o seu parceiro fez forem quase exatamente o mesmo patch. Caso contrário, o rebase não conseguirá perceber que se trata de uma duplicata e adicionará outro patch semelhante a C4 (que provavelmente falhará ao ser aplicado de forma limpa, já que as alterações já estariam lá pelo menos em parte).

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

Se você está usando git pull e deseja 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ê fizer o rebase apenas de commits que nunca saíram do seu próprio computador, tudo ficará bem. Se você fizer o rebase de commits que foram enviados (pushed), mas que ninguém mais baseou os seus commits neles, você também ficará bem. Se você fizer rebase de commits que já foram enviados publicamente e as pessoas possam ter baseado trabalho nesses commits, então você poderá ter alguns problemas frustrantes e o desprezo dos seus colegas de equipe.

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

Rebase vs. Mesclar (Merge)

Agora que você viu o rebase e o merge em ação, você pode estar se perguntando qual deles é o melhor. Antes de respondermos a isso, vamos dar um passo atrás e falar sobre o que significa o histórico.

Um ponto de vista sobre isso é que o histórico de commits do seu repositório é um registro do que realmente aconteceu. É um documento histórico, valioso por si só, e não deve ser adulterado. Desse ângulo, alterar o histórico de commits é quase uma blasfêmia; você está mentindo sobre o que realmente aconteceu. E daí se houve uma série bagunçada de commits de mesclagem (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 o seu projeto foi feito. Você não publicaria o primeiro rascunho de um livro, então por que mostrar o seu trabalho bagunçado? Ao trabalhar em um projeto, você pode precisar de um registro de todos os seus erros e becos sem saída, mas quando chegar a hora de mostrar o seu trabalho ao mundo, você pode querer contar uma história mais coerente de como ir de A a B. As pessoas nesse campo (camp) usam ferramentas como rebase e filter-branch para reescrever os seus commits antes de serem mesclados (merged) no branch da linha principal (mainline). Eles usam ferramentas como rebase e filter-branch para contar a história da maneira que for melhor para os futuros leitores.

Agora, sobre a questão de saber se mesclar (merging) ou fazer rebase é melhor: esperamos que você perceba que não é tão simples. O Git é uma ferramenta poderosa e permite que você faça muitas coisas no seu histórico e com ele, mas cada equipe e cada projeto são diferentes. Agora que você sabe como as duas coisas funcionam, cabe a você decidir qual é a melhor para a sua situação em particular.

Você pode obter o melhor dos dois mundos: faça o rebase das alterações locais antes de enviar (pushing) para limpar o seu trabalho, mas nunca faça o rebase de nada que você já tenha enviado (pushed) para algum lugar.