Reescrita de Histórico de Commits
Reescreve o histórico de commits de um repositório existente para estar em conformidade com commits convencionais. Analisa commits, classifica-os por tipo, identifica candidatos a squash e executa a reescrita com todas as proteções de segurança.
Visão Geral
Histórico é documentação. Cada mensagem de commit na main é um registro permanente que futuros engenheiros (e agentes) usarão para entender o que mudou e por quê. Mensagens como "fix", "update", "wip" destroem essa documentação. Release-please analisa mensagens de commit para determinar incrementos de versão — mensagens não conformes produzem versões erradas ou nenhuma versão.
Código é um passivo. A Lei de Hyrum também se aplica ao histórico do Git: com ferramentas suficientes dependendo de mensagens de commit (changelogs, automação de release, git bisect, git blame), o formato dessas mensagens se torna um contrato. Limpar o histórico antes de adotar automação evita que mensagens antigas poluam todas as ferramentas downstream.
Esta skill reescreve as mensagens, não o código. A árvore de arquivos deve ser idêntica antes e depois — apenas os metadados do commit mudam.
Quando Usar
- Adotando commits convencionais em um repositório com histórico não conforme existente
- O log de commits está poluído com mensagens WIP, fixup, merge ou sem sentido
- Preparando um repositório para release-please (que precisa de commits convencionais para funcionar)
- Limpando antes de tornar o código aberto ou transferir um repositório
Quando NÃO Usar
- O repositório já foi publicado com commits convencionais (não há nada para reescrever)
- Ramificações compartilhadas com colaboradores ativos que não foram notificados
- Você só precisa corrigir os últimos commits (use
git rebase -i HEAD~Ndiretamente) - O repositório é um fork onde o upstream controla o formato dos commits
Processo
Passo 1: Avaliar o Repositório
# Contar commits totais
git rev-list --count HEAD
# Mostrar resumo das mensagens de commit
git log --oneline --no-merges
# Verificar colaboradores (diversidade de autoria)
git shortlog -sne --no-merges
# Identificar densidade de commits de merge
git log --merges --oneline | wc -l
Ponto de decisão: Se a contagem de commits < 50, use rebase interativo. Se >= 50, use git-filter-repo.
Passo 2: Classificar Commits Existentes
Analise cada commit usando dois sinais: o texto da mensagem e os arquivos alterados. Quando a mensagem é ambígua (ex.: "update stuff"), o diff é a fonte da verdade.
Sinal 1: Conteúdo da Mensagem
| Sinal na mensagem | Tipo Atribuído |
|---|---|
| "add", "new", "implement", "create", "introduce", "support" | feat |
| "fix", "bug", "patch", "resolve", "correct", "handle error" | fix |
| "refactor", "restructure", "reorganize", "simplify", "extract", "move" | refactor |
| "update dep", "bump", "upgrade", "version", "config", "setup" | chore |
| "test", "spec", "coverage", "assert" | test |
| "doc", "readme", "comment", "typo in doc" | docs |
| "ci", "workflow", "pipeline", "action", "deploy" | ci |
Sinal 2: Arquivos Alterados (quando a mensagem é ambígua)
git show --stat --format="" <hash>
| Padrão de arquivo | Tipo Atribuído |
|---|---|
Apenas *.md, docs/** | docs |
Apenas *test*, *spec*, __tests__/** | test |
Apenas .github/workflows/**, .gitlab-ci.yml, Jenkinsfile | ci |
Apenas package.json, *.lock, requirements.txt, Cargo.toml | chore |
Apenas arquivos de configuração (.eslintrc, tsconfig.json, .prettierrc) | chore |
Candidatos a Squash
| Sinal | Ação |
|---|---|
| Mensagem é "WIP", "wip", "temp", "save", "checkpoint", palavra única ou vazia | squash no commit significativo mais próximo |
| Mensagem é "fixup", "fix typo", "oops", "lint", "formatting" | squash no anterior |
| Commits de merge (de merges não-squash) | candidato a squash — avalie se o conteúdo já está na main |
Para commits ambíguos, verifique o diff completo (git show <hash>) para determinar o tipo a partir do conteúdo.
Passo 3: Escrever Mensagens de Qualidade
As mensagens reescritas devem seguir os padrões de mensagens de commit:
<tipo>[escopo opcional]: <descrição imperativa curta>
[corpo opcional explicando POR QUE, não o que]
Boas reescritas:
"initial setup" → "chore: initial project setup"
"added login" → "feat: add user login"
"fix bug" → "fix: resolve login redirect loop"
"Update README.md" → "docs: update README with setup instructions"
"updated deps" → "chore(deps): update dependencies"
Reescritas ruins (não faça estas):
"stuff" → "chore: stuff" ← ainda sem sentido
"changes" → "feat: changes" ← sem descrição
"fix" → "fix: fix" ← redundante
Quando a mensagem original não tem significado, leia o diff para escrever uma descrição adequada. Se o diff for muito grande ou ambíguo, pergunte ao usuário sobre o que era o commit.
Passo 4: Apresentar o Plano de Reescrita
Antes de qualquer ação destrutiva, mostre ao usuário um plano completo:
PLANO DE REESCRITA
===================
Total de commits: 47
Commits a reescrever: 32
Commits a fazer squash: 8
Commits já em conformidade: 7
Autores preservados: 3 (Alice <alice@co>, Bob <bob@co>, CI Bot <bot@co>)
MUDANÇAS PROPOSTAS:
abc1234 "initial setup" → "chore: initial project setup"
def5678 "added login" → "feat: add user login"
ghi9012 "wip" → [SQUASH em def5678]
jkl3456 "fix bug" → "fix: resolve login redirect loop"
mno7890 "Update README.md" → "docs: update README with setup instructions"
pqr1234 "feat: add search" → [MANTER — já em conformidade]
...
SEGURANÇA:
✓ Branch de backup: backup/pre-rewrite-<timestamp>
⚠ Push forçado necessário após a reescrita
⚠ Todos os colaboradores devem clonar novamente ou resetar após o push
INALTERADO:
- Árvore de arquivos (zero mudanças de conteúdo)
- Nome do autor, email e data em cada commit
- Ordem dos commits e relações de pai (exceto commits com squash)
Aguarde aprovação explícita do usuário antes de prosseguir. Não prossiga com "parece bom" — exija "sim" ou equivalente.
Passo 5: Criar Backup
# Crie um branch de backup no HEAD atual
git branch "backup/pre-rewrite-$(date +%Y%m%d-%H%M%S)"
# Verifique se o backup existe
git branch -l 'backup/*'
# Opcionalmente, envie o backup para o remoto para segurança extra
git push origin "backup/pre-rewrite-$(date +%Y%m%d-%H%M%S)"
Passo 6: Executar a Reescrita
Opção A: Rebase Interativo (< 50 commits)
# Rebase a partir da raiz
git rebase -i --root
No editor:
- Marque candidatos a squash com
squashoufixup - Marque reescritas de mensagens com
reword - Preserve a ordem dos commits
Preservação de autoria: O rebase interativo preserva nome, email e data do autor por padrão. NÃO use --reset-author. NÃO use --committer-date-is-author-date a menos que seja especificamente solicitado.
Opção B: git-filter-repo (>= 50 commits)
Crie um script de callback Python (rewrite-messages.py) que mapeia mensagens antigas para novas:
import re
REWRITES = {
b'prefixo do hash antigo': b'nova mensagem',
# ... gerado a partir do plano
}
def rewrite(commit, metadata):
# Pesquisar pela mensagem original ou hash
if commit.original_id in REWRITES:
commit.message = REWRITES[commit.original_id]
Aplique com:
git filter-repo --commit-callback "$(cat rewrite-messages.py)" --force
Preservação de autoria: git-filter-repo preserva a autoria por padrão. NÃO use --mailmap ou flags de reescrita de autor a menos que o usuário solicite explicitamente.
Squashing Commits
Para candidatos a squash, combine no commit significativo mais próximo. O commit resultante:
- Usa a mensagem de commit convencional do commit significativo
- Preserva o autor original do commit significativo (não do alvo do squash)
- Combina todas as alterações do intervalo do squash
Passo 7: Verificar
Execute todas as verificações de validação. Cada verificação deve passar antes de prosseguir para o push.
# 1. Revise o log reescrito
git log --oneline --no-merges
# 2. Valide TODAS as mensagens correspondem a commits convencionais
FAILURES=$(git log --format="%s" --no-merges | grep -cvE "^(feat|fix|docs|chore|refactor|test|ci)(\(.+\))?!?: .+")
if [[ "$FAILURES" -gt 0 ]]; then
echo "FALHA: $FAILURES commits não conformes encontrados:"
git log --format="%h %s" --no-merges | grep -vE "^[a-f0-9]+ (feat|fix|docs|chore|refactor|test|ci)(\(.+\))?!?: .+"
exit 1
fi
echo "PASSOU: todos os commits estão em conformidade"
# 3. Verifique ZERO mudanças de conteúdo (crítico — prova que apenas mensagens mudaram)
DIFF=$(git diff "backup/pre-rewrite-"*..HEAD --stat)
if [[ -n "$DIFF" ]]; then
echo "FALHA: diferenças de conteúdo detectadas — a reescrita alterou código, não apenas mensagens"
echo "$DIFF"
exit 1
fi
echo "PASSOU: árvore de arquivos idêntica"
# 4. Verifique preservação de autoria
echo "=== Autores antes ==="
git -C . log backup/pre-rewrite-* --format="%an <%ae>" --no-merges | sort -u
echo "=== Autores depois ==="
git log --format="%an <%ae>" --no-merges | sort -u
# Estas duas listas devem ser idênticas
Se alguma verificação falhar, NÃO faça push. Restaure a partir do backup e investigue:
git reset --hard backup/pre-rewrite-*
Passo 8: Push (com Confirmação)
Isto é destrutivo para colaboradores remotos. Confirme com o usuário:
⚠ AVISO DE FORCE PUSH
Branch: main
Remoto: origin
Ação: git push --force-with-lease origin main
Após este push, todos os colaboradores devem executar:
git fetch origin
git reset --hard origin/main
Digite "sim" para prosseguir:
git push --force-with-lease origin main
Use --force-with-lease (não --force) para evitar sobrescrever alterações remotas concorrentes. Se --force-with-lease for rejeitado, alguém enviou para o branch desde a reescrita — investigue antes de tentar novamente.
Limpeza Pós-Reescrita
Após push bem-sucedido e notificação aos colaboradores:
# Verifique se o remoto corresponde ao local
git log --oneline -5
git log --oneline -5 origin/main
# Estes devem ser idênticos
# Mantenha o branch de backup por pelo menos 2 semanas, depois delete
# git branch -D backup/pre-rewrite-*
# git push origin --delete backup/pre-rewrite-*
Racionalizações
| Racionalização | Realidade |
|---|---|
| "O histórico antigo não importa" | Release-please analisa mensagens de commit para determinar versões. Histórico não conforme produz versões erradas ou nenhuma versão. git bisect, git blame e git log --grep dependem de mensagens significativas. |
| "Vou simplesmente começar do zero a partir deste commit" | Você perde blame, autoria e contexto. Um branch --orphan destrói a narrativa do desenvolvimento. Reescrever preserva tudo, exceto mensagens ruins. |
| "É muito arriscado reescrever" | O branch de backup torna isso totalmente reversível. Verifique se o diff mostra zero mudanças de conteúdo. Se algo der errado: git reset --hard backup/pre-rewrite-*. |
| "Vou reescrever depois, quando for importante" | As reescritas de histórico ficam mais difíceis com o tempo, à medida que mais pessoas clonam e criam branches a partir dele. Faça agora, antes que existam mais dependências downstream. |
| "Force push é perigoso" | --force-with-lease protege contra sobrescrever o trabalho de outros. Combinado com o branch de backup, isso é seguro. |
| "As mensagens são boas o suficiente" | "Boas o suficiente" significa que release-please não vai analisá-las. Commits convencionais são um formato estrito — ou uma mensagem está em conformidade ou não está. |
| "Posso simplesmente marcar releases manualmente" | Marcas manuais divergem, são esquecidas e carecem de integração com changelog. O objetivo é automação — mas automação precisa de entrada analisável. |
Bandeiras Vermelhas
- Pular a criação do branch de backup
- Usar
--forceem vez de--force-with-lease - Reescrever um branch no qual outros estão trabalhando ativamente sem notificação
- Diferenças de conteúdo aparecendo no diff do Passo 7 (significa que a reescrita alterou código, não apenas mensagens)
- Contagens de autores mudando após a reescrita (significa que a autoria não foi preservada)
- Prosseguir sem aprovação do usuário do plano de reescrita
- Mensagens reescritas que ainda são sem sentido (ex.:
"chore: stuff") - Fazer squash de commits de diferentes autores sem discussão
- Não manter o branch de backup por pelo menos 2 semanas após a reescrita