← Retour à la page précédente

Git bisect : le chasseur de bugs !

Cet article est également disponible dans d'autres langues :
Placeholder banner image for article

Git bisect est une commande disponible depuis Git 2.6.0. Elle utilise un algorithme de recherche dichotomique pour trouver quel commit dans l'historique a introduit un changement particulier (bug ou amélioration).
Cette commande aide à parcourir rapidement l'historique de commits et à réduire au fur et à mesure le périmètre de recherche. Cette démarche permet d'identifier un changement étant à l'origine d'un problème ou d'un effet de bord, y compris lorsque l'on n'a aucune idée d'où chercher.

Pour quels cas d'usage ?

Git bisect n'est pas la réponse à tous vos problèmes. Pour tirer profit de son potentiel sans qu'il ne vous fasse perdre plus de temps qu'autre chose, il faut connaître ses cas d'utilisation types. Ainsi la commande est plus ou moins adaptée à certains contextes.

Elle est particulièrement efficace lorsque :

  • Le bug ou changement à retrouver est clairement identifié, systématiquement reproductible via un scénario fixe.
  • Vous n'êtes pas sûrs d'où et quand le changement impactant a eu lieu (s'il s'agit d'un effet de bord lié à un composant transverse, ou d'une amélioration fortuite des performances par exemple).
  • Il y a beaucoup de commits à parcourir et vous n'avez absolument aucune idée de quoi chercher (aiguille dans la botte de foin : cas du bug incompréhensible qui use les nerfs).
  • Vous avez la possibilité de tester tous les commits (ou presque) atomiquement. C'est-à-dire, chaque commit est dans un état suffisamment stable pour permettre de dérouler le scénario à tester.
  • Le scénario de vérification est relativement rapide. Il sera beaucoup plus agréable et intuitif de tester la présence ou absence d'un comportement via un processus qui prend peu de temps, plutôt qu'une compilation de 20 min à chaque étape risquant de vous décourager en chemin.

Comment ça marche ?

Pour mieux comprendre comment opère l'outil, voici les grandes lignes de l'utilisation de git bisect dans le cas d'une recherche de bug :

  1. Vous commencez par indiquer à git bisect un "mauvais" commit, connu car le bug y est présent et vous le reproduisez.
  2. Vous indiquez ensuite un "bon" commit suffisamment loin dans le passé, connu car le bug en est absent et vous ne le reproduisez pas.
  3. Git bisect vous déplace automatiquement jusqu'au commit situé au milieu de l'intervalle formé par vos deux commits "bon" et "mauvais".
  4. Vous devez ensuite tester ce commit en reproduisant votre scénario (build, lancement, test d'une feature...) et indiquer par la suite à git bisect s'il est "bon" (avec git bisect good) ou "mauvais" (avec git bisect bad).
  5. Git bisect vous déplace alors vers un nouveau commit, basé sur les informations fournies jusqu'ici. Concrètement, il divise en deux l'intervalle entre votre mauvais commit le plus ancien et votre bon commit le plus récent.
  6. Vous testez ce commit comme les précédents, l'indiquez comme bon ou mauvais, etc.
  7. Ce process de recherche itérative prend fin de lui-même lorsque git bisect a terminé son parcours de l'historique et vous indique quel commit a introduit le bug.

Vous l'aurez compris, git bisect n'a pas la science infuse et nécessite que vous soyez rigoureux dans votre procédé de test et de "tag" de commits pour donner des résultats concluants.

Si à une étape de la recherche dichotomique, vous ne pouvez pas attester qu'un commit est "bon" ou "mauvais" (par exemple, s'il s'agissait d'un travail en cours, non stabilisé, ou qui ne compile pas) vous pouvez utiliser git bisect skip pour passer à l'étape suivante.

git bisect skip va ignorer le commit, mais à la fin du process, vous aurez une liste contenant plus d'un commit potentiellement en cause, incluant ceux que vous avez ignorés au cours de la recherche.

Exemple

Prenons un cas concret pour mieux illustrer.

Vous souhaitez déterminer quel commit a mis à mal votre superbe système d'injection de dépendances que vous aviez mis en place dans la release 6.0.1 de votre projet frontend.

Le problème est le suivant : vous ne savez pas du tout par où commencer car de nombreux changements ont eu lieu partout dans la base de code depuis cette release, et vous ne savez pas non plus où le problème trouve sa source. Du tout.

Vous allez démarrer git bisect comme ceci, en vous plaçant sur le commit actuel où vous constatez le problème :

git bisect start
git bisect bad  # Le commit actuel est mauvais
git bisect good v6.0.1  # Le tag de la version v6.0.1 est fonctionnel

Une fois cette première étape validée, git bisect sélectionne votre premier commit à tester basé sur ce que vous lui avez indiqué, vous y amène (= checkout) et affiche quelque chose comme :

Bisecting: 675 revisions left to test after this (roughly 10 steps)

C'est le moment de re-compiler (et peut-être relancer) cette version du code pour tester à nouveau votre scénario.

