12k
All articles

Usando o git bisect para encontrar um commit problemático

Use git bisect para localizar o commit que causou uma regressão. Escolha referências confiáveis, automatize testes, lide com resultados instáveis e confirme o commit responsável.

OpenReplay Team
OpenReplay Team
Usando o git bisect para encontrar um commit problemático

O git bisect encontra o commit que causou uma regressão por meio de uma busca binária. Você marca um commit sabidamente bom e um commit sabidamente ruim, e cada teste divide o intervalo restante pela metade. Assim, 300 commits exigem cerca de oito ou nove verificações.

A funcionalidade funcionava no mês passado. Desde então, 300 commits entraram na main, ninguém se lembra de ter mexido no código do carrinho, e ler cada diff levaria a tarde inteira.

5 comandos Git além de commit e push apresenta start, good, bad, run, skip e reset. Este artigo vai além: mostra como escolher um commit bom em que você possa confiar, como escrever um script para o git bisect run cujos códigos de saída não enganem o Git e o que fazer quando um teste instável (flaky) ou uma marcação errada leva a busca para o caminho errado.

Principais conclusões

  • Cada vez que o intervalo do bisect dobra, adiciona-se apenas uma verificação. Por isso, o custo real de um commit bom muito antigo são os commits antigos que já não compilam e precisam ser pulados.
  • Com o git bisect run, o código de saída 0 marca o commit como bom, 125 o pula, qualquer outro código de 1 a 127 o marca como ruim e 128 ou mais aborta o bisect.
  • Para uma falha intermitente, marque o commit como ruim se qualquer uma de várias execuções falhar, e como bom somente se todas as execuções passarem.
  • Uma marcação errada não significa recomeçar do zero: salve o git bisect log, apague a linha errada e tudo o que vem depois dela e, em seguida, execute git bisect reset e git bisect replay.
  • Confirme o resultado testando o commit apontado e o seu commit pai. O pai deve passar e o commit apontado deve falhar.

O ciclo manual do git bisect

O ciclo manual exige três comandos para ser configurado: git bisect start, depois git bisect bad em um commit com defeito e, por fim, git bisect good <ref> em um commit que funciona. A partir daí, você testa cada commit para o qual o Git faz checkout e o marca, até que o Git aponte o culpado. O capítulo do Pro Git sobre depuração com Git aborda o mesmo fluxo. Se você ainda está começando com Git, 10 comandos Git que todo desenvolvedor deveria conhecer cobre o básico do dia a dia.

git bisect start
git bisect bad              # HEAD has the bug
git bisect good v4.12.0     # last release known to work
Bisecting: 149 revisions left to test after this (roughly 7 steps)
[9e41c07b2d85a3f16c0e7b94d2a58f3e1c6b0d27] Refactor cart line-item formatter

O Git fez checkout do ponto médio. Execute seu teste e informe o resultado:

git bisect bad
Bisecting: 74 revisions left to test after this (roughly 6 steps)
[2b7f0d9a4c16e83b5f2a07d9c4e1b68a3f05c9d1] Update currency util types

Continue testando e marcando. Quando restar apenas um candidato, o Git exibe o resultado (os SHAs e nomes abaixo são fictícios):

3c9d2e7a51f04b8e96d1a2c7f05e4b3d8a6c1f92 is the first bad commit
commit 3c9d2e7a51f04b8e96d1a2c7f05e4b3d8a6c1f92
Author: Example Dev <dev@example.com>
Date:   <date>

    Round line totals before applying discount

 src/cart/total.ts | 4 ++--

Como escolher o commit bom?

O melhor commit bom para o git bisect é a tag de release mais recente que você sabe que foi publicada sem o bug, testada antes de ser marcada. Uma ref marcada como boa que, na verdade, é ruim ainda produz uma resposta convincente — geralmente um commit logo após a sua ref boa —, e essa resposta está errada.

