Reescrevendo o histórico com segurança
Toda reescrita - commit --amend, reset, rebase, filter-repo - produz novos commits
e move o ponteiro da branch para longe dos antigos. Isso é inofensivo enquanto ninguém mais tiver os commits
antigos, e destrutivo no momento em que alguém os tem: o clone dessa pessoa ainda tem o histórico antigo,
e na próxima vez que ela fizer push ou pull, ele volta como duplicatas ou conflitos. Portanto, a primeira
pergunta nunca é "como", é "quem mais tem esses commits?"
1. Decida: reescrever ou reverter
git fetch --prune
git status --short --branch # ahead/behind, e estado sujo
git branch -r --contains <commit> # quais branches remotas já o contêm
git log --oneline @{u}..HEAD # commits que existem apenas localmente
| Onde o commit reside | Faça isto |
|---|---|
| Apenas local, nunca enviado | Reescreva à vontade. |
| Enviado para uma branch na qual apenas este usuário trabalha (branch de PR dele) | Reescreva, depois faça push com lease (passo 5). |
Em uma branch compartilhada ou protegida: main, master, develop, release/*, ou qualquer coisa com commits de outras pessoas em cima | Não reescreva. Use git revert. |
| Já lançado ou implantado | git revert. Uma reescrita não pode desfazer a implantação. |
Quando o usuário diz "livre-se daquele commit da main", ele geralmente quer que seu efeito
suma, não seu SHA. git revert <sha> faz isso, mantém o histórico honesto e precisa apenas de um push
comum. Para um commit de merge, git revert -m 1 <merge-sha>. Explique por que escolheu reverter,
e se o usuário insistir em apagá-lo de uma branch compartilhada, nomeie quem será afetado e
obtenha um sim explícito primeiro.
Um revert gera conflitos quando commits posteriores editaram as mesmas linhas - que é exatamente o caso
em que commits foram construídos em cima do ruim. Resolva como qualquer conflito (veja
git-conflict-resolution): remova a alteração ruim, mantenha o trabalho posterior, depois
git revert --continue.
Uma armadilha do revert: depois de reverter um merge, mesclar a mesma branch novamente não traz nada de volta, porque o git acha que esses commits já chegaram. Para trazer o trabalho de volta, reverta o revert primeiro.
2. Crie um ponto de recuperação antes de tocar em qualquer coisa
git rev-parse HEAD # anote
git branch backup/<branch>-pre-rewrite # uma ref real, não apenas o reflog
O reflog também registra onde a branch estava, mas é local a este clone, expira
(90 dias para entradas alcançáveis, 30 para inalcançáveis por padrão), e git gc após
reflog expire o encerra. Uma branch de backup nomeada não custa nada. Deixe-a no lugar até que o
usuário tenha confirmado o resultado.
3. Escolha o comando menos destrutivo
| Objetivo | Comando | Notas |
|---|---|---|
| Corrigir a mensagem do último commit ou adicionar um arquivo esquecido | git commit --amend | -m "..." ou --no-edit evita abrir um editor. |
| Desfazer commit mas manter as mudanças staged | git reset --soft HEAD~1 | |
| Desfazer commit e unstaged, manter os arquivos | git reset --mixed HEAD~1 (o padrão) | |
| Mover uma branch para trás, recusando perder edições locais | git reset --keep <commit> | Aborta se sobrescreveria mudanças não commitadas. Prefira isto a --hard. |
| Jogar fora também o trabalho não commitado | git reset --hard | O trabalho não commitado não está no reflog e não pode ser recuperado. Só após confirmar que nada ali é desejado; git stash -u antes se não tiver certeza. |
| Descartar alterações em um arquivo | git restore <file> | |
| Squash ou agrupar correções em commits anteriores | git commit --fixup=<sha> e depois autosquash | Veja abaixo. |
| Remover ou reordenar commits | rebase -i com um todo fornecido | Veja abaixo. |
Um hook de pre-commit falho significa que nenhum commit foi feito. Executar git commit --amend para
"tentar novamente" modifica o commit anterior, mesclando as novas alterações no errado.
Após uma falha de hook, corrija o problema, re-faça o stage e faça um novo commit normal.
Rebase interativo sem um editor interativo
git rebase -i abre um editor, que trava em um shell não interativo. Não confie nele.
Duas maneiras que funcionam:
# Agrupar commits de correção em seus alvos - aceite o todo que o git gera:
git commit --fixup=<target-sha>
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>
# Remover ou reordenar: escreva o todo você mesmo.
GIT_SEQUENCE_EDITOR="sed -i '/^pick <short-sha> /d'" git rebase -i <base>
<base> é o commit anterior ao primeiro que você deseja alterar. Se um rebase parar em um
conflito, resolva-o, git add, depois GIT_EDITOR=true git rebase --continue. Se der errado,
git rebase --abort retorna ao ponto de partida.
4. Verifique se a reescrita alterou o que você pretendia
Uma edição de mensagem, reordenação ou squash deve deixar a árvore final idêntica:
git diff backup/<branch>-pre-rewrite HEAD --stat # vazio = mesmo conteúdo
git range-diff backup/<branch>-pre-rewrite...HEAD # comparação commit por commit
Se você pretendia remover conteúdo, o diff deve mostrar exatamente esse conteúdo e nada mais. Qualquer coisa inesperada significa restaurar a partir do backup e refazer.
5. Publique - apenas com a autorização do usuário
Enviar uma reescrita é o passo voltado para o exterior e difícil de reverter. Confirme primeiro, a menos que o usuário já tenha pedido o push.
git push --force-with-lease=<branch>:<expected-remote-sha> --force-if-includes origin <branch>
- Nunca
--forcepuro. Ele sobrescreve o que estiver no remoto, incluindo um push de colega que você não viu. - Nomeie o SHA esperado. Um
--force-with-leasesimples compara contra sua ref de rastreamento remoto, e umgit fetchde fundo do IDE atualiza silenciosamente essa ref, então o lease não protege nada. Use o SHA que você viu no passo 1 (git rev-parse origin/<branch>). --force-if-includes(git 2.30+) adicionalmente recusa se o remoto moveu para algo que sua branch local nunca integrou.- Nunca faça force push em
main/master/develop/release/*. Se uma regra de hospedagem bloquear, essa regra está correta; não a contorne.
Após o push, informe ao usuário que qualquer um com a branch antiga em checkout deve fazer reset para a nova em vez de pull, e que os comentários de revisão de um PR aberto podem perder suas âncoras.
Guardrails
- Commits assinados perdem suas assinaturas ao serem reescritos; re-assine com
-Sou avise o usuário. - Não reescreva commits de outra pessoa sem perguntar, mesmo em uma branch de funcionalidade.
- Branches empilhadas acima da reescrita agora estão desatualizadas. Use
git-stacked-pr-rebasepara reempilhá-las em vez de fazer rebase de cada uma manualmente. - Um segredo commitado não é corrigido por uma reescrita. Rode a credencial primeiro; assuma que está comprometida.
- Relate claramente: qual era a ponta antiga, qual é a nova, onde está a ref de backup, e se algo não foi enviado. Se revert foi a escolha certa, diga que foi o que você fez.