Desmistificando git rebase
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
.envincluí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:
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:
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:
E---F feature
/
A---B---C---D main Por alguma razão, um colega de trabalho decidiu colaborar com um commit:
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:
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:
pick df4adc E
pick 180a94 F
pick 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:
E---F---G feature
/
A---B---C---D main Depois de rodar git rebase -i main, você vai ver a seguinte todo list:
pick df4adc E
pick 180a94 F
pick 490b6c G Agora, simplesmente inverta a ordem das linhas:
pick 490b6c G
pick 180a94 F
pick df4adc E Salve o arquivo e feche. Caso não haja conflitos, tudo funcionará e o resultado vai ser esse novo commit history:
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:
df4adc Add script
180a94 foo
490b6c bar O commit “Add script” tem um bug e você quer consertar. Você fez o fix e
commitou com git commit -m "Fix script":
df4adc Add script
180a94 foo
490b6c bar
3409df Fix script Depois de rodar git rebase -i main, a seguinte todo list vai aparecer:
pick df4adc Add script
pick 180a94 foo
pick 490b6c bar
pick 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.
pick df4adc Add script
fixup 3409df Fix script
pick 180a94 foo
pick 490b6c bar No commit history resultante vai parecer que você nunca introduziu aquele bug:
2d5b33 Add script
c404e9 foo
20ec55 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ê.
pick df4adc Add script
fixup 3409df0 fixup! Add script
pick 180a94 Add environment variables
pick 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:
# 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.
$ # 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:
$ # 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.