No sistema de controle de versão Git, existem duas maneiras de combinar um branch com outro, representadas por comandos diferentes:
git merge. Os commits de um branch são transferidos para outro criando um commit de merge.
git rebase. Os commits de um branch são transferidos para outro branch preservando a ordem original das alterações.
Em resumo: com git merge, os commits de um branch são “fundidos” em um único commit, enquanto com git rebase eles permanecem intactos, mas os branches são combinados.
Assim, o comando git rebase permite combinar os commits de ambos os branches, formando um histórico compartilhado de alterações.
Este guia aborda o comando git rebase, responsável por rebasear (mover) commits (alterações) de um branch para outro.
Todos os exemplos apresentados utilizam o Git versão 2.34.1, executado em um servidor Hostman com o sistema operacional Ubuntu 22.04.
Você pode usar estes guias para instalar o Git em sua máquina:
Git Rebase é um comando poderoso do Git usado principalmente para integrar alterações de um branch em outro, reescrevendo o histórico de commits. Diferente do git merge, que cria um novo commit de merge para combinar branches e preserva o histórico de ambos, o git rebase move ou “reexecuta” uma série de commits de um branch em outro. Esse processo resulta em um histórico linear, fazendo parecer que o branch de feature foi desenvolvido diretamente a partir do commit mais recente do branch de destino, como main ou master. Dessa forma, ele limpa o histórico de commits e remove merges desnecessários, resultando em um histórico de projeto mais organizado.
A melhor maneira de entender como o rebase funciona no Git é observar um repositório abstrato com vários branches e analisar a operação passo a passo.
Vamos supor que criamos um repositório com um único branch master, contendo apenas um commit. O branch master fica assim:
master
commit_1
Depois, com base no master, criamos um novo branch chamado hypothesis, onde decidimos testar alguns recursos. Nesse novo branch, fizemos vários commits melhorando o código. O branch agora fica assim:
hypothesis
commit_4
commit_3
commit_2
commit_1
Mais tarde, adicionamos outro commit ao branch master, corrigindo urgentemente uma vulnerabilidade. O branch master agora fica assim:
master
commit_5
commit_1
Agora nosso repositório possui dois branches:
master
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_1
O branch master é o principal, enquanto hypothesis é secundário (derivado). Commits mais recentes são listados acima dos anteriores, semelhante à saída do comando git log.
Suponha que queremos continuar trabalhando no recurso que movemos anteriormente para o branch separado hypothesis. No entanto, esse branch não contém a correção crítica que fizemos no master.
Portanto, queremos “sincronizar” o estado de hypothesis com o master para que o commit de correção também apareça no branch de recurso. Em outras palavras, queremos que a estrutura do repositório fique assim:
master
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_5
commit_1
Como você pode ver, o branch hypothesis agora repete exatamente o histórico do master, mesmo que tenha sido criado antes do commit_5. Em outras palavras, hypothesis agora contém o histórico de ambos os branches — o seu e o do master.
Para alcançar isso, precisamos fazer um rebase usando o comando git rebase.
Depois disso, as alterações feitas em hypothesis podem ser mescladas ao master usando o comando clássico git merge, que cria um commit de merge.
A estrutura do repositório ficará assim:
master
commit_merge
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_5
commit_1
Além disso, executar git merge após git rebase pode reduzir a probabilidade de conflitos.
Agora que cobrimos o aspecto teórico do comando git rebase, podemos prosseguir para testá-lo em um repositório real de um projeto de exemplo. A estrutura do repositório será igual à do exemplo teórico anterior.
Primeiro, vamos criar um diretório separado para armazenar o repositório:
mkdir rebase
Depois, acesse o diretório:
cd rebase
Agora podemos inicializar o repositório:
git init
No console, aparecerá uma mensagem informativa padrão:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
...
E no diretório atual, aparecerá uma pasta oculta chamada .git, que você pode visualizar com o seguinte comando:
ls -a
A opção -a significa all (tudo) e permite visualizar o sistema de arquivos em modo detalhado. O conteúdo será:
. .. .git
Antes de fazer commits, precisamos configurar algumas informações básicas do usuário.
Primeiro, o nome:
git config --global user.name "NAME"
Depois, o e-mail:
git config --global user.email "NAME@HOST.COM"
Usando arquivos de texto simples, vamos simular a adição de diferentes funções ao projeto. Cada nova função será representada como um commit separado.
Crie um arquivo para a primeira função improvisada:
nano function_1
Adicione o seguinte conteúdo:
Function 1
Agora indexe as alterações feitas no repositório:
git add .
Para garantir, verifique o status da indexação:
git status
No console, aparecerá a seguinte mensagem, mostrando as alterações indexadas:
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: function_1
Agora podemos fazer o commit:
git commit -m "commit_1"
O console exibirá uma mensagem confirmando o commit bem-sucedido no master:
[master (root-commit) 4eb7cc3] commit_1
1 file changed, 1 insertion(+)
create mode 100644 function_1
Agora vamos criar um novo branch chamado hypothesis:
git checkout -b hypothesis
A opção -b é necessária para alternar imediatamente para o novo branch.
O console exibirá uma mensagem de confirmação:
Switched to a new branch 'hypothesis'
Em seguida, precisamos fazer três commits em sequência com três arquivos, de forma semelhante ao master:
commit_2 com o arquivo function_2 e conteúdo: Function 2commit_3 com o arquivo function_3 e conteúdo: Function 3commit_4 com o arquivo function_4 e conteúdo: Function 4Se verificarmos a lista de commits:
git log --oneline
O console exibirá a seguinte sequência:
d3efb82 (HEAD -> hypothesis) commit_4
c9f57b7 commit_3
c977f16 commit_2
4eb7cc3 (master) commit_1
Aqui, a opção --oneline é usada para exibir informações de commit em formato compacto.
A última etapa é adicionar outro commit ao branch principal master. Vamos alternar para ele:
git checkout master
O console confirmará a troca:
Switched to branch 'master'
Agora, crie outro arquivo de função improvisado:
nano function_5
Com o seguinte conteúdo:
Function 5
Em seguida, indexe as alterações:
git add .
E faça o novo commit:
git commit -m "commit_5"
Se verificarmos a lista de commits:
git log --oneline
O branch master agora terá dois commits:
3df7a00 (HEAD -> master) commit_5
4eb7cc3 commit_1
Para realizar o rebase, primeiro é necessário alternar para o branch hypothesis:
git checkout hypothesis
E executar o rebase:
git rebase master
Após isso, o console exibirá uma mensagem confirmando o rebase bem-sucedido:
Successfully rebased and updated refs/heads/hypothesis.
Agora, verifique a lista de commits:
git log --oneline
O console mostrará uma lista contendo os commits de ambos os branches na ordem original:
8ecfd58 (HEAD -> hypothesis) commit_4
f715aba commit_3
ee47470 commit_2
3df7a00 (master) commit_5
4eb7cc3 commit_1
Agora o branch hypothesis contém o histórico completo de todo o repositório.
Assim como com o git merge, ao usar o comando git rebase, podem ocorrer conflitos que precisam ser resolvidos manualmente.
Vamos modificar nosso repositório de forma a criar artificialmente um conflito de rebase.
Crie outro arquivo no branch hypothesis:
nano conflict
E insira o seguinte texto nele:
There must be a conflict here!
Indexe as alterações:
git add .
E faça outro commit:
git commit -m "conflict_1"
Agora alterne para o branch master:
git checkout master
Crie um arquivo semelhante:
nano conflict
E insira o seguinte conteúdo:
There must NOT be a conflict here!
Novamente, indexe as alterações:
git add .
E faça o commit:
git commit -m "conflict_2"
Reabra o arquivo criado:
nano conflict
E altere seu conteúdo para:
There definitely must NOT be a conflict here!
Mais uma vez, indexe as alterações:
git add .
E faça outro commit:
git commit -m "conflict_3"
Agora volte para o branch hypothesis:
git checkout hypothesis
E execute outro rebase:
git rebase master
O console exibirá uma mensagem de conflito:
Auto-merging conflict
CONFLICT (add/add): Merge conflict in conflict
error: could not apply 6003ed7... conflict_1
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 6003ed7... conflict_1
O Git sugere editar o arquivo em conflito, indexar as alterações com git add e, em seguida, continuar o rebase usando a opção --continue.
É exatamente isso que faremos:
nano conflict
O arquivo conterá duas versões conflitantes envoltas em marcadores especiais:
<<<<<<< HEAD
There definitely must NOT be a conflict here!
=======
There must be a conflict here!
>>>>>>> 6003ed7 (conflict_1)
Nosso trabalho é remover as partes desnecessárias e preencher o arquivo com uma versão final de texto arbitrário:
There must absolutely definitely unanimously NOT be any conflict here!
Agora indexe as alterações:
git add .
E continue o rebase:
git rebase --continue
Depois disso, o console abrirá um editor de texto sugerindo modificar a mensagem de commit original do commit onde o conflito ocorreu:
conflict_1
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto bd7aefc
# Last commands done (4 commands done):
# pick 8ecfd58 commit_4
# pick 6003ed7 conflict_1
# No commands remaining.
# You are currently rebasing branch 'hypothesis' on 'bd7aefc'.
#
# Changes to be committed:
# modified: conflict
#
O console então exibirá uma mensagem sobre a conclusão bem-sucedida do processo de rebase:
[detached HEAD 482db49] conflict_1
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/hypothesis.
Agora, se você verificar a lista de commits no branch hypothesis:
git log --oneline
Você verá a sequência original de todas as alterações feitas:
482db49 (HEAD -> hypothesis) conflict_1
bd5d036 commit_4
407e245 commit_3
948b41c commit_2
bd7aefc (master) conflict_3
d98648d conflict_2
3df7a00 commit_5
4eb7cc3 commit_1
Observe que os commits conflict_2 e conflict_3, feitos no branch master, aparecem no histórico antes do commit conflict_1. No entanto, isso se aplica a quaisquer commits feitos no branch master.
Além de trabalhar com branches locais, o rebase também pode ser feito ao puxar alterações de um repositório remoto. Para isso, é necessário adicionar a opção --rebase ao comando pull padrão:
git pull --rebase remote branch
Onde:
remote é o repositório remoto
branch é o branch remoto
Basicamente, essa configuração de pull é equivalente ao git rebase, exceto pelo fato de que as alterações (commits) aplicadas ao branch atual vêm do repositório remoto.
O comando git rebase permite criar um histórico linear do branch de destino, composto por commits realizados em sequência. Essa sequência sem ramificações torna o histórico mais fácil de compreender e analisar.
Executar git rebase antecipadamente pode reduzir significativamente a probabilidade de conflitos ao mesclar branches com git merge. Os conflitos são mais fáceis de resolver em commits sequenciais do que em commits combinados em um único merge commit. Isso é especialmente relevante ao enviar branches para repositórios remotos.
Diferente do merge, o rebase reescreve parcialmente o histórico do branch de destino, removendo elementos desnecessários.
A capacidade de reestruturar significativamente o histórico de commits pode levar a erros irreversíveis no repositório. Isso significa que alguns dados podem ser perdidos permanentemente.
O Git rebase é particularmente útil ao trabalhar em branches pequenos ou de recursos individuais que eventualmente serão mesclados em um branch principal compartilhado. Também é excelente para manter um histórico limpo e linear, o que é especialmente vantajoso em projetos de código aberto ou quando é necessário preservar o histórico de commits de um projeto para facilitar o rastreamento e a compreensão.
No entanto, em ambientes de equipe com vários colaboradores, o rebase deve ser usado com cautela para evitar problemas relacionados à reescrita de histórico público. É importante comunicar-se com a equipe sobre quando o rebase é apropriado e garantir que todos estejam cientes do potencial de conflitos. Em muitos casos, usar git merge para integrar branches pode ser mais seguro e simples, especialmente ao trabalhar em branches compartilhados ou quando um histórico não linear é aceitável.
Um dos aspectos mais importantes do git rebase é que ele reescreve o histórico de commits, o que significa que os hashes SHA-1 dos commits rebaseados mudam. Isso pode causar problemas em ambientes colaborativos, especialmente ao rebasear um branch que já foi enviado para um repositório remoto compartilhado. A reescrita do histórico pode gerar conflitos com outros desenvolvedores que basearam seu trabalho no histórico anterior. Também pode causar problemas ao tentar enviar o branch rebaseado, pois o repositório remoto reconhecerá que o histórico local não corresponde mais ao remoto.
Uma boa prática comum é evitar rebasear branches públicos que já foram compartilhados com outras pessoas. Como o rebase altera o histórico de commits, rebasear branches públicos dos quais vários desenvolvedores dependem pode resultar em históricos divergentes, causando confusão e conflitos de merge difíceis de resolver. Em geral, o git rebase é mais apropriado para branches locais ou para preparar um branch de recurso para uma mesclagem final em um branch principal. Branches públicos, especialmente aqueles em que várias pessoas estão trabalhando, geralmente devem ser mesclados em vez de rebaseados.
Durante um rebase, se houver alterações conflitantes tanto no branch de recurso quanto no branch de destino, o Git interromperá o processo e solicitará que o conflito seja resolvido manualmente. Embora resolver conflitos durante um rebase seja semelhante a fazê-lo durante um merge, o rebase exige que você resolva conflitos para cada commit que precisa ser reaplicado, o que pode tornar o processo mais trabalhoso, especialmente em branches grandes. Depois que os conflitos forem resolvidos, você poderá continuar o rebase com o comando git rebase --continue.
Mesclar dois branches usando rebase, implementado com o comando git rebase, é fundamentalmente diferente da mesclagem clássica feita com o comando git merge.
git merge transforma os commits de um branch em um único commit em outro.
git rebase move os commits de um branch para o final de outro, preservando a ordem original.
Um efeito de rebase semelhante também pode ser alcançado ao usar o comando git pull com a opção adicional --rebase.
Por um lado, o comando git rebase permite obter um histórico de commits mais limpo e compreensível no branch principal, o que aumenta a capacidade de manutenção do repositório.
Por outro lado, o git rebase reduz o nível de detalhes das alterações dentro do branch, simplificando o histórico e removendo alguns de seus registros.
Por esse motivo, o rebase é um recurso destinado a usuários mais experientes que entendem como o Git funciona.
Na maioria das vezes, o comando git rebase é usado em conjunto com o comando git merge, permitindo alcançar a estrutura mais otimizada possível de repositórios e branches.