A ferramenta mais evitada do Git
Pergunte a uma sala cheia de desenvolvedores qual comando Git os deixa nervosos, e git rebase -i vai aparecer entre os primeiros. O rebase interativo tem fama: ele reescreve o histórico, abre uma estranha todo list no seu editor e parece perigosamente fácil quebrar alguma coisa. Então a maioria das pessoas simplesmente o evita -- e suas pull requests chegam carregando commits como "wip", "fix typo" e "actually fix it".
É uma pena, porque o rebase interativo é a melhor ferramenta que existe para transformar uma branch bagunçada em um histórico limpo e fácil de revisar. Neste guia vamos desmistificá-lo com um exemplo concreto que nos acompanhará do início ao fim: uma branch de feature com 9 commits bagunçados que vamos remodelar em 3 commits limpos antes de abrir uma pull request. Se você ainda está se perguntando se o rebase é mesmo a escolha certa para o seu fluxo de trabalho, comece pela nossa comparação entre rebase e merge -- este artigo assume que você já decidiu fazer rebase e quer fazê-lo bem.
Por que um histórico limpo importa
Limpar os commits antes de uma pull request não é uma questão de estética. Um histórico organizado compensa de três maneiras muito práticas:
- Revisabilidade. Um revisor lendo três commits lógicos -- "Add login form with validation", "Add remember me option", "Add error messages" -- pode revisar cada alteração isoladamente. O mesmo código espalhado por nove commits pela metade o força a revisar o diff inteiro de uma vez, e a qualidade da revisão despenca.
- git bisect. Quando um bug aparece semanas depois,
git bisectpercorre o histórico para encontrar o commit que o introduziu. Isso só funciona se cada commit compila e faz sentido por si só. Um histórico cheio de commits "wip" que nem compilam torna o bisect praticamente inútil. - Revert. Se uma funcionalidade precisa ser desfeita, reverter um commit autocontido é trivial. Reverter uma funcionalidade espalhada por nove commits entrelaçados é uma tarde inteira de arqueologia.
Nosso exemplo guia: 9 commits bagunçados
Aqui está a branch que vamos limpar. É um histórico de trabalho perfeitamente normal -- é assim que todo mundo realmente programa:
$ git log --oneline main..feature/login
c9d0e1f oops forgot the css file
b8c9d0e add error messages
a7b8c9d wip 2
f6a7b8c add remember me checkbox
e5f6a7b actually fix it
d4e5f6a fix typo
c3d4e5f add form validation
b2c3d4e wip
a1b2c3d add login form skeleton
Não há nada de errado em fazer commits assim enquanto você trabalha. O problema é apenas se for entregue assim. Nosso objetivo é terminar com três commits:
- Add login form with validation
- Add remember me option
- Add error messages with styling
Iniciando o rebase: git rebase -i HEAD~9
Para reescrever os últimos 9 commits, execute:
git rebase -i HEAD~9
HEAD~9 significa "o commit 9 passos antes do atual" -- tudo depois desse ponto fica disponível para edição. Git abre seu editor com a todo list:
pick a1b2c3d add login form skeleton
pick b2c3d4e wip
pick c3d4e5f add form validation
pick d4e5f6a fix typo
pick e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
pick a7b8c9d wip 2
pick b8c9d0e add error messages
pick c9d0e1f oops forgot the css file
Duas coisas para entender antes de tocar em qualquer coisa. Primeiro, isto é apenas um arquivo de texto: nada acontece até você salvar e fechar, e se você apagar todas as linhas ou fechar sem salvar alterações significativas, o rebase é cancelado sem causar dano algum. Segundo, a ordem é do mais antigo para o mais recente -- o oposto de git log. Git vai reaplicar os commits de cima para baixo, executando o verbo no início de cada linha.
Os seis verbos
pick
Mantém o commit exatamente como está. É o padrão para todas as linhas.
reword
Mantém as alterações do commit, mas para para deixar você editar a mensagem. Perfeito para consertar "wip 2" sem tocar no código.
edit
Pausa o rebase neste commit. Você pode fazer amend, dividi-lo em vários commits ou testá-lo, e depois continuar com git rebase --continue. É o verbo mais avançado e o que você usará com menos frequência.
squash
Funde este commit com o de cima, e abre o editor para você combinar as duas mensagens de commit em uma só.
fixup
Como squash, mas descarta completamente a mensagem deste commit. O commit de cima mantém sua mensagem inalterada. É o que você quer para commits "fix typo" -- as mensagens deles não carregam nenhuma informação que valha a pena guardar.
drop
Exclui o commit e suas alterações completamente. Apagar a linha da todo list tem o mesmo efeito, mas escrever drop torna sua intenção explícita.
Limpando a branch, passo a passo
Agora vamos aplicar isso aos nossos 9 commits. Duas operações são necessárias: reordenar e trocar os verbos.
Primeiro, a reordenação. Olhando os diffs, "oops forgot the css file" é na verdade a folha de estilo do checkbox remember me, não das mensagens de erro. Na todo list, reordenar é simplesmente mover uma linha: recorte a linha c9d0e1f e cole logo depois do grupo do remember me. Git vai reaplicar os commits na nova ordem.
Depois, os verbos. Tudo até "actually fix it" pertence ao formulário de login, então isso se condensa no primeiro commit. Usamos squash em "add form validation" porque queremos mesclar sua mensagem na mensagem final, e fixup nos commits de ruído cujas mensagens merecem desaparecer. Aqui está a todo list editada:
pick a1b2c3d add login form skeleton
fixup b2c3d4e wip
squash c3d4e5f add form validation
fixup d4e5f6a fix typo
fixup e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
fixup a7b8c9d wip 2
fixup c9d0e1f oops forgot the css file
reword b8c9d0e add error messages
Salve e feche. Git reaplica os commits e para duas vezes: uma para você escrever a mensagem combinada do primeiro grupo (digite "Add login form with validation"), e outra para o reword (digite "Add error messages with styling"). Para o commit do remember me, um reword rápido também teria funcionado -- aqui o deixamos como pick e sua mensagem original já era decente. O resultado:
$ git log --oneline main..feature/login
3f4e5d6 Add error messages with styling
2e3d4c5 Add remember me option
1d2c3b4 Add login form with validation
Nove commits viraram três, cada um autocontido e fácil de revisar. Note que todos os hashes mudaram -- estes são commits totalmente novos. Guarde esse detalhe; ele é o coração da regra de ouro abaixo.
A jogada de profissional: --autosquash
Quando você estiver confortável, pode preparar a limpeza enquanto trabalha em vez de resolver tudo no final. Quando você conserta algo de um commit anterior, registre assim:
git commit --fixup a1b2c3d
Git cria um commit cuja mensagem é fixup! add login form skeleton. Mais tarde, execute:
git rebase -i --autosquash main
Git pré-preenche a todo list com cada commit fixup! já movido para baixo do seu commit alvo e marcado como fixup. Você só precisa salvar e fechar. Para tornar isso o comportamento padrão, configure uma única vez:
git config --global rebase.autosquash true
A regra de ouro: nunca faça rebase de commits compartilhados
O rebase não modifica commits -- ele os substitui por commits novos. Se colegas já fizeram pull dos commits antigos e basearam trabalho neles, reescrever esses commits cria dois históricos divergentes, e o próximo merge vira uma bagunça de duplicatas e conflitos.
A regra é simples: só faça rebase de commits que ainda não foram enviados com push, ou que vivem em uma branch na qual só você trabalha. Limpar sua própria branch de feature antes de abrir uma pull request é o caso seguro clássico. Se você já fez push da branch e ainda é o único autor dela, envie o histórico reescrito com git push --force-with-lease, que se recusa a sobrescrever trabalho que outra pessoa enviou nesse meio tempo. Nunca reescreva a main nem qualquer branch da qual sua equipe faz pull.
Quando dá errado
No meio de um rebase, duas coisas podem acontecer. Se um commit reaplicado entra em conflito com uma alteração anterior, Git pausa e pede que você resolva o conflito, depois faça git add dos arquivos e execute git rebase --continue. E se em qualquer momento você se sentir perdido, existe um botão de ejeção total:
git rebase --abort
Isso devolve a branch ao estado exato em que ela estava antes de você começar, sem fazer perguntas. Enquanto você lembrar que --abort existe, um rebase em andamento nunca poderá prendê-lo.
E se o rebase terminou e você percebe que o resultado está errado? Mesmo assim, nada está perdido. Os commits antigos ainda existem -- Git os mantém por semanas, e o reflog registra cada posição para a qual sua branch já apontou, então você pode voltar ao estado pré-rebase com um único reset. Cobrimos essa rede de segurança em detalhes em recuperando commits perdidos com git reflog. E se o seu erro for menor -- apenas o último commit precisa mudar -- você nem precisa de um rebase: veja como desfazer um commit do Git.
Rebase interativo no GitSquid
A todo list é poderosa, mas editar verbos em um arquivo de texto é exatamente a parte que deixa as pessoas nervosas. GitSquid a substitui por um rebase interativo visual: você reordena commits com drag & drop e escolhe a ação de cada commit -- pick, squash, fixup, reword ou drop -- em um seletor dropdown. O plano inteiro fica visível de relance antes de qualquer coisa ser executada, o que elimina a maior parte do fator medo.
GitSquid também ajuda antes mesmo de você começar: o Conflict Predictor, incluído no plano gratuito, mostra quais arquivos vão entrar em conflito antes de você iniciar um rebase, então você sabe no que está se metendo em vez de descobrir conflitos no meio do caminho. O rebase está disponível diretamente nos menus de contexto de branch e commit no grafo de commits.
Referência rápida
| Comando / verbo | O que faz |
|---|---|
git rebase -i HEAD~n |
Reescreve interativamente os últimos n commits |
pick |
Mantém o commit como está |
reword |
Mantém as alterações, edita a mensagem |
edit |
Pausa neste commit para fazer amend ou dividi-lo |
squash |
Funde com o commit anterior, combina as mensagens |
fixup |
Funde com o commit anterior, descarta esta mensagem |
drop |
Exclui o commit por completo |
git commit --fixup <hash> |
Registra uma correção destinada a um commit anterior |
git rebase -i --autosquash |
Posiciona automaticamente os commits fixup! na todo list |
git rebase --continue |
Retoma após resolver um conflito |
git rebase --abort |
Cancela e restaura o estado pré-rebase |
O rebase interativo recompensa um pouco de prática com muito poder. Comece em uma branch descartável, mantenha o --abort em mente, respeite a regra de ouro, e transformar "wip, fix typo, actually fix it" em um histórico limpo vira rotina. E se você preferir ver o plano em vez de editar um arquivo de texto, baixe o GitSquid grátis e experimente o rebase visual na sua próxima branch de feature.