Desmistificando git rebase - Phelipe Teles

Desmistificando git rebase

9 min.
Ver código fonte

git rebase é um comando usado para reescrever histórico de commit, que parece assustador de início mas você vai aprender a gostar.

Por que aprender git rebase?

Eu lembro que, como inciante no git, foi muito difícil para entender o que era o rebase. Eu não sei explicar o porquê da dificuldade, mas eu chutaria que é porque eu não entendia quando eu precisaria usar esse comando.

E acontece que você não precisa de rebase no seu dia-a-dia, você pode trabalhar tranquilamente sabendo o básico do git: add-commit-push-pull.

git rebase é bastante similar ao git merge, no sentido de que você também o usa para pegar commits mais recentes em uma branch upstream. Mas, além disso, é útil também para:

  • mudar a ordem de commits.
  • juntar vários commits em um só.
  • separar um commit em vários.
  • editar um commit, por exemplo remover um .env incluído acidentalmente.
  • editar a mensagem de um commit.
  • deletar um commit.

Normalmente você vai querer fazer uma dessas coisas para arrumar o histórico de commit, por qualquer razão que seja. E é isso, é essa razão de usar o rebase, deixar o commit history organizado.

Nesse blog post, eu vou tentar explicar o que é o rebase com vários exemplos de como eu uso diariamente.

Entendendo o git rebase

git rebase vai repetir os commits da sua branch sobre os commits de outra branch.

Para ilustrar, vamos imaginar que você esteja trabalhar numa feature branch, e a main branch foi atualizada com código que você quer:

[Sem nome]
      E---F---G feature     /A---B---C---D main

Você pode rodar git rebase main, se você já estiver com a feature branch checked out, para chegar ao seguinte resultado:

[Sem nome]
              E'---F'---G' feature             /A---B---C---D main

Os commits E, F e G agora são diferentes, o hash deles mudou, porque o commit pai deles mudou.

Isto é parecido com o git merge main, mas a branch resultante é mais limpa, porque o histórico é linear: parece que você nunca começou a desenvolver a feature a partir de um commit antigo de main.

Razões para temer o rebase

Devido ao rebase poder mudar a identidade de um commit (seu hash), você nunca deve fazê-lo em uma branch pública, ou seja, uma branch que mais alguém pode estar contribuindo.

Para ilustrar, vamos imaginar uma feature branch que você já compartilhou com seus colegas no repositório remoto:

[Sem nome]
              E---F feature             /A---B---C---D main

Por alguma razão, um colega de trabalho decidiu colaborar com um commit:

[Sem nome]
              E---F---G feature             /A---B---C---D main

Mas, antes disso, você decidiu fazer um rebase, talvez porque quis melhorar a a mensagem do commit E, e deu um push:

[Sem nome]
              E'---F' feature             /A---B---C---D main

Agora, o seu colega não vai conseguir fazer um push das changes dele, porque o commit G foi feito sobre o commit F, que não existe mais na branch remota.

Quando seu colega quiser compartilhar o trabalho dele, o git vai falar: “remote contains work that you do not have locally. […] You may want to first integrate the remote changes”.

Ele poderia então fazer um git force --push, mas isso só pioraria as coisas é o seu trabalho que seria descartado.

Agora que o dano foi feito, você precisa recomendar a leitura da seção RECOVERING FROM UPSTREAM REBASE na man page do git-rebase, e proceder como aconselhado lá.

Razões para NÃO temer rebase

Você não deveria evitar rebase com medo de trabalho perdido — se você commitou seu código, é muito difícil de perder.

Mesmo que você acabe se enrolando e feito cagada, você pode sempre resetar sua branch para um estado anterior em que as coisas estavam funcionando, basta procurar no git reflog feature ou git reflog HEAD e então usar git reset para voltar àquele estado, por exemplo com git reset --hard feature@{1} ou git reset --hard feature@{one.min.ago}. Leia a documentação sobre git-reflog para aprender mais.

Rebase interativo

Você deve estar se perguntando: ok mas como o rebase vai me ajudar a separar um commit?

Isso é possível no modo interativo, disponível sob a opção --interactive. Nesse modo, o git vai abrir o seu editor de texto com uma lista dos commits que serão repetidos sobre a outra branch:

[Sem nome]
pick df4adc Epick 180a94 Fpick 490b6c G# Rebase 3409df0 onto main (3 commands)## Commands:# p, pick <commit> = use commit# r, reword <commit> = use commit, but edit the commit message# e, edit <commit> = use commit, but stop for amending# s, squash <commit> = use commit, but meld into previous commit# f, fixup <commit> = like "squash", but discard this commit's log message# x, exec <command> = run command (the rest of the line) using shell# b, break = stop here (continue rebase later with 'git rebase --continue')# d, drop <commit> = remove commit# l, label <label> = label current HEAD with a name# t, reset <label> = reset HEAD to a label# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]# .       create a merge commit using the original merge commit's# .       message (or the oneline, if no original merge commit was# .       specified). Use -c <commit> to reword the commit message.## These lines can be re-ordered; they are executed from top to bottom.## If you remove a line here THAT COMMIT WILL BE LOST.## However, if you remove everything, the rebase will be aborted.#

