Trouver l'origine d'un bug efficacement avec git bisect

Blog article image cover
Vous voulez trouver rapidement le commit qui a introduit un bug dans votre base de code mais vous avez plusieurs centaines, ou milliers de commits ? git bisect est probablement l'outil qu'il vous faut.

La théorie

En anglais bisect veut dire "Couper en deux", et c'est exactement ce que fait git bisect :
  • on lui donne un commit "bon", c'est-à-dire un commit où le bug n'était pas encore présent ;
  • ensuite, on lui donne un commit "mauvais" (souvent le commit actuel) ;
  • enfin, tant que git bisect trouve plusieurs commits entre le "bon" et le "mauvais" commit, il prend le commit entre les 2 et nous demande si ce commit est ok.
L'avantage d'utiliser une recherche binaire est que l'on sait combien d'étapes il faudra au maximum pour trouver le commit qui a introduit le bug, et que ce nombre d'étapes n'augmente que de 1 à chaque fois que notre nombre de commits suspects double.
Par exemple le code de Linux contient plus de 1 million de commits, pourtant il ne faudrait au maximum que 20 étapes (logarithme binaire de 1 000 000) pour trouver le commit qui introduit un bug !
Voila pour la théorie, maintenant on va voir comment utiliser cet outil en pratique.

La pratique

Pour montrer l'utilisation de git bisect, nous allons utiliser ce dépôt git (qui contient un bug).
Nous allons commencer par télécharger le code :
git clone https://github.com/mle-moni/bisect-test
Le fichier test.js contient ce code :
// numbers is an array of numbers function getNumber(numbers, index) { if (!numbers[index]) { throw new Error('no number for this index') } return numbers[index] } const numbers = [ 42, -121, 4235, 0 ] const index = process.argv[2] console.log(number is ${getNumber(numbers,index)})
La commande suivante nous permettra de savoir si le commit contient le bug ou non :
node test.js 3
(on considérera le commit comme mauvais si cette commande renvoie une erreur)
Maintenant voyons quels commits ont été faits :
# montre la liste des commits avec leur auteur du plus ancien au plus récent git shortlog
LE MONIES DE SAGAZAN Mayeul (28): no bug here no bug here too no bug here too no bug here too no bug here too no bug here too no bug here too no bug here too no bug here too no bug here too bug introduction we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we still don't know that there is a bug we just discovered that there is a bug Create README.md
Notre but sera, avec git bisect, de trouver que le commit fautif est bien celui qui porte le nom "bug introduction".
C'est parti !
git bisect start
# on choisit un commit où il n'y avait pas le bug (ici, c'est le premier commit du dépôt git) git bisect good 0f436453aac33b7d39f04be33b909097b34def10
# on précise que le commit actuel est mauvais git bisect bad
L'outil nous déplace ensuite sur le commit entre le bon et le mauvais :
Bisecting: 13 revisions left to test after this (roughly 4 steps) [fb045ac20c5972136afd8c10e510c0483f97b1a9] we still don't know that there is a bug
Ensuite on teste le code (ici c'est facile car c'est du JavaScript, souvent on aura une étape de compilation) :
node test.js 3 Error: no number for this index
Ce commit contient le bug, on va donc dire à git bisect que le commit est mauvais :
git bisect bad
Bisecting: 6 revisions left to test after this (roughly 3 steps) [991ef4bb20a5d29cc6a307dd3a289a5fc3159c3d] no bug here too
Ensuite on continue cette routine jusqu'à trouver le mauvais commit !
node test.js 3 number is 0 git bisect good Bisecting: 3 revisions left to test after this (roughly 2 steps) [bc4dd976f1b8e7e79a7109ac074b610dddcf6dd5] no bug here too
node test.js 3 number is 0 git bisect good Bisecting: 1 revision left to test after this (roughly 1 step) [f1a089670548b09e2b52737aa05aa25921ee463f] we still don't know that there is a bug
node test.js 3 Error: no number for this index git bisect bad Bisecting: 0 revisions left to test after this (roughly 0 steps) [9d7a3917e0fdfc71be8426f19ccf26b217d0f546] bug introduction
Et enfin :
node test.js 3 Error: no number for this index git bisect bad 9d7a3917e0fdfc71be8426f19ccf26b217d0f546 is the first bad commit commit 9d7a3917e0fdfc71be8426f19ccf26b217d0f546 Author: LE MONIES DE SAGAZAN Mayeul <mail@example.com> Date: Sun May 15 14:45:17 2022 +0200 bug introduction test.js | 3 +++ 1 file changed, 3 insertions(+)
Enfin on peut regarder quels changements ont provoqué l'apparition du bug :
git diff HEAD^ function getNumber(numbers, index) { + if (!numbers[index]) { + throw new Error('no number for this index') + } return numbers[index] }
Ici le "bug" était donc la condition
if (!numbers[index]) {
puisque numbers[index] peut être 0 et que !0 donne true, il aurait fallu être plus précis et afficher l'erreur uniquement si numbers[index] était undefined :
if (numbers[index] === undefined) {

Cas particuliers

Si un des commits ne peut pas être testé (s'il ne build pas par exemple), on a plusieurs solutions :
  • choisir à la main un autre commit :
git reset HARD~2 # se placer sur 2 commits avant celui choisi par git bisect
  • laisser git bisect choisir le prochain commit :
git bisect skip
Si on souhaite arrêter la recherche binaire on peut le faire simplement :
git bisect reset
Voilà, c'est tout pour l'outil git bisect !

Bonus

Si vous voulez avoir la possibilité de regarder les diff avec VS Code tout en continuant d'avoir les outils de git (rebase, merge, ...) en CLI avec vim, c'est possible en configurant git difftool.
Voici par exemple ma configuration ~/.gitconfig
[core] editor = vim [user] name = LE MONIES DE SAGAZAN Mayeul email = mail@example.com [diff] tool = vscode [difftool "vscode"] cmd = code --wait --diff $LOCAL $REMOTE
Ensuite il suffit d'utiliser git difftool de la même façon qu'on utilise git diff :
git difftool HEAD^ Launch 'vscode' [Y/n]? Y
Vous souhaitez être accompagné pour lancer votre projet digital ?
Déposez votre projet dès maintenant
Article presentation image
Comment utiliser GitLab CI/CD pour améliorer votre flow de développement ?
Lors du développement d'une application, il y a toujours une petite appréhension lors la mise en production. Cette petite ...
Matthieu Locussol
Matthieu Locussol
Full-Stack Developer @ Galadrim
Article presentation image
Comment changer de version de Node.js avec NVM ?
Vous voulez changer rapidement de version de `node` ? nvm est l’outil qu’il vous faut. Pourquoi nvm ? `node` est un exécutable. ...
Florian Yusuf Ali
Florian Yusuf Ali
Full-Stack Developer @ Galadrim
Article presentation image
Next.js App Router : le cache et ses dangers
“Il y a seulement 2 problèmes compliqués en informatique : nommer les choses, et l’invalidation de cache”. Phil Karlton. Avec ...
Valentin Gerest
Valentin Gerest
Full-Stack Developer @ Galadrim