Teste a tag com a mesma verificação que o bisect vai usar (o Vitest aqui é apenas um exemplo de test runner):

git switch --detach v4.12.0
npm ci && npx vitest run src/cart/total.test.ts   # must pass
git switch -

Não tente reduzir o intervalo à custa da certeza. Cada vez que o intervalo dobra, adiciona-se apenas uma verificação: cerca de 8 a 9 verificações para 300 commits, 9 a 10 para 600 e 11 a 12 para 2.400. Voltar muito no histórico tem outro custo. Commits antigos podem exigir uma versão mais antiga do Node, uma dependência que desde então foi despublicada ou um formato de lockfile diferente. Cada commit que não compila precisa ser pulado, e um trecho longo de commits pulados pode esconder o culpado.

Os replays de sessão mostram quando uma regressão chegou pela primeira vez a usuários reais, o que restringe o commit bom à release implantada logo antes disso.

Como o git bisect run automatiza a busca?

O git bisect run <script> executa seu script em cada commit candidato e interpreta o código de saída como o veredito. A documentação do git-bisect define o mapeamento:

Código de saídaSignificado
0Bom
1–127, exceto 125Ruim
125Pular (não é possível testar)
128 ou maisAbortar o bisect

Mantenha o script fora do repositório. Assim, o checkout de commits mais antigos não pode alterá-lo, e comandos de limpeza como git clean -fdx não podem apagá-lo:

cat > ../bisect-test.sh <<'EOF'
#!/usr/bin/env bash
npm ci --silent        || exit 125   # can't install: skip
npm run build --silent || exit 125   # can't build: skip
npx vitest run src/cart/total.test.ts || exit 1
EOF
chmod +x ../bisect-test.sh
git bisect run ../bisect-test.sh

Uma falha na instalação ou no build sai com 125, de modo que um problema não relacionado é pulado em vez de ser contado como ruim. O || exit 1 final mapeia qualquer falha de teste para 1, para que um test runner que eventualmente retorne 125 ou 128+ não possa pular um commit ou abortar a execução por acidente.

Os códigos de saída 126 (não executável) e 127 (comando não encontrado) normalmente contam como ruins. Portanto, um caminho de script errado ou a falta da permissão de execução poderia marcar todos os commits como ruins. As notas de release do Git 2.36 descrevem a proteção: o Git tenta identificar um script que não pode ser executado e para antecipadamente, em vez de apontar um culpado. Se o git bisect run parar antes da hora, verifique o caminho e as permissões do script antes de olhar o código.

Problemas comuns: skip, reset e árvore de trabalho com alterações

A maioria das interrupções vem de três situações: um commit que você não consegue testar, uma sessão que você precisa encerrar ou alterações locais que atrapalham.

git bisect skip

O git bisect skip deixa de lado o commit atual e o Git passa para um commit próximo. Se o culpado acabar dentro de uma sequência de commits pulados, o Git lista os candidatos em vez de apontar apenas um.

git bisect reset              # back to the branch you started on
git bisect reset 3c9d2e7      # end the session on a specific commit

Faça commit ou stash das alterações locais antes do git bisect start. Você pode limitar a busca a determinados caminhos com git bisect start -- src/cart/. O git bisect visualize abre o gitk com os suspeitos restantes ou, quando o Git não encontra uma sessão de desktop gráfica, recorre ao git log.

Como lidar com testes instáveis e marcações erradas?

Para fazer bisect de uma falha intermitente, execute o teste várias vezes em cada commit. Marque-o como ruim se qualquer execução falhar, e como bom somente se todas as execuções passarem. O motivo é que uma única marcação errada leva a busca para a metade errada, e mesmo assim o Git reporta um primeiro commit ruim. Um commit realmente ruim pode passar por sorte, mas um commit bom nunca deveria falhar nesse teste.

#!/usr/bin/env bash
npm ci --silent        || exit 125
npm run build --silent || exit 125
for i in $(seq 1 10); do
  npx vitest run src/cart/total.test.ts || exit 1   # any failure = bad