Si cette version fonctionne comme attendu, tapez :

git bisect good

Si cette version est cassée (le bug est présent), tapez :

git bisect bad

Ce qui nous amène à un nouvel affichage de git bisect du type :

Bisecting: 337 revisions left to test after this (roughly 9 steps)

Continuez à répéter ce procédé : compilez votre code, testez le, et lancez la commande appropriée pour tagger chaque commit que git bisect vous propose.

A force d'itérations, il n'y aura plus de commits à tester et inspecter, et la commande affichera la référence et la description du premier mauvais commit qu'elle a identifié. Victoire !

Dans l'historique, la référence refs/bisect/bad pointera sur ce commit, visible dans l'arbre git tant que vous n'utilisez pas la commande git bisect reset.

Termes alternatifs

Parfois, ce n'est pas un bug que nous recherchons mais une amélioration impromptue. Dans ce cas les termes "bon" et "mauvais" peuvent être source de confusion (nous recherchons un changement à impact positif, donc la sémantique de recherche est inversée).

Pour plus de clarté, nous pouvons utiliser les termes old et new au lieu de good et bad.

Exemple : Vous démarrez git bisect avec un "nouveau" commit contenant l'amélioration recherchée, et un "vieux" commit qui ne la contient pas. Cela peut être une amélioration de performances par exemple, sans que vous ne sachiez ce qui l'a provoquée.

Pour chaque commit contenant cette amélioration, marquez-les comme "nouveaux". Pour les autres, marquez les comme "vieux".

A la fin de la recherche, git bisect indiquera le premier "nouveau" commit contenant l'amélioration.

Il n'est pas possible de mixer good/bad et old/new dans une même recherche git bisect. Pour les changer, vous devrez utiliser git bisect reset ou recommencer une autre recherche de zéro.

Vous pouvez également utiliser vos propres termes pour une recherche git bisect, comme ceci :

git bisect start --term-old <term-old> --term-new <term-new>

Par exemple pour un problème de performances résolu par un commit non identifié :

git bisect start --term-new fast --term-old slow

Ces changements, bien que pur sucre syntaxique, peuvent faciliter la recherche.

Commandes pour usage avancé

git bisect (visualize|view)
Affiche une liste des suspects restants dans la liste de test de git bisect, via git log.

git bisect log
Affiche une liste des commits taggés comme "bons" ou "mauvais" jusqu'ici.

git bisect replay
Rejoue une recherche git bisect depuis un fichier de log sauvegardé.

Si vous avez fait une erreur lors de l'identification d'un commit, vous pouvez enregistrer le log de votre recherche dans un fichier externe, l'éditer, puis utiliser ces commandes pour vous remettre en piste :

git bisect reset
git bisect replay my-file

git bisect run {cmd}
Si vous avez sous la main un script vous permettant de déterminer si le code source actuel est bon ou mauvais, vous pouvez lancer une bisection automatique via la commande :

git bisect run my_script arguments

Votre script doit se terminer par un code d'éxécution 0 si le code source est bon, et par n'importe quel code entre 1 et 127 (inclus, sauf 125) si le code source est mauvais.

Le code spécial 125 doit uniquement être renvoyé lorsque le code ne peut pas être testé, et déclenchera un skip.

Si n'importe quel autre code est renvoyé, la recherche sera interrompue.

Potentiel d'automatisation

La commande git bisect run peut être utilisée dans des scripts d'automatisation pour de l'intégration continue, afin de trouver automatiquement le(s) commit(s) fautifs lorsqu'un build est cassé.

Par exemple, si nous avons un simple script capable de builder et renvoyer le code de retour approprié en fonction du résultat de build, nous pouvons automatiser git bisect sur un intervalle de commits :

(master) $git bisect start HEAD <sha1>
Bisecting: xxx revisions left to test after this (roughly x steps)
[A commit SHA1] A commit message

((bisect/bad~512)|BISECTING) $ git bisect run test.sh

running ../test.sh
Bisecting: xxx revisions left to test after this (roughly x steps)
[Another commit SHA1] Another commit message

...

running ../test.sh

Et finalement, le résultat:

Bisecting: 1 revision left to test after this (roughly 1 step)
[465194af92951519c7da6542eaca0c56ee09fcd9] I have no idea what I'm doing

465194af92951519c7da6542eaca0c56ee09fcd9 is the first bad commit
commit 465194af92951519c7da6542eaca0c56ee09fcd9
Author: Jean-Michel Apeuprès <jean-mi.approximately@jean-mi-is-the-best.fr>
Date:   Sat Feb 8 16:39:47 2014 +0100
   I have no idea what I'm doing

{A detailed list of changes in the changeset}

bisect run success

Voilà, c'est la fin de cet article ! Git bisect est une commande très intéressante dotée d'un fort potentiel d'automatisation. Pour peu qu'elle soit utilisée dans un contexte adapté, elle peut se révéler bien plus efficace et rigoureuse qu'une recherche manuelle.

Pour plus d'informations, n'hésitez pas à vous référer à la documentation officielle de Git.