Como usar git bisect para encontrar bugs rapidamente em bases grandes

1. Introdução ao git bisect: o detector binário de bugs

Em grandes repositórios com milhares de commits, encontrar a origem de um bug regressivo pode ser como procurar uma agulha em um palheiro. O git bisect resolve esse problema aplicando o princípio da busca binária ao histórico do Git. Em vez de verificar cada commit individualmente, o comando reduz o espaço de busca pela metade a cada iteração. Com 1000 commits entre a versão boa e a ruim, você precisa testar apenas cerca de 10 commits para isolar o culpado.

O git bisect é ideal para:
- Bugs regressivos: algo que funcionava antes e parou de funcionar
- Mudanças silenciosas: alterações em dependências, configurações ou lógica que não geram erros explícitos
- Débitos técnicos: problemas introduzidos por refatorações ou correções apressadas

Pré-requisitos essenciais:
1. Um commit onde o bug não existia (bom)
2. Um commit onde o bug está presente (ruim)
3. Um teste confiável que reproduza o bug de forma determinística

2. Configurando o cenário ideal para o bisect

Antes de iniciar o bisect, identifique com precisão os commits bom e ruim. Use git log --oneline para navegar pelo histórico:

git log --oneline -20

Crie um script de teste reprodutível. O script deve retornar 0 para sucesso (bug ausente) e não-zero para falha (bug presente). Exemplo:

#!/bin/bash
# test_bug.sh
./compilar.sh
./executar_teste_unitario
if [ $? -eq 0 ]; then
    exit 0  # bom, sem bug
else
    exit 1  # ruim, bug presente
fi

Cuidados para evitar falsos positivos:
- Use um ambiente limpo (container Docker ou máquina virtual)
- Limpe caches entre execuções: git clean -fd e git reset --hard
- Desabilite dependências de rede ou serviços externos
- Garanta que o teste seja rápido (menos de 30 segundos por iteração)

3. Executando o bisect manualmente passo a passo

Inicie o processo com o commit ruim (onde o bug aparece) e o commit bom (última versão estável):

git bisect start
git bisect bad HEAD                    # commit atual contém o bug
git bisect good abc1234                # commit abc1234 não contém o bug

O Git automaticamente faz checkout de um commit intermediário. Você testa manualmente:

# Executa seu teste manual
./test_bug.sh
# Se o bug está presente:
git bisect bad
# Se o bug está ausente:
git bisect good

Repita o processo até que o Git exiba o primeiro commit ruim:

abc5678 is the first bad commit

Finalize com:

git bisect reset

4. Automatizando o bisect com scripts de teste

Para repositórios grandes com centenas de iterações, a automação é essencial. Use git bisect run com um script:

git bisect start HEAD abc1234
git bisect run ./test_bug.sh

O script deve retornar:
- 0: commit bom (bug ausente)
- 1-127: commit ruim (bug presente)
- 125: commit não testável (não compila, infraestrutura quebrada)

Exemplo completo de script robusto:

#!/bin/bash
# auto_bisect.sh
set -e

echo "=== Testando commit $(git rev-parse HEAD) ==="

# Tenta compilar
if ! make clean && make; then
    echo "Falha na compilação - pulando"
    exit 125
fi

# Executa o teste
if ./test_suite --specific-test=bug_regressivo; then
    echo "Bug não presente - GOOD"
    exit 0
else
    echo "Bug presente - BAD"
    exit 1
fi

Execute:

git bisect run ./auto_bisect.sh

5. Estratégias para bases grandes e históricos profundos

Em repositórios com mais de 10.000 commits, otimize o processo:

Use git bisect skip para commits que não podem ser testados:

git bisect skip          # pula o commit atual
git bisect skip range    # pula um intervalo

Reduza o espaço de busca com intervalos amplos:

git bisect good v2.0.0   # versão estável antiga
git bisect bad v2.5.0    # versão com bug

Combine com filtros para restringir candidatos:

# Verifique apenas commits de um autor específico
git log --author="Maria" --oneline v2.0.0..v2.5.0 | head -50

Use --first-parent em históricos com merges complexos para seguir apenas a linha principal:

git bisect start --first-parent

6. Casos reais e armadilhas comuns

Bug intermitente: Execute o teste múltiplas vezes e use média ou votação:

#!/bin/bash
success=0
for i in {1..5}; do
    ./test_bug.sh && success=$((success+1))
done
if [ $success -ge 3 ]; then
    exit 0  # maioria dos testes passou
else
    exit 1
fi

Dependências de merge: Em merges complexos, o bisect pode apontar para um merge commit. Use --first-parent para evitar isso.

Submodules: O bisect não gerencia submodules automaticamente. Sincronize manualmente:

git submodule update --init --recursive

Sparse checkout: Verifique se os arquivos necessários estão disponíveis no checkout parcial.

7. Boas práticas e integração no fluxo de trabalho

Documente o commit culpado com git bisect log:

git bisect log > bisect_log.txt

Integre no CI/CD como ferramenta de pós-deploy:

# Script de CI que executa bisect automaticamente
git bisect start production-canary HEAD~100
git bisect run ./ci_test.sh

Treine a equipe com um checklist rápido:

  1. Identifique commit bom e ruim
  2. Crie script de teste determinístico
  3. Execute git bisect run
  4. Analise o commit culpado
  5. Documente e compartilhe o resultado

Mantenha commits atômicos: Quanto menor e mais focado cada commit, mais preciso será o bisect.

Referências