done
exit 0                                              # all passed = good

Veja a conta para um caso hipotético: se um commit ruim falha 20% das vezes, a probabilidade de ocorrerem dez execuções bem-sucedidas por acaso é de 0,8¹⁰ ≈ 0,11. Mais execuções reduzem esse risco. Pular commits ambíguos não resolve nada, porque o resultado não confiável continua lá e a resposta final apenas fica mais ampla. Essa regra pressupõe que a instabilidade é nova. Se o teste já era instável antes da regressão, estabilize-o ou escreva antes uma verificação mais específica; caso contrário, ele vai marcar commits bons como ruins.

Se você já fez uma marcação errada, é possível corrigi-la sem recomeçar:

git bisect log > ../bisect.log
# edit ../bisect.log
git bisect reset
git bisect replay ../bisect.log

O log salvo também inclui linhas de comentário. Aqui são mostradas apenas as linhas de comando. Antes da edição:

git bisect start
git bisect bad 8d1f...
git bisect good 5a0c...
git bisect good 9e41...    <- wrong: this commit was flaky
git bisect bad 2b7f...

Na edição, apague a linha errada e tudo o que vem depois dela:

git bisect start
git bisect bad 8d1f...
git bisect good 5a0c...

O git bisect replay restaura a sessão até a última marcação correta, e você continua a partir daí.

Como confirmar o resultado?

Trate o commit apontado pelo git bisect como uma pista até verificá-lo. Leia o diff com git show 3c9d2e7 e, em seguida, teste o commit pai e o próprio commit:

git switch --detach 3c9d2e7^   # parent: test should pass
git switch --detach 3c9d2e7    # culprit: test should fail

Se o pai passar e o commit falhar, você encontrou a regressão. A partir daqui, use o git blame para entender por que as linhas ao redor mudaram e o reescrita do histórico do Git para corrigir commits que ainda não foram enviados (push). Na próxima regressão, comece a partir de uma tag de release testada e de um script que execute o teste várias vezes em cada commit.

Perguntas frequentes

O git bisect consegue encontrar o commit que corrigiu um bug, em vez do que o introduziu?

Sim. Use os termos old e new em vez de good e bad. Execute git bisect start, marque um commit já corrigido com git bisect new e um commit mais antigo com defeito com git bisect old, e o Git reportará o primeiro commit new, que é a correção. Para rótulos mais claros, execute git bisect start --term-old broken --term-new fixed e, em seguida, marque os commits com git bisect fixed e git bisect broken.

Como o git bisect lida com commits de merge vindos de branches de funcionalidade?

Por padrão, o git bisect também testa commits de dentro das branches mescladas, incluindo commits de trabalho em andamento que podem não compilar. Executar git bisect start --first-parent, opção adicionada no Git 2.29, mantém a busca na linha principal do histórico. Na main, ele para no merge que trouxe o bug e nunca testa os commits próprios da branch. Um segundo bisect sobre essa branch encontra o commit exato.

O que acontece se o commit bom não for ancestral do commit ruim?

O Git faz checkout de uma merge base dos dois commits e pede que você a teste primeiro. Se a merge base for boa, a bissecção continua normalmente sobre os commits entre ela e o commit ruim. Se a merge base já for ruim, o Git para em vez de continuar a busca, porque o commit bom está em uma linha separada do histórico, na qual o bug não existe ou foi corrigido.

Qual é a diferença entre git bisect e git blame?

O git blame informa, linha por linha, qual commit alterou um arquivo mais recentemente, portanto só responde quem editou por último uma determinada linha. O git bisect testa o comportamento ao longo dos commits, então encontra uma regressão mesmo quando a causa é uma atualização de dependência, uma mudança de configuração ou um arquivo diferente daquele que apresenta o sintoma. Use o bisect para encontrar o commit e, depois, o blame para rastrear as linhas que ele alterou.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.