Utiliser git bisect pour identifier un commit fautif
Utilisez git bisect pour repérer le commit à l’origine d’une régression. Choisissez des références fiables, automatisez les tests, gérez les résultats instables et vérifiez le coupable.
git bisect identifie le commit à l’origine d’une régression par recherche dichotomique. Vous marquez un commit dont vous savez qu’il fonctionne et un commit dont vous savez qu’il est défaillant. Chaque test divise ensuite par deux la plage restante : 300 commits nécessitent environ huit ou neuf vérifications.
La fonctionnalité marchait le mois dernier. Depuis, 300 commits ont été intégrés à main, personne ne se souvient d’avoir touché au code du panier, et relire chaque diff prendrait tout l’après-midi.
5 Git Commands Beyond Commit and Push présente start, good, bad, run, skip et reset. Cet article va plus loin. Il explique comment choisir un commit « good » fiable et comment écrire un script git bisect run dont les codes de sortie ne peuvent pas induire Git en erreur. Il indique aussi quoi faire lorsqu’un test instable (flaky) ou un mauvais marquage engage la recherche sur une fausse piste.
Points clés
- Chaque doublement de la plage de bisection n’ajoute qu’une vérification. Le vrai coût d’un commit « good » très ancien vient des vieux commits qui ne compilent plus et doivent être ignorés.
- Avec
git bisect run, le code de sortie 0 marque un commit comme bon et 125 l’ignore. Tout autre code entre 1 et 127 le marque comme mauvais, et un code de 128 ou plus interrompt la bisection. - Pour une défaillance intermittente, marquez un commit comme mauvais si l’une des exécutions échoue. Ne le marquez comme bon que si toutes réussissent.
- Un mauvais marquage n’oblige pas à tout recommencer. Sauvegardez
git bisect loget supprimez la ligne erronée ainsi que tout ce qui suit. Exécutez ensuitegit bisect resetpuisgit bisect replay. - Confirmez le résultat en testant le commit désigné et son parent. Le parent doit réussir le test et le commit désigné doit l’échouer.
La boucle manuelle de git bisect
La boucle manuelle se met en place en trois commandes. Lancez d’abord git bisect start, puis git bisect bad sur un commit défaillant, puis git bisect good <ref> sur un commit fonctionnel. Ensuite, vous testez chaque commit que Git extrait et vous le marquez, jusqu’à ce que Git désigne le coupable. Le chapitre de Pro Git consacré au débogage avec Git décrit le même processus. Si vous débutez avec Git, 10 Git commands every developer should know couvre les bases du quotidien.
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
Git a extrait le commit situé au milieu de la plage. Lancez votre test et indiquez le résultat :
git bisect bad
Bisecting: 74 revisions left to test after this (roughly 6 steps)
[2b7f0d9a4c16e83b5f2a07d9c4e1b68a3f05c9d1] Update currency util types
Continuez à tester et à marquer. Lorsqu’il ne reste qu’un candidat, Git affiche le résultat (les SHA et les noms ci-dessous sont fictifs) :
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 ++--
Comment choisir le commit « good » ?
Le meilleur commit « good » pour git bisect est le tag de version le plus récent dont vous savez qu’il a été livré sans le bogue. Testez-le avant de le marquer. Une référence « good » qui est en réalité défaillante produit quand même une réponse assurée, généralement un commit situé juste après cette référence. Et cette réponse est fausse.
Testez le tag avec la même vérification que celle utilisée par la bisection (Vitest n’est ici qu’un exemple de lanceur de tests) :
git switch --detach v4.12.0
npm ci && npx vitest run src/cart/total.test.ts # must pass
git switch -
Ne cherchez pas à réduire la plage au détriment de la certitude. Chaque doublement n’ajoute qu’une vérification : environ 8 à 9 vérifications pour 300 commits, 9 à 10 pour 600 et 11 à 12 pour 2 400. Remonter loin a un autre coût. Les anciens commits peuvent exiger une version plus ancienne de Node, une dépendance qui a depuis été dépubliée ou un autre format de fichier de verrouillage (lockfile). Chaque commit qui ne compile pas doit être ignoré, et une longue série de commits ignorés peut masquer le coupable.
Les enregistrements de session (session replay) montrent à quel moment une régression a touché de vrais utilisateurs pour la première fois. Le commit « good » se resserre alors sur la version déployée juste avant.
Comment git bisect run automatise-t-il la recherche ?
git bisect run <script> exécute votre script sur chaque commit candidat et interprète son code de sortie comme un verdict. La documentation de git-bisect définit la correspondance :
| Code de sortie | Signification |
|---|---|
| 0 | Bon (good) |
| 1–127, sauf 125 | Mauvais (bad) |
| 125 | Ignorer (impossible à tester) |
| 128 et plus | Interrompre la bisection |
Placez le script en dehors du dépôt. Ainsi, l’extraction de commits plus anciens ne peut pas le modifier, et les commandes de nettoyage comme git clean -fdx ne peuvent pas le supprimer :
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
Un échec d’installation ou de build renvoie 125 : une panne sans rapport avec le bogue est donc ignorée au lieu d’être comptée comme mauvaise. Le || exit 1 final ramène tout échec de test au code 1. Un lanceur de tests qui renverrait par hasard 125 ou 128+ ne peut donc pas ignorer un commit ni interrompre l’exécution par accident.
Les codes de sortie 126 (non exécutable) et 127 (commande introuvable) comptent normalement comme mauvais. Un chemin de script erroné ou un bit d’exécution manquant pourrait donc marquer tous les commits comme mauvais. Les notes de version de Git 2.36 décrivent le garde-fou mis en place : Git tente de détecter un script impossible à exécuter et s’arrête prématurément au lieu de désigner un coupable. Si git bisect run s’arrête prématurément, vérifiez le chemin et les permissions du script avant d’examiner le code.
Écueils courants : skip, reset et arbre de travail modifié
La plupart des interruptions ont trois causes : un commit impossible à tester, une session à terminer ou des modifications locales qui gênent.
git bisect skip
git bisect skip met de côté le commit courant et Git passe à un commit voisin. Si le coupable se trouve au sein d’une série de commits ignorés, Git liste les candidats au lieu d’en désigner un seul.
git bisect reset # back to the branch you started on
git bisect reset 3c9d2e7 # end the session on a specific commit
Commitez ou remisez avec stash vos modifications locales avant git bisect start. Vous pouvez limiter la recherche à certains chemins avec git bisect start -- src/cart/. git bisect visualize ouvre gitk sur les suspects restants, ou se rabat sur git log lorsque Git ne détecte aucune session graphique.
Comment gérer les tests instables et les mauvais marquages ?
Pour bissecter une défaillance intermittente, exécutez le test plusieurs fois sur chaque commit. Marquez le commit comme mauvais si une seule exécution échoue, et comme bon uniquement si toutes réussissent. En effet, un seul mauvais marquage envoie la recherche dans la mauvaise moitié, et Git signale malgré tout un premier commit fautif. Un commit réellement défaillant peut réussir le test par chance, mais un bon commit ne devrait jamais l’échouer.
#!/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
Voici le calcul pour un cas hypothétique. Si un commit défaillant échoue 20 % du temps, la probabilité d’obtenir dix exécutions réussies par hasard est de 0,8¹⁰ ≈ 0,11. Davantage d’exécutions réduisent ce risque. Ignorer les commits ambigus ne règle rien : le résultat peu fiable demeure, et la réponse finale devient simplement plus large. Cette règle suppose que l’instabilité est nouvelle. Si le test était déjà instable avant la régression, stabilisez-le ou écrivez d’abord une vérification plus ciblée, sinon il marquera de bons commits comme mauvais.
Si vous avez déjà fait un mauvais marquage, vous pouvez le corriger sans tout recommencer :
git bisect log > ../bisect.log
# edit ../bisect.log
git bisect reset
git bisect replay ../bisect.log
Le journal sauvegardé contient aussi des lignes de commentaire. Seules les lignes de commande sont affichées ici. Avant modification :
git bisect start
git bisect bad 8d1f...
git bisect good 5a0c...
git bisect good 9e41... <- wrong: this commit was flaky
git bisect bad 2b7f...
Après modification, la ligne erronée et tout ce qui la suit ont été supprimés :
git bisect start
git bisect bad 8d1f...
git bisect good 5a0c...
git bisect replay restaure la session jusqu’au dernier marquage correct, et vous reprenez à partir de là.
Comment confirmer le résultat ?
Considérez le commit désigné par git bisect comme une piste tant que vous ne l’avez pas vérifié. Lisez le diff avec git show 3c9d2e7, puis testez le parent du commit et le commit lui-même :
git switch --detach 3c9d2e7^ # parent: test should pass
git switch --detach 3c9d2e7 # culprit: test should fail
Si le parent réussit le test et que le commit l’échoue, vous avez trouvé la régression. À partir de là, utilisez git blame pour comprendre pourquoi les lignes voisines ont changé. Pour corriger des commits qui n’ont pas encore été poussés, consultez l’article sur la réécriture de l’historique git. Lors de la prochaine régression, partez d’un tag de version testé et d’un script qui exécute le test plusieurs fois sur chaque commit.
FAQ
git bisect peut-il trouver le commit qui a corrigé un bogue plutôt que celui qui l'a introduit ?
Oui. Utilisez les termes old et new au lieu de good et bad. Lancez git bisect start, marquez un commit corrigé avec git bisect new et un commit plus ancien défaillant avec git bisect old. Git signale alors le premier commit new, c'est-à-dire le correctif. Pour des libellés plus explicites, lancez git bisect start --term-old broken --term-new fixed, puis marquez les commits avec git bisect fixed et git bisect broken.
Comment git bisect gère-t-il les commits de fusion provenant de branches de fonctionnalité ?
Par défaut, git bisect teste aussi les commits situés à l'intérieur des branches fusionnées, y compris des commits de travail en cours qui ne compilent pas forcément. L'option git bisect start --first-parent, ajoutée dans Git 2.29, maintient la recherche sur la ligne principale de l'historique. Sur main, elle s'arrête sur la fusion qui a introduit le bogue et ne teste jamais les commits propres à la branche. Une seconde bisection sur cette branche permet ensuite de trouver le commit exact.
Que se passe-t-il si le commit good n'est pas un ancêtre du commit bad ?
Git extrait une base de fusion (merge base) des deux commits et vous demande de la tester en premier. Si cette base de fusion est bonne, la bisection se poursuit normalement sur les commits situés entre elle et le commit défaillant. Si elle est déjà défaillante, Git s'arrête au lieu de chercher : le commit good se trouve alors sur une ligne d'historique distincte, où le bogue est absent ou a été corrigé.
Quelle est la différence entre git bisect et git blame ?
git blame indique, ligne par ligne, quel commit a modifié un fichier en dernier. Il répond donc uniquement à la question de savoir qui a modifié une ligne donnée en dernier. git bisect teste le comportement d'un commit à l'autre. Il trouve donc une régression même lorsque la cause est une mise à jour de dépendance, un changement de configuration ou un autre fichier que celui où le symptôme apparaît. Utilisez bisect pour trouver le commit, puis blame pour retracer les lignes qu'il a modifiées.