La suite de tests est au vert depuis trois semaines. Aujourd’hui, npm test échoue sur une assertion qui n’a rien à voir avec ce que vous venez de modifier, et git log v1.4.0..HEAD affiche 340 commits de quatre personnes depuis la dernière release dont quelqu’un était sûr qu’elle fonctionnait. Lire chaque diff vous coûte une journée entière. Faire des checkouts de commits au hasard va plus vite, mais ce n’est toujours pas un plan.
git bisect transforme cela en recherche binaire. Vous lui donnez un commit que vous savez bon et un que vous savez cassé ; il fait le checkout du commit à mi-chemin entre les deux et attend que vous testiez ce commit unique et rapportiez le résultat. Chaque réponse divise l’intervalle par deux. 340 commits se résolvent en 9 tests maximum, puisque 2^9 = 512.
Comment git bisect réduit la recherche de moitié à chaque étape
Imaginez les 340 commits alignés, du plus ancien au plus récent, le tag bon connu à une extrémité et le HEAD cassé à l’autre. git bisect ne parcourt pas cette ligne. Il saute au milieu, attend un verdict, puis élimine la moitié que ce verdict exclut :
| |
Disons que le commit du milieu passe. Le bug se trouve dans la moitié la plus récente, donc la moitié la plus ancienne disparaît définitivement et vous ne reverrez plus ces commits dans cette session. Répétez : milieu de ce qui reste, testez, éliminez la moitié. Après environ log2(n) tours, l’intervalle se réduit à un seul commit — celui où le comportement est passé de fonctionnel à défaillant. Ce n’est pas toujours le commit qui a “causé” le bug au sens le plus profond. C’est le commit où le comportement a changé, ce qui est presque toujours exactement ce dont vous avez besoin.
Comment démarrer une session git bisect
Exécutez ceci depuis un arbre de travail propre. Bisect fait des checkouts de commits sous vos pieds, et refuse de démarrer en présence de modifications non validées.
| |
Git fait le checkout du point médian et vous indique ce qu’il reste à faire :
| |
Exécutez votre test sur ce commit, puis rapportez ce que vous avez trouvé :
| |
ou bien
| |
Git fait automatiquement le checkout du nouveau point médian après chaque réponse. Continuez à tester et à rapporter jusqu’à ce qu’il n’y ait plus rien à réduire :
| |
Voilà votre réponse. Lisez ce diff, pas les huit commits autour. git bisect reset vous replace sur la branche et le commit de départ, et les checkouts effectués par bisect en chemin ne laissent aucune trace.
| |
Automatiser la boucle avec git bisect run
Répondre good ou bad à la main convient pour un bug repérable en quelques secondes. Pour ce qui nécessite une vraie exécution de tests, confiez toute la boucle à un script :
| |
bisect-test.sh doit seulement sortir avec le bon code : 0 signifie bon, tout code de 1 à 127 sauf 125 signifie cassé, et 125 signifie “impossible de tester ce commit, passez-le”.
| |
La plupart des test runners sortent déjà avec 0 en cas de succès et avec un code différent de zéro en cas d’échec, donc le script se résume souvent à un simple appel au runner. git bisect run fait le checkout de chaque point médian, exécute le script, lit le code de sortie et rapporte lui-même le résultat, en affichant à la fin le même résumé “first bad commit”.
Le script peut faire plus qu’appeler un test runner. Si la régression n’apparaît que dans un artefact construit, un script qui exécute docker build puis met l’image à l’épreuve est une chose normale à confier à git bisect run — les étapes de build de Construire et Publier une Image Docker avec GitHub Actions fonctionnent aussi bien dans un script que dans un workflow. Prévoyez-le : chaque tour coûte désormais une build complète de l’image.
Une contrainte concerne le script lui-même. Gardez-le hors de l’intervalle sur lequel vous faites le bisect, ou assurez-vous qu’il n’a pas changé sur cet intervalle. Si le script vit dans l’historique exploré, un ancien commit fait le checkout d’une ancienne version du script en même temps que de l’ancien code, et vous n’exécutez plus le même test à chaque étape.
Ignorer les commits que vous ne pouvez pas tester avec git bisect skip
Certains commits ne peuvent pas être testés isolément : un commit en plein refactoring qui ne compile pas, une migration de schéma qui nécessite un état de base de données que vous n’avez pas. Forcer un verdict good/bad sur l’un d’eux fausse le résultat. Excluez-le plutôt :
| |
Bisect fait le checkout d’un commit voisin et continue en contournant le trou. Dans bisect run, le code de sortie 125 fait la même chose automatiquement, donc un script capable de détecter “ce commit ne compile même pas” peut le passer plutôt que de rapporter un faux bad. Ignorez trop de commits adjacents et bisect finit par manquer de terrain testable : la réponse finale devient “le bug vient de l’un de ces N commits” plutôt qu’un commit unique.
Quand git bisect n’est pas le bon outil
Bisect suppose que votre test est déterministe : même commit, même résultat, à chaque fois. Un test instable brise cette hypothèse en silence. Bisect ne peut pas distinguer “ce commit est cassé” de “ce test a flanché”, et une réponse erronée au début envoie chaque tour suivant dans la mauvaise moitié. Corrigez ou excluez l’assertion instable avant de faire un bisect, pas après.
Il suppose aussi qu’un commit a produit un seul changement de comportement net. Un bug qui s’est glissé sur plusieurs commits, ou qui dépend d’un état accumulé plutôt que d’un diff unique, ne converge vers rien d’utile. Un historique fortement rebasé est une version atténuée du même problème (voir Git Rebase vs Merge : différences et quand les utiliser) : le premier commit défaillant trouvé par bisect peut être un commit écrasé regroupant cinq changements logiques. Correct, mais trop grossier pour être relu isolément.
Aucun des deux cas n’appelle un outil différent, seulement un point de départ différent. Rendez d’abord le test fiable, puis choisissez un commit good suffisamment ancien pour être sûr que le bug n’était pas déjà là.
La prochaine fois qu’une régression surgit sans cause évidente, marquez les deux extrémités et répondez good ou bad quelques fois — ou écrivez le script de trois lignes et laissez git bisect run répondre à votre place.