Isso é referido como uma todo list. Aqui, você usa comandos para descrever o que o git deve fazer a cada commit para te ajudar a produzir o commit history desejado.

O comando padrão para todos os commits é “pick”, que significa “repita esse commit nessa ordem, sem mudar nada”.

Esse arquivo é só um arquivo de texto, você pode editar ele normalmente. Quando terminar, salve e feche o arquivo.

Agora, vamos ver na prática como usar o git rebase -i.

Mudando a ordem dos commits

Digamos que você queira inverter a ordem dos commits, dado o commit history abaixo:

[Sem nome]
              E---F---G feature             /A---B---C---D main

Depois de rodar git rebase -i main, você vai ver a seguinte todo list:

[Sem nome]
pick df4adc Epick 180a94 Fpick 490b6c G

Agora, simplesmente inverta a ordem das linhas:

[Sem nome]
pick 490b6c Gpick 180a94 Fpick df4adc E

Salve o arquivo e feche. Caso não haja conflitos, tudo funcionará e o resultado vai ser esse novo commit history:

[Sem nome]
              G'---F'---E' feature             /A---B---C---D main

Juntando commits, sem mudar a mensagem

Eis um cenário mais típico: você commitou algum código quebrado e só percebeu um tempo depois e agora quer consertar aquele commit.

Se o commit for o mais recente, você pode só escrever o fix e commitar com git commit --amend. Se você não quiser editar a mensagem, você pode até usar git commit --amend --no-edit.

Mas, caso o commit não seja o mais recente, você vai precisar usar o rebase. Se você não quer editar a mensagem, o comando certo a se usar é o fixup.

Por exemplo, suponha que você tenha a seguinte branch:

[Sem nome]
df4adc Add script180a94 foo490b6c bar

O commit “Add script” tem um bug e você quer consertar. Você fez o fix e commitou com git commit -m "Fix script":

[Sem nome]
df4adc Add script180a94 foo490b6c bar3409df Fix script

Depois de rodar git rebase -i main, a seguinte todo list vai aparecer:

[Sem nome]
pick df4adc Add scriptpick 180a94 foopick 490b6c barpick 3409df Fix script

Para juntar os dois commits, você precisa mover a linha do commit “Fix script” para cima e usar o comando fixup, ao invés de pick.

[Sem nome]
pick df4adc Add scriptfixup 3409df Fix scriptpick 180a94 foopick 490b6c bar

No commit history resultante vai parecer que você nunca introduziu aquele bug:

[Sem nome]
2d5b33 Add scriptc404e9 foo20ec55 bar

Outro modo de se chegar ao mesmo resultado é fazendo o commit com git commit --fixup=df4adc (ou git commit --fixup=:/"Add script"). Se você então rodar o comando git rebase --interactive --autosquash, o git vai editar a todo list automaticamente para você.

[Sem nome]
pick df4adc Add scriptfixup 3409df0 fixup! Add scriptpick 180a94 Add environment variablespick 490b6c Add .gitlab-ci.yml

Juntar commits, preservando a mensagem

Se quisermos juntar vários commits porém preservar ou reusar suas mensagens, precisamos usar o comando squash.

A única diferença de usar o comando fixup é que você vai ter a chance de editar a mensagem de commit final durante o processo de rebase:

[Sem nome]
# This is a combination of 2 commits.# This is the 1st commit message:Add script# This is the commit message #2:squash! Fix script# Please enter the commit message for your changes. Lines starting# with '#' will be ignored, and an empty message aborts the commit.## ...

Analogamente à opção --fixup disponível no git commit, também pode-se usar git commit --squash=sha.

Separar um commit

Para separar um commit em um ou mais, você pode usar o comando edit.

O git vai então parar o rebase naquele commit e deixar você fazer o que bem entender. Quando terminar, rode o comando git rebase --continue.

Para separar um commit, eu normalmente primeiro desfaço o commit com git reset HEAD^, e então vou adicionando e commitando as changes de novo de forma diferente.

Bash
$ # undo the commit, but keep its changes in the working tree$ git reset HEAD^$ git add foo$ git commit -m "One commit"$ git add bar$ git commit -m "Another commit"$ git rebase --continue

Remover um arquivo de um commit

Você vai precisar do comando edit aqui também.

Mas agora, é mais conveniente usar o git reset --soft para desfazer o commit, porque as mudanças daquele commit vão permanecer na staging area, e então basta remover o arquivo da staging area e commitar de novo:

Bash
$ # undo the commit, but keep its changes in the staging area$ git reset --soft HEAD^$ # remove the file from the staging area$ git rm --cached .env$ # reuse the previous commit message$ git commit -c ORIG_HEAD

Editar uma mensagem

Use o comando reword. git vai abrir seu editor e deixar você editar a mensagem como se fosse pela primeira vez commitando.

Deletar um commit

Para deletar um commit, use o comando drop ou apenas delete a linha.