git bisect: come trovare il commit che ha introdotto un bug
git bisect trova il commit in cui è comparso un errore con una ricerca binaria nella storia. Eseguite git bisect start, segnate il commit difettoso con git bisect bad e uno sicuramente funzionante con git bisect good <hash>, poi verificate i commit proposti finché Git non indica il colpevole; alla fine git bisect reset.
git bisect cerca il commit che ha introdotto un errore con il metodo della ricerca binaria. Indicate un commit in cui il bug c'è e uno in cui non c'era: Git vi porta a metà dell'intervallo e chiede «qui va bene o no?». Ogni risposta scarta metà dei candidati, quindi tra 1000 commit il colpevole si trova in circa 10 verifiche.
È particolarmente prezioso quando l'errore non è evidente: un test fallisce, le prestazioni calano, il layout si rompe, e dai diff non si capisce nulla. Invece di leggere centinaia di modifiche, fate più volte la stessa semplice domanda.
Come funziona la ricerca binaria nella storia
Supponiamo che tra la versione funzionante v2.3 e il main attuale ci siano 200 commit. Git prende il commit a metà e lo verifica. Se lì è già rotto, l'errore è comparso nella prima metà; altrimenti nella seconda. Il numero di candidati si dimezza a ogni passo: 200, 100, 50, 25, 13, 7, 4, 2, 1. Otto verifiche invece di duecento.
Da qui il requisito principale: la proprietà «rotto» deve essere monotona. Se il bug è comparso, scomparso e ricomparso, bisect troverà uno dei passaggi, ma non necessariamente quello che vi interessa.
git bisect passo passo: modalità manuale
1. Individuate un commit buono e uno cattivo
Quello cattivo di solito è l'HEAD attuale. Quello buono è qualsiasi commit in cui tutto funzionava di sicuro: il tag della release precedente, un commit di una settimana fa. Per trovare un candidato aiutano git log --oneline, git tag o git log --since="2 weeks ago".
2. Avviate la sessione
git bisect start
git bisect bad # il commit attuale è rotto
git bisect good v2.3 # nella release v2.3 funzionava tutto
Bisecting: 99 revisions left to test after this (roughly 7 steps)
[4f5e6d7c...] Aggiorna le dipendenze del frontend
Lo stesso si può scrivere in una riga: git bisect start HEAD v2.3, prima il cattivo, poi il buono. Git sposta subito la working directory sul commit a metà (in stato di detached HEAD).
3. Verificate e segnate
Compilate il progetto, lanciate il test o riproducete il bug a mano. Poi comunicate il risultato:
git bisect good # qui funziona
Bisecting: 49 revisions left to test after this (roughly 6 steps)
git bisect bad # qui è rotto
Bisecting: 24 revisions left to test after this (roughly 5 steps)
4. Ottenete il risultato
Quando resta un solo candidato, Git lo mostra:
a1b2c3d4e5f6... is the first bad commit
commit a1b2c3d4e5f6...
Author: Mario Rossi <mario@example.com>
Date: Tue Sep 29 11:40:12 2026 +0200
Metti in cache il calcolo del carrello
src/cart.js | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
5. Chiudete la sessione
git bisect reset
Il comando vi riporta sul branch da cui eravate partiti. Non saltate questo passo: finché la sessione è aperta restate in detached HEAD ed è facile perdere nuovi commit. La variante git bisect reset HEAD vi lascia sul commit trovato, mentre git bisect reset <commit> su qualsiasi commit indicato.
Ricerca automatica: git bisect run
Se la verifica si può esprimere con un comando, lasciate tutto il lavoro a Git. git bisect run esegue uno script a ogni passo e assegna da solo l'esito in base al codice di uscita:
| Codice di uscita | Significato |
|---|---|
| 0 | good: il commit è buono |
| 1–127, tranne 125 | bad: il commit è cattivo |
| 125 | skip: il commit non si può verificare, saltarlo |
| oltre 127 (o <0) | interrompere bisect (ad esempio, lo script è terminato per un segnale) |
Esempi di avvio:
git bisect start HEAD v2.3
git bisect run npm test
git bisect run pytest tests/test_cart.py -x
git bisect run make check
Per verifiche complesse scrivete un piccolo script. È importante distinguere «non compila» da «il bug si è riprodotto», altrimenti bisect scambierà una build rotta per l'errore cercato:
#!/bin/sh
# check.sh
npm ci --silent || exit 125 # dipendenze non installate: saltare
npm run build || exit 125 # build fallita: saltare
node scripts/check-cart-total.js # 0 = ok, 1 = bug
chmod +x check.sh
git bisect run ./check.sh
Tenete lo script fuori dai file tracciati o in una cartella presente in tutti i commit verificati: bisect cambia la working directory e un file aggiunto di recente sparirà nei commit vecchi. La soluzione più affidabile è metterlo in /tmp o in qualsiasi cartella fuori dal repository.
Un test che prima non esisteva
Situazione frequente: avete scritto un test che riproduce il bug, ma nei commit vecchi quel file non c'è. La soluzione è sempre una copia fuori dal repository:
cp tests/test_regression.py /tmp/
git bisect run pytest /tmp/test_regression.py
git bisect skip: quando un commit non si può verificare
A volte il commit a metà non compila o fallisce per un altro motivo. Segnarlo good o bad non è corretto: falserebbe il risultato. Saltatelo:
git bisect skip
git bisect skip v2.4..v2.4.3 # saltare un intervallo
Git sceglierà un commit vicino. Se i commit saltati si trovano proprio al confine, alla fine vedrete un elenco di «possibili colpevoli» invece di uno solo: in quel caso guardate i loro diff a mano.
Termini personalizzati al posto di good e bad
Bisect non cerca solo bug. Ad esempio, volete trovare il commit dopo il quale la pagina ha iniziato a caricarsi più velocemente. Chiamare «cattivi» i commit veloci crea confusione, quindi esistono parole alternative:
git bisect start --term-old=slow --term-new=fast
git bisect fast HEAD
git bisect slow v2.3
I sinonimi integrati sono old e new: git bisect new, git bisect old. La regola è una sola: lo stato «vecchio» deve appartenere al commit più antico.
Il registro della sessione: log, replay, visualize
git bisect log: mostra tutte le vostre marcature. Salvatelo in un file se la sessione è lunga.git bisect replay file: ripete la sessione dal registro. Avete sbagliato al sesto passo? Salvate il log, cancellate le righe errate, eseguitegit bisect resetereplay.git bisect visualize(ogit bisect view): mostra i candidati rimasti; con--onelineli elenca tramite git log.
git bisect log > /tmp/bisect.log
# modifichiamo il log
git bisect reset
git bisect replay /tmp/bisect.log
Bisect e commit di merge
In una storia con molti merge, bisect può portarvi dentro un branch unito, in commit creati prima che il branch fosse sincronizzato con main e quindi costruiti su una base diversa. Se vi interessa prima capire quale merge ha portato il bug, usate:
git bisect start --first-parent
Così Git segue solo i primi genitori dei commit di merge, cioè la storia del branch principale. Trovato il merge colpevole, potete lanciare un secondo bisect dentro il branch corrispondente.
Errori comuni
- Avete confuso good e bad. Il risultato sarà privo di senso. Ricostruite la sessione con
git bisect logereplay, senza ricominciare da capo. - Working directory sporca. Le modifiche non committate impediscono il cambio di commit e influenzano la verifica. Prima di iniziare fate un commit o
git stash. - Artefatti di build non puliti. Una cache vecchia o
node_modulesdi un altro commit danno risultati falsi. Nello script fate una build pulita. - Test instabile (flaky). Se il test a volte fallisce da solo, bisect segnerà un commit a caso. Ripetete la verifica più volte o sistemate il test prima della ricerca.
- Avete dimenticato reset. Siete rimasti in detached HEAD e avete iniziato a lavorare. Il reflog aiuta a ritrovare i commit persi.
Esempio: ricerca di una regressione dall'inizio alla fine
Gli utenti segnalano che il totale del carrello è sbagliato quando c'è uno sconto. Nella release v2.3 era tutto corretto, in main no. Scriviamo una verifica minima e avviamo la ricerca:
cat > /tmp/check.sh <<'SH'
#!/bin/sh
npm ci --silent || exit 125
node -e "const c=require('./src/cart'); process.exit(c.total([{price:100}], 0.1) === 90 ? 0 : 1)"
SH
chmod +x /tmp/check.sh
git stash # mettere da parte le modifiche non committate
git bisect start HEAD v2.3
git bisect run /tmp/check.sh
...
a1b2c3d4e5f6... is the first bad commit
git bisect log > /tmp/cart-bisect.log
git bisect reset
git stash pop
Tutta la ricerca tra un centinaio di commit avviene senza il vostro intervento, mentre vi occupate d'altro. Il log salvato vi tornerà utile se nel ticket dovete mostrare ai colleghi come è stato trovato il colpevole.
Cosa fare con il commit trovato
Trovato il colpevole, guardatelo per intero: git show a1b2c3d. Un commit piccolo con un messaggio chiaro suggerisce subito la causa: è uno dei motivi per cui i team apprezzano i commit atomici, mentre bisect su una storia di enormi «fix stuff» è quasi inutile. Poi le strade sono due: correggere l'errore con un nuovo commit oppure, se la modifica è sbagliata nel suo insieme ed è già pubblicata, annullarla con git revert a1b2c3d.
In sintesi
git bisect trasforma la ricerca di una regressione dalla lettura di centinaia di diff in qualche risposta «funziona / non funziona» e, con git bisect run, in un solo comando. Servono solo due punti di riferimento, un commit buono e uno cattivo, e una verifica riproducibile. La lettura della storia, la navigazione tra i commit e la ricerca di errori sono trattate in pratica nel corso su Git.
Domande frequenti
Quanti passi richiede git bisect?
Circa log2 del numero di commit nell'intervallo: circa 7 passi per 100 commit e circa 10 per 1000. Git stesso mostra la stima roughly N steps.
Si può avviare bisect se non si conosce un commit buono?
Ne serve almeno uno. Prendete un commit sicuramente vecchio o il tag di una release e verificatelo a mano; i passi in più dovuti a un intervallo ampio non costano quasi nulla.
git bisect modifica la storia del repository?
No, cambia solo la working directory tra i commit e conserva le marcature in riferimenti di servizio refs/bisect, che vengono rimossi con git bisect reset.
Si possono indicare più commit buoni?
Sì, a git bisect good si possono passare più commit: questo restringe l'intervallo, soprattutto in una storia con molti branch.
Bisect serve solo a trovare bug?
No, permette di trovare il momento in cui è cambiata qualsiasi proprietà: quando è comparso un file, quando la build è diventata più veloce o quando è cambiato un comportamento. Per questo sono comodi i termini old/new o quelli personalizzati con --term-old e --term-new.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Git da zero a sviluppatore sicuro».
Vai al corso