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

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
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
Qu'est-ce que le SMS pumping et comment s'en protéger ?
La fraude appelée SMS pumping survient lorsque des fraudeurs exploitent un champ de saisie de numéro de téléphone de votre ...
Arnaud Albalat
Arnaud Albalat
CTO @ Galadrim
Les Normes RGAA et l’accessibilité numérique
En France, 90 % de la population, parmi lesquels se trouvent vos clients, se connectent à des sites web et des contenus en ...
Arnaud Albalat
Arnaud Albalat
CTO @ Galadrim