Seu CI está vermelho na main, mas estava verde ontem. Em algum lugar entre então e agora, um bug se infiltrou. Você poderia rolar por 200 commits, lendo diffs e adivinhando. Você poderia perguntar no Slack e torcer para alguém se lembrar de ter tocado no código relevante. Ou você poderia deixar o Git fazer o trabalho para você.

git bisect é uma ferramenta de busca binária para seu histórico de commits. Você marca um commit como bad e outro como good. O Git faz checkout do ponto médio. Você testa, marca como good ou bad, e o Git repete. Em 8 passos, ele pode isolar um único commit entre 256. Em 12 passos, ele pode encontrar a agulha em 4.096. É a coisa mais próxima que o Git tem de um debugger para o próprio histórico.

Por que bisection manual é um desperdício do seu tempo

Desenvolvedores já fazem bisection manual sem perceber. Você faz checkout de um commit mais antigo, roda os testes, e pensa “ainda quebrado, tenho que ir mais para trás.” Depois você faz checkout de um ainda mais antigo, e os testes passam. Agora você sabe que o bug está em algum lugar entre esses dois pontos. Então você escolhe um no meio e continua reduzindo.

Isso é busca binária. git bisect automatiza a parte de controle para que você não perca a noção de quais commits já testou. Mais importante, ele previne a heurística preguiçosa de “provavelmente foi naquele grande refactor de terça” que te manda pelo buraco de coelho errado por duas horas.

O valor real não é velocidade, embora seja mais rápido. O valor real é correção. Quando você está frustrado e caçando um bug às 18h, você vai cometer erros. Você vai esquecer de rebuildar após o checkout. Você vai testar o commit errado duas vezes. Você vai ler mal um resultado de teste e marcar um commit bad como good, destruindo seu espaço de busca. git bisect enforce a disciplina que você não tem quando está irritado.

Como a busca binária realmente funciona

O Git não busca cronologicamente. Ele busca topologicamente, caminhando pelo grafo de commits para encontrar o ponto médio entre seus commits known-good e known-bad. Isso importa quando seu histórico tem merges, porque o ponto médio cronológico pode nem ser alcançável a partir de ambos os endpoints.

Aqui está o que acontece por baixo dos panos. Você inicia o bisect e fornece limites:

git bisect start
git bisect bad HEAD          # current commit is broken
git bisect good v2.1.0       # this release was fine

O Git computa o número de commits entre esses dois pontos. Ele faz checkout do exatamente no meio e espera você testar. Você roda sua reprodução, vê se o bug está presente, e diz ao Git:

git bisect bad   # this commit has the bug
git bisect good  # this commit is clean

O Git descarta metade do espaço de busca e repete. Quando só resta um commit, ele para e mostra o primeiro commit bad. A saída inclui o hash do commit, autor, data e mensagem. Não há ambiguidade. Esse commit introduziu o bug, ponto final.

Um passo a passo concreto com comandos reais

Suponha que seus integration tests começaram a falhar esta manhã. Você sabe que eles passaram no último release taggeado, v1.4.0. Aqui está a sessão completa:

# Start the session
git bisect start

# Mark the current HEAD as bad
git bisect bad HEAD

# Mark the last known good release
git bisect good v1.4.0

# Git checks out a midpoint commit automatically
# You run your reproduction script or test suite:
npm test -- --grep "checkout flow"

# Tests fail. Mark it bad.
git bisect bad

# Git checks out another midpoint. Run tests again.
npm test -- --grep "checkout flow"

# Tests pass. Mark it good.
git bisect good

# Repeat until Git tells you:
# "<commit-hash> is the first bad commit"

No final, o Git te deixa no commit bad. Você pode inspecioná-lo com git show, criar uma correção, e depois limpar:

git bisect reset

Isso te retorna à branch em que você estava antes de começar. Se você esquecer de resetar, ficará em um detached HEAD e vai se perguntar por que seu próximo commit não está na sua branch. Eu já fiz isso. É embaraçoso.

Automatizando o processo inteiro com um script

A versão manual ainda exige que você rode testes e digite good ou bad repetidamente. Se sua reprodução é um único comando que sai com 0 para sucesso e não-zero para falha, você pode entregar tudo ao Git:

git bisect start
git bisect bad HEAD
git bisect good v1.4.0

# Hand over control to an automated script
git bisect run npm test -- --grep "checkout flow"

O Git vai fazer checkout de commits, rodar seu comando, e classificar resultados automaticamente. Quando terminar, você recebe a mesma saída “first bad commit” sem tocar no teclado. É aqui que git bisect passa de útil a indispensável.

