La test suite è verde da tre settimane. Oggi npm test fallisce su un’asserzione che non ha niente a che fare con quello che hai appena modificato, e git log v1.4.0..HEAD mostra 340 commit di quattro persone dall’ultima release, di cui qualcuno era sicuro che funzionasse. Leggere ogni diff ti costa una giornata. Fare checkout di commit a caso è più veloce, ma resta comunque non un piano.

git bisect trasforma tutto questo in una ricerca binaria. Gli dai un commit che sai essere buono e uno che sai essere rotto; lui fa il checkout del commit a metà tra i due e aspetta che tu lo testi e riporti il risultato. Ogni risposta dimezza l’intervallo. 340 commit si risolvono in al massimo 9 test, perché 2^9 = 512.

Come git bisect dimezza la ricerca a ogni passo

Immagina i 340 commit in fila, dal più vecchio al più nuovo, il tag buono noto a un’estremità e l’HEAD rotto all’altra. git bisect non percorre quella fila. Salta al centro, aspetta un verdetto, poi butta via qualunque metà quel verdetto escluda:

1
2
3
4
good                                          bad
 |------------------------|------------------|
                    ^
              testa questo

Diciamo che il commit centrale passa. Il bug è nella metà più recente, quindi la metà più vecchia sparisce per sempre e in questa sessione non rivedrai più quei commit. Ripeti: centro di quello che resta, testa, scarta metà. Dopo circa log2(n) round l’intervallo si riduce a un solo commit — quello in cui il comportamento è passato da corretto a rotto. Non è sempre il commit che ha “causato” il bug in senso più profondo. È il commit in cui il comportamento è cambiato, che quasi sempre è esattamente ciò che ti serve.

Come avviare una sessione di git bisect

Esegui questo comando da una working tree pulita. Bisect fa checkout di commit senza chiedertelo, e si rifiuta di partire se ci sono modifiche non salvate.

1
2
3
git bisect start
git bisect bad HEAD
git bisect good v1.4.0

Git fa checkout del punto medio e ti dice quanto resta:

1
2
Bisecting: 169 revisions left to test after this (roughly 8 steps)
[a1b2c3d4e5f6...] Refactor payment retry logic

Esegui il tuo test su quel commit, poi riporta cosa hai trovato:

1
2
npm test
git bisect bad     # il test è fallito qui

oppure

1
git bisect good    # il test è passato qui

Git fa checkout automaticamente del nuovo punto medio dopo ogni risposta. Continua a testare e riportare finché non c’è più niente da restringere:

1
2
3
4
5
6
a1b2c3d4e5f6789... is the first bad commit
commit a1b2c3d4e5f6789...
Author: Priya Nair <[email protected]>
Date:   Tue Sep 2 11:14:02 2026 +0000

    Refactor payment retry logic

Questa è la tua risposta. Leggi quel diff, non gli otto commit intorno. git bisect reset ti riporta sul branch e sul commit da cui eri partito, e i checkout fatti da bisect lungo il percorso non lasciano tracce.

1
git bisect reset

Automatizzare il ciclo con git bisect run

Rispondere good o bad a mano va bene per un bug che noti in pochi secondi. Per qualcosa che richiede un’esecuzione di test vera, affida l’intero ciclo a uno script:

1
2
3
4
git bisect start
git bisect bad HEAD
git bisect good v1.4.0
git bisect run ./bisect-test.sh

bisect-test.sh deve solo uscire con il codice giusto: 0 significa buono, qualsiasi codice da 1 a 127 tranne 125 significa rotto, e 125 significa “non riesco a testare questo commit, saltalo”.

1
2
#!/bin/sh
npm test

La maggior parte dei test runner esce già con 0 quando passa e con un codice diverso da zero quando fallisce, quindi spesso lo script si riduce a chiamare il runner. git bisect run fa checkout di ogni punto medio, esegue lo script, legge il codice di uscita e riporta lui stesso il risultato, stampando alla fine lo stesso riepilogo “first bad commit”.

Lo script può fare più che chiamare un test runner. Se la regressione si vede solo in un artefatto costruito, uno script che esegue docker build e poi mette alla prova l’immagine è una cosa normale da passare a git bisect run — i passaggi di build di Costruire e Pubblicare un’Immagine Docker con GitHub Actions funzionano in uno script tanto quanto in un workflow. Mettilo in conto: ogni round ora costa una build completa dell’immagine.

C’è un vincolo sullo script stesso: tienilo fuori dall’intervallo su cui fai bisect, oppure assicurati che non sia cambiato in quell’intervallo. Se lo script fa parte della porzione di storia che stai analizzando, un commit vecchio fa checkout di una vecchia versione dello script insieme al vecchio codice, e non stai più eseguendo lo stesso test a ogni passo.

Saltare i commit che non puoi testare con git bisect skip

Alcuni commit non si possono testare da soli: un commit a metà refactoring che non compila, una migrazione dello schema che richiede uno stato del database che non hai. Forzare un verdetto good/bad su uno di questi avvelena il risultato. Escludilo invece:

1
git bisect skip

Bisect fa checkout di un commit vicino e continua saltando il buco. In bisect run, il codice di uscita 125 fa la stessa cosa automaticamente, quindi uno script capace di capire “questo commit non compila nemmeno” può saltarlo invece di riportare un falso bad. Se salti troppi commit adiacenti, bisect esaurisce il terreno testabile: la risposta finale diventa “il bug arriva da uno di questi N commit” invece di un commit singolo.

Quando git bisect non è lo strumento giusto

Bisect presuppone che il tuo test sia deterministico: stesso commit, stesso risultato, ogni volta. Un test flaky rompe silenziosamente questa premessa. Bisect non riesce a distinguere “questo commit è rotto” da “questo test ha fatto i capricci”, e una risposta sbagliata all’inizio manda ogni round successivo nella metà sbagliata. Sistema o escludi l’asserzione flaky prima di fare bisect, non dopo.

Presuppone anche che un commit abbia prodotto un unico cambiamento di comportamento pulito. Un bug insinuatosi lungo diversi commit, o uno che dipende da uno stato accumulato piuttosto che da un singolo diff, non converge su niente di utile. Una storia pesantemente rebasata è una versione più lieve dello stesso problema (vedi Git Rebase vs Merge: differenze e quando usarli): il primo commit rotto trovato da bisect potrebbe essere un commit squashato che raggruppa cinque modifiche logiche. Corretto, ma troppo grezzo da rivedere isolatamente.

Nessuno dei due casi richiede uno strumento diverso, solo un punto di partenza diverso. Prima rendi il test affidabile, poi scegli un commit good abbastanza indietro da essere sicuro che il bug non ci fosse già.

La prossima volta che salta fuori una regressione senza causa evidente, segna i due estremi e rispondi good o bad un po’ di volte — oppure scrivi lo script di tre righe e lascia che sia git bisect run a rispondere.

Articoli correlati