Git filter-branch vs filter-repo: reescrevendo histórico com segurança

1. Introdução à reescrita de histórico no Git

Reescrever o histórico de um repositório Git é uma operação delicada, porém necessária em diversas situações. Os casos de uso mais comuns incluem:

  • Remoção de dados sensíveis: arquivos com senhas, chaves de API ou informações pessoais que nunca deveriam ter sido commitados
  • Limpeza de arquivos grandes: remoção de binários ou arquivos que inflam desnecessariamente o repositório
  • Correção de autoria: ajuste de e-mails ou nomes de autores incorretos

Os riscos inerentes à reescrita de histórico são significativos: commits órfãos, conflitos em branches compartilhados, perda irreversível de dados e quebra do fluxo de trabalho da equipe. Por isso, a escolha da ferramenta correta é crucial.

Duas ferramentas dominam esse cenário: o clássico git filter-branch (considerado legado) e o moderno git filter-repo (oficialmente recomendado). Neste artigo, exploraremos ambas em profundidade.

2. Git filter-branch: a ferramenta clássica (e problemática)

O git filter-branch foi durante anos a única opção nativa para reescrita de histórico. Sua sintaxe básica permite operações como remoção de arquivos:

git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch arquivo-sensivel.pdf" \
  --prune-empty --tag-name-filter cat -- --all

Para alterar autor:

git filter-branch --env-filter \
  'if [ "$GIT_AUTHOR_EMAIL" = "antigo@email.com" ]; then
     export GIT_AUTHOR_EMAIL="novo@email.com";
     export GIT_COMMITTER_EMAIL="novo@email.com";
   fi' -- --all

Limitações conhecidas

O filter-branch apresenta problemas graves:

  • Lentidão extrema: em repositórios com milhares de commits, a operação pode levar horas
  • Comportamento imprevisível: falha silenciosamente em merges complexos
  • Falta de segurança: não exige confirmação explícita antes de reescrever referências
  • Uso intensivo de memória: mantém todo o histórico em RAM

Armadilhas comuns

Usuários frequentes do filter-branch conhecem bem os perigos:

# PERIGO: sobrescrita acidental de branches
git filter-branch --tree-filter 'rm -f senhas.txt' HEAD

# PERIGO: sem backup, sem verificação
# Se algo der errado, o reflog pode não salvar

A documentação oficial do Git passou a desencorajar ativamente o uso do filter-branch, classificando-o como uma ferramenta que "pode causar problemas" e "não é segura".

3. Git filter-repo: a alternativa moderna e segura

O git filter-repo é uma ferramenta externa, oficialmente recomendada pela equipe do Git. Sua instalação é simples:

# Instalação via pip
pip install git-filter-repo

# Ou via gerenciador de pacotes do sistema
brew install git-filter-repo  # macOS
sudo apt install git-filter-repo  # Ubuntu/Debian

Comandos essenciais

Remoção de arquivo específico:

git filter-repo --path arquivo-sensivel.pdf --invert-paths

Substituição de e-mails usando mailmap:

# Primeiro, crie um arquivo .mailmap
git filter-repo --mailmap <(echo "Novo Nome <novo@email.com> <antigo@email.com>")

Renomeação de tags:

git filter-repo --tag-rename 'v1:release-v1'

Vantagens sobre filter-branch

O filter-repo oferece benefícios substanciais:

  • Performance: 10x a 100x mais rápido que filter-branch
  • Segurança: exige --force explícito para reescrever
  • Suporte nativo a merges: processa merges complexos sem intervenção manual
  • Uso eficiente de recursos: opera em modo streaming, sem carregar tudo em memória

4. Comparação prática: filter-branch vs filter-repo

Cenário 1: Remoção de dados sensíveis

Com filter-branch:

git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch dados-clientes.csv" \
  --prune-empty --tag-name-filter cat -- --all
# Tempo estimado: 15 minutos para 5000 commits

Com filter-repo:

git filter-repo --path dados-clientes.csv --invert-paths
# Tempo estimado: 30 segundos para 5000 commits

Cenário 2: Substituição de e-mails em massa

Com filter-branch:

git filter-branch --env-filter \
  'if [ "$GIT_AUTHOR_EMAIL" = "@empresa-antiga.com" ]; then
     export GIT_AUTHOR_EMAIL="@empresa-nova.com";
     export GIT_COMMITTER_EMAIL="@empresa-nova.com";
   fi' -- --all

Com filter-repo:

git filter-repo --email-callback \
  'return email.replace(b"@empresa-antiga.com", b"@empresa-nova.com")'

Cenário 3: Extração de subdiretório

Com filter-branch (problemático):

git filter-branch --subdirectory-filter src/modulo -- --all
# Perde histórico de merges e tags

Com filter-repo:

git filter-repo --path src/modulo/ --path-rename src/modulo/:
# Preserva merges e permite renomear caminhos

Em repositórios de médio porte (10.000 commits, 500MB), o filter-repo completa operações em segundos onde o filter-branch levaria horas.

5. Boas práticas de segurança ao reescrever histórico

Antes de qualquer operação de reescrita, siga este checklist:

Backup completo

# Crie um mirror completo do repositório
git clone --mirror https://github.com/usuario/repo.git repo-backup

# Verifique a integridade do backup
cd repo-backup
git fsck --full

Uso de branches isolados

# Nunca reescreva main diretamente
git checkout -b reescrita-segura
git filter-repo --path dados-sensiveis.txt --invert-paths

# Apenas após validação, force o push
git push origin reescrita-segura --force

Verificação pós-operação

# Verifique a integridade do repositório
git fsck --full

# Compare hashes de commits preservados
git log --oneline --all | head -20

# Revise o diff final
git diff main..reescrita-segura --stat

Comunicação com a equipe

Sempre avise a equipe com antecedência e estabeleça uma janela de manutenção. Todos devem ter feito push de seus trabalhos antes da reescrita.

6. Casos especiais e resolução de problemas

Merges complexos

O filter-repo lida nativamente com merges complexos. O filter-branch exige o uso de --parent-filter:

# filter-branch: necessário filtro manual para merges
git filter-branch --parent-filter \
  'sed "s/^\$//"' -- --all

Tags e releases

# filter-repo preserva tags por padrão
git filter-repo --path arquivo-grande.zip --invert-paths

# Se precisar renomear tags
git filter-repo --tag-rename 'v:release-'

Recuperação de desastres

Se algo der errado:

# Use o reflog para encontrar o estado anterior
git reflog show --all

# Restaure a partir do backup
cd repo-backup
git push ../repo-original --all --force
git push ../repo-original --tags --force

7. Conclusão e recomendações finais

As diferenças cruciais entre as ferramentas são claras:

Característica filter-branch filter-repo
Performance Lento (horas) Rápido (segundos)
Segurança Baixa Alta (--force obrigatório)
Merges Problemático Suporte nativo
Manutenção Legado (não recomendado) Ativo (recomendado)

Quando usar filter-branch? Apenas em ambientes extremamente restritos onde a instalação de ferramentas externas é proibida. Em todos os outros casos, use filter-repo.

Checklist final para reescrita segura:

  1. ✅ Crie backup completo (git clone --mirror)
  2. ✅ Trabalhe em branch isolado
  3. ✅ Teste a operação em um clone local
  4. ✅ Valide o resultado com git fsck
  5. ✅ Comunique a equipe
  6. ✅ Use git push --force-with-lease (não --force simples)
  7. ✅ Confirme que todos os membros atualizaram seus clones

Reescrever histórico é uma operação de alto risco. Com as ferramentas e práticas corretas, você pode realizá-la com segurança e eficiência.

Referências