Seu script não precisa ser uma test suite. Pode ser qualquer executável que retorne códigos de saída significativos. Aqui está um shell script mínimo que verifica uma mensagem de log específica:

#!/bin/bash
# reproduce-bug.sh
# Exit 0 if the bug is NOT present (good)
# Exit 1 if the bug IS present (bad)

if curl -s http://localhost:3000/api/health | grep -q "database_timeout"; then
    exit 1  # bug is present
fi
exit 0  # bug is not present

Rode:

chmod +x reproduce-bug.sh
git bisect run ./reproduce-bug.sh

O contrato de código de saída é estrito. Saída 0 significa “good,” saída 1 a 124 significa “bad,” e saída 125 significa “skip this commit, it is untestable.” Saída 125 é útil quando um commit não compila, ou o servidor não consegue iniciar por causa de uma mudança de configuração não relacionada. O Git vai pular esse commit e buscar ao redor dele.

Onde o bisect desmorona: os gotchas

git bisect assume que seu bug é monotônico. Uma vez que um commit o introduz, todo commit descendente também é bad. Se o bug pisca, aparecendo e desaparecendo entre commits, a busca binária desmorona. Você vai obter resultados sem sentido ou o Git vai reclamar que não consegue isolar um único commit.

Bugs não-determinísticos são os piores culpados. Uma race condition que falha 10% das vezes vai ocasionalmente marcar um commit bad como good, corrompendo a busca. Se sua reprodução é flaky, corrija a instabilidade primeiro. Ou rode o teste múltiplas vezes no seu script e só marque um commit como good se ele passar em todas as execuções. Isso é mais lento, mas mais confiável.

Artefatos de build são outra armadilha. Se você muda de um commit que altera a versão de uma ferramenta de build para um que não altera, seus node_modules stale ou binários compilados podem não corresponder ao código em checkout. Sempre limpe e rebuild dentro do seu script de reprodução se seu projeto tem um passo de build.

#!/bin/bash
# safer-reproduce.sh
rm -rf node_modules dist
npm ci
npm run build
npm test -- --grep "checkout flow"

Isso adiciona alguns segundos por iteração, mas elimina uma categoria inteira de falsos positivos onde o bug está na verdade em artefatos stale.

O que o bisect não faz

git bisect encontra o commit. Ele não te diz por que o commit é bad. Um refactor de 5.000 linhas que toca quarenta arquivos pode ser o primeiro commit bad, e você ainda tem que ler o diff para entender qual linha é a culpada. O bisect reduz seu escopo de “em algum lugar no último mês” para “em algum lugar neste diff.” O resto ainda é seu trabalho.

Ele também não ajuda com bugs que existem no codebase mas nunca foram pegos por testes. Se sua test suite já estava verde quando o bug foi introduzido, o bisect não vai te salvar. Você precisa de uma reprodução primeiro. O bisect é uma ferramenta de busca, não de detecção.

Quando usar bisect em vez de blame ou log

git blame é ótimo quando você já sabe qual arquivo contém o bug. git log --grep é ótimo quando você se lembra de algo sobre a mensagem de commit. O bisect é para quando você não sabe nenhum dos dois. Você só sabe que funcionava no ponto A e falha no ponto B, e o espaço entre eles é grande demais para raciocinar.

Se sua equipe roda CI em todo commit, você pode às vezes bissectar mais rápido lendo o histórico de CI. Mas CI só captura o que testa. Uma regressão de performance, um bug visual, ou um erro sutil de lógica que seus testes deixam passar não vai aparecer nos logs de CI. O bisect funciona para qualquer reproducer que você possa scriptar, independentemente do que seu CI valida.

Como tornar o bisect parte do seu workflow

Você não precisa de uma configuração especial. Os comandos são built-in no Git. Mas há dois hábitos que tornam o bisect mais suave quando você realmente precisa dele.

Primeiro, tag seus releases. O bisect precisa de um commit known-good, e tags são a maneira mais fácil de fornecer um. Se seu último release foi três semanas atrás e você sabe que estava limpo, git bisect good v1.4.0 é um ponto de âncora de uma linha.

Segundo, mantenha sua reprodução mínima. Uma reprodução que leva trinta segundos para rodar é tolerável para oito iterações. Uma reprodução que leva dez minutos é tortura. Antes de bissectar, gaste cinco minutos reduzindo a reprodução ao menor comando possível. Seu eu do futuro vai agradecer.

Quando terminar, rode git bisect reset imediatamente. Um detached HEAD com mudanças não committed é uma ótima maneira de perder trabalho. Eu aprendi isso da maneira difícil para que você não precise.