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
.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:
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 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:
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 Epick 180a94 Fpick 490b6c G
Agora, simplesmente inverta a ordem das linhas:
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:
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 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"
:
df4adc Add script180a94 foo490b6c bar3409df Fix script
Depois de rodar git rebase -i main
, a seguinte todo list vai aparecer:
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
.
pick df4adc Add scriptfixup 3409df Fix scriptpick 180a94 foopick 490b6c bar
No commit history resultante vai parecer que você nunca introduziu aquele bug:
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ê.
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:
# 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.