git rebase -i: arrumar os commits antes de alguém os ler
Um ramo pode funcionar perfeitamente e continuar a ser mau de rever. Cinco commits chamados wip, wip2, corrige typo e agora vai fazem exatamente o mesmo que três commits com uma ideia cada — só que custam o dobro a quem os lê.
O git rebase -i serve para isso: arrumar antes de mostrar. Fica dito uma vez e não se repete: isto só se faz em commits que mais ninguém tenha na máquina dele.
1. A lista aparece ao contrário
Abre-se com o número de commits a mexer, contado a partir da ponta:
git rebase -i HEAD~5E aparece um ficheiro parecido com este:
pick 4c1d09 primeiro esboço de um validador
pick a3f9c2 wip
pick 7b2e44 wip2
pick 9f2a1b corrige typo
pick 8e4b17 agora vai
# p, pick = usar o commit
# r, reword = usar o commit, mas mudar a mensagem
# e, edit = parar neste commit para o alterar
# s, squash = juntar ao de cima, e juntar as mensagens
# f, fixup = juntar ao de cima, e deitar a mensagem fora
# d, drop = deitar o commit foraO primeiro tropeção é sempre o mesmo: esta lista está por ordem cronológica, o mais antigo em cima — ao contrário do git log, onde o mais recente vem primeiro. Quem não repara nisso junta os commits ao contrário e não percebe porquê.
Mudar a ordem das linhas muda a ordem dos commits. E apagar uma linha faz o mesmo que drop: o commit desaparece, sem confirmação nenhuma pelo caminho.
2. squash ou fixup
São o mesmo movimento, com um detalhe a separá-los: o squash abre o editor para juntar as duas mensagens; o fixup deita fora a mensagem do commit absorvido.
Na prática é quase sempre fixup. Um commit chamado corrige typo não tem mensagem nenhuma que valha a pena guardar.
pick 4c1d09 primeiro esboço de um validador
fixup a3f9c2 wip
fixup 7b2e44 wip2
pick 9f2a1b aceita endereços sem protocolo
fixup 8e4b17 agora vaiCinco commits ficam dois, cada um com uma ideia inteira e uma mensagem que se lê.
3. O truque que dispensa a lista
Há forma de não fazer nada disto à mão. Quando o que se está a escrever corrige um commit anterior, em vez de inventar mais uma mensagem:
git commit --fixup=4c1d09Isso cria um commit com a mensagem fixup! primeiro esboço de um validador. Depois, na altura de arrumar:
git rebase -i --autosquash HEAD~5A lista já vem ordenada e com os fixup nos sítios certos — só falta gravar e fechar. E como isto se quer sempre:
git config --global rebase.autosquash trueÉ a diferença entre arrumar a história no fim, de memória, e ir dizendo ao Git o que pertence a quê enquanto ainda se sabe.
4. Partir um commit em dois
O caso contrário: um commit que faz duas coisas. Marca-se com edit, e o rebase pára ali, com o commit já desfeito à espera.
git reset HEAD~ # desfaz o commit, mantém as alterações
git add src/validador.ts
git commit -m "aceita endereços sem protocolo"
git add src/media.ts
git commit -m "usa o validador nas miniaturas"
git rebase --continueO git add -p ajuda aqui: deixa escolher pedaços de um ficheiro, e não o ficheiro inteiro.
5. Os conflitos vêm um por commit
Um merge resolve o conflito uma vez. O rebase reaplica os commits um a um, portanto o mesmo conflito pode aparecer várias vezes seguidas, uma por cada commit que toque nas mesmas linhas. Não é sinal de que algo correu mal; é a consequência de reescrever.
# resolver os ficheiros e depois
git add .
git rebase --continue
git rebase --skip # descartar este commit e seguir
git rebase --abort # desistir, e fica tudo como estavaPara não resolver a mesma coisa cinco vezes, há uma opção que o Git tem há anos e quase ninguém liga:
git config --global rerere.enabled trueO rerere — reuse recorded resolution — grava como resolveu um conflito e aplica a mesma resolução da próxima vez que o encontrar. Num rebase longo, é a diferença entre cinco minutos e meia hora.
6. O --onto, para quando se ramificou do sítio errado
Ramificou de funcionalidade-antiga em vez de main, e agora o seu ramo arrasta commits que não são seus. É este o caso que justifica conhecer o comando a sério:
# Levar os commits de "minha-funcionalidade" que não estão em
# "funcionalidade-antiga", e assentá-los em cima da main
git rebase --onto main funcionalidade-antiga minha-funcionalidadeLê-se de trás para a frente: o que mexer é o último argumento, onde começa é o do meio, e para onde vai é o primeiro.
7. Publicar o que foi reescrito
Depois de um rebase, o ramo local e o remoto divergiram, e um git push normal é recusado. A saída óbvia é a errada:
git push --force # atropela o que lá estiver
git push --force-with-lease # recusa se alguém publicou entretantoO --force-with-lease só empurra se o remoto ainda estiver onde o seu Git pensa que ele está. Se um colega publicou alguma coisa nesse intervalo, falha em vez de a apagar. Não custa mais nada a escrever, e é a diferença entre reescrever o seu trabalho e reescrever o de outra pessoa.
Se correr mal
A meio, o git rebase --abort devolve tudo ao estado anterior. Depois de acabar, o ramo como estava antes continua no git reflog: reescrever no Git não apaga nada, apenas deixa de apontar para o que lá estava.