FNSP Web Page

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~5

E 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 fora

O 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 vai

Cinco 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=4c1d09

Isso cria um commit com a mensagem fixup! primeiro esboço de um validador. Depois, na altura de arrumar:

git rebase -i --autosquash HEAD~5

A 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 --continue

O 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 estava

Para 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 true

O rererereuse 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-funcionalidade

Lê-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 entretanto

O --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.

Comentários

Ainda ninguém comentou este artigo.

Voltar ao blog

Gostávamos de saber quantas pessoas visitam o site, com o Google Analytics. Sem a sua autorização não corre nada. Política de Privacidade.