Votre CI est rouge sur main, mais elle était verte hier. Quelque part entre les deux, un bug s’est glissé. Vous pourriez faire défiler 200 commits, lire les diffs et deviner. Vous pourriez demander sur Slack et espérer que quelqu’un se souvienne avoir touché le code pertinent. Ou vous pourriez laisser Git faire le travail pour vous.
git bisect est un outil de recherche binaire pour votre historique de commits. Vous marquez un commit comme mauvais et un comme bon. Git checkoute le point médian. Vous le testez, le marquez bon ou mauvais, et Git répète. En 8 étapes, il peut isoler un seul commit parmi 256. En 12 étapes, il peut trouver l’aiguille dans 4 096. C’est la chose la plus proche qu’ait Git d’un débogueur pour l’histoire elle-même.
Pourquoi la bisection manuelle est une perte de temps
Les développeurs bisectent déjà manuellement sans s’en rendre compte. Vous checkout un commit plus ancien, exécutez les tests, et pensez « toujours cassé, il faut remonter plus loin. » Puis vous checkout un encore plus ancien, et les tests passent. Maintenant vous savez que le bug est quelque part entre ces deux points. Alors vous en choisissez un au milieu et continuez à rétrécir.
C’est de la recherche binaire. git bisect automatise le suivi pour que vous ne perdiez pas la trace des commits que vous avez déjà testés. Plus important, il empêche l’heuristique paresseuse du « c’était probablement dans ce gros refactor de mardi » qui vous envoie dans le mauvais terrier pendant deux heures.
La vraie valeur n’est pas la vitesse, bien que ce soit plus rapide. La vraie valeur est la correction. Quand vous êtes frustré et chassez un bug à 18 h, vous ferez des erreurs. Vous oublierez de rebuild après le checkout. Vous testerez le mauvais commit deux fois. Vous malinterpréterez un résultat de test et marquerez un mauvais commit comme bon, détruisant votre espace de recherche. git bisect enforce la discipline que vous n’avez pas quand vous êtes énervé.
Comment fonctionne réellement la recherche binaire
Git ne recherche pas chronologiquement. Il recherche topologiquement, parcourant le graphe de commits pour trouver le point médian entre vos commits connus comme bons et connus comme mauvais. C’est important quand votre historique a des merges, parce que le point médian chronologique pourrait ne même pas être atteignable depuis les deux extrémités.
Voici ce qui se passe sous le capot. Vous démarrez le bisect et fournissez les bornes :
git bisect start
git bisect bad HEAD # current commit is broken
git bisect good v2.1.0 # this release was fine
Git calcule le nombre de commits entre ces deux points. Il checkout celui exactement au milieu et attend que vous le testiez. Vous exécutez votre reproduction, voyez si le bug est présent, et dites à Git :
git bisect bad # this commit has the bug
git bisect good # this commit is clean
Git écarte la moitié de l’espace de recherche et répète. Quand il ne reste qu’un seul commit, il s’arrête et vous montre le premier mauvais commit. La sortie inclut le hash du commit, l’auteur, la date, et le message. Il n’y a pas d’ambiguïté. Ce commit a introduit le bug, point final.
Un walkthrough concret avec de vraies commandes
Supposons que vos tests d’intégration ont commencé à échouer ce matin. Vous savez qu’ils passaient dans la dernière release taguée, v1.4.0. Voici la session complète :
# 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"
À la fin, Git vous laisse sur le mauvais commit. Vous pouvez l’inspecter avec git show, créer une correction, puis nettoyer :
git bisect reset
Ça vous ramène à la branche sur laquelle vous étiez avant de commencer. Si vous oubliez de reset, vous resterez sur un HEAD détaché et vous demanderez pourquoi votre prochain commit n’est pas sur votre branche. Je l’ai fait. C’est embarrassant.
Automatiser tout le processus avec un script
La version manuelle exige encore que vous exécutiez des tests et tapiez good ou bad à répétition. Si votre reproduction est une seule commande qui sort avec 0 en cas de succès et non-zéro en cas d’échec, vous pouvez confier le tout à 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"
Git checkoutera des commits, exécutera votre commande, et classera les résultats automatiquement. Quand il termine, vous obtenez la même sortie « first bad commit » sans toucher votre clavier. C’est là que git bisect passe d’utile à indispensable.
Votre script n’a pas besoin d’être une suite de tests. Ça peut être n’importe quel exécutable qui retourne des codes de sortie significatifs. Voici un script shell minimal qui vérifie un message de log spécifique :
#!/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
Exécutez-le :
chmod +x reproduce-bug.sh
git bisect run ./reproduce-bug.sh
Le contract de code de sortie est strict. La sortie 0 signifie « good », la sortie 1 à 124 signifie « bad », et la sortie 125 signifie « skip this commit, it is untestable. » La sortie 125 est utile quand un commit ne compile pas, ou que le serveur ne peut pas démarrer à cause d’un changement de configuration sans rapport. Git sautera ce commit et cherchera autour.
Là où bisect échoue : les pièges
git bisect suppose que votre bug est monotone. Une fois qu’un commit l’introduit, chaque commit descendant est aussi mauvais. Si le bug clignote, apparaissant et disparaissant à travers les commits, la recherche binaire s’effondre. Vous obtiendrez des résultats absurdes ou Git se plaindra qu’il ne peut pas isoler un seul commit.
Les bugs non déterministes sont les pires coupables. Une race condition qui échoue 10 % du temps marquera occasionnellement un mauvais commit comme bon, corrompant la recherche. Si votre reproduction est flaky, corrigez d’abord le flaky. Ou exécutez le test plusieurs fois dans votre script et ne marquez un commit comme bon que s’il passe à chaque exécution. C’est plus lent mais plus fiable.
Les artifacts de build sont un autre piège. Si vous passez d’un commit qui change une version d’outil de build à un qui ne le fait pas, vos node_modules périmés ou binaires compilés pourraient ne pas correspondre au code check-outé. Nettoyez et rebuild toujours dans votre script de reproduction si votre projet a une étape de build.
#!/bin/bash
# safer-reproduce.sh
rm -rf node_modules dist
npm ci
npm run build
npm test -- --grep "checkout flow"
Ça ajoute quelques secondes par itération, mais ça élimine toute une catégorie de faux positifs où le bug est en réalité dans des artifacts périmés.
Ce que bisect ne fait pas
git bisect trouve le commit. Il ne vous dit pas pourquoi le commit est mauvais. Un refactor de 5 000 lignes qui touche quarante fichiers peut être le premier mauvais commit, et vous devez toujours lire le diff pour comprendre quelle ligne est la coupable. Bisect rétrécit votre scope de « quelque part dans le dernier mois » à « quelque part dans ce diff. » Le reste est toujours votre travail.
Ça n’aide pas non plus avec les bugs qui existent dans le codebase mais qui n’ont jamais été attrapés par les tests. Si votre suite de tests était déjà verte quand le bug a été introduit, bisect ne vous sauvera pas. Vous avez besoin d’une reproduction d’abord. Bisect est un outil de recherche, pas un outil de détection.
Quand utiliser bisect plutôt que blame ou log
git blame est génial quand vous savez déjà quel fichier contient le bug. git log --grep est génial quand vous vous souvenez de quelque chose dans le message de commit. Bisect est pour quand vous ne savez ni l’un ni l’autre. Vous savez juste que ça fonctionnait au point A et échoue au point B, et l’espace entre eux est trop grand pour raisonner.
Si votre équipe exécute la CI sur chaque commit, vous pouvez parfois bisecter plus vite en lisant l’historique de la CI. Mais la CI n’attrape que ce qu’elle teste. Une régression de performance, un bug visuel, ou une erreur de logique subtile que vos tests manquent n’apparaîtra pas dans les logs de la CI. Bisect fonctionne pour n’importe quel reproducer que vous pouvez scripter, indépendamment de ce que votre CI valide.
Comment faire de bisect une partie de votre workflow
Vous n’avez pas besoin d’une configuration spéciale. Les commandes sont intégrées dans Git. Mais il y a deux habitudes qui rendent bisect plus fluide quand vous en avez réellement besoin.
Premièrement, taguez vos releases. Bisect a besoin d’un commit connu comme bon, et les tags sont le moyen le plus simple d’en fournir un. Si votre dernière release date de trois semaines et que vous savez qu’elle était propre, git bisect good v1.4.0 est un point d’ancrage en une ligne.
Deuxièmement, gardez votre reproduction minimale. Une reproduction qui prend trente secondes à s’exécuter est tolérable pour huit itérations. Une reproduction qui prend dix minutes est une torture. Avant de bisecter, passez cinq minutes à rétrécir la reproduction à la commande la plus petite possible. Votre futur vous vous en remerciera.
Quand vous avez terminé, exécutez git bisect reset immédiatement. Un HEAD détaché avec des changements non commités est un excellent moyen de perdre du travail. Je l’ai appris à mes dépens pour que vous n’ayez pas à le faire.