12k
All articles

Qui est le coupable ? Retrouver le fautif avec git blame

git blame expliqué : lire la sortie, cibler des lignes avec -L, -w, -M, -C, ignorer les gros commits et retrouver le vrai changement.

OpenReplay Team
OpenReplay Team
Qui est le coupable ? Retrouver le fautif avec git blame

git blame annote chaque ligne d’un fichier avec le commit qui l’a modifiée en dernier, ainsi que l’auteur et la date de ce commit.

Le commit désigné est souvent le mauvais. Vous cherchez l’origine d’une ligne étrange dans du code que vous n’avez pas écrit, et blame vous renvoie un commit « apply prettier » de 4 000 fichiers datant d’il y a dix-huit mois.

Cette impasse est le résultat habituel d’un git blame brut, et savoir la dépasser constitue la véritable compétence. Cet article explique comment lire la sortie par défaut, comment la restreindre et en éliminer le bruit avec -L, -w, -M et -C, comment remonter de parent en parent jusqu’à atteindre le changement qui compte, ainsi que deux ajouts récents : --diff-algorithm (Git 2.53 ou ultérieur) et git last-modified (Git 2.52 ou ultérieur).

Points clés à retenir

  • Le commit que git blame affiche pour une ligne est le dernier à l’avoir touchée, ce qui correspond souvent à un reformatage, un renommage ou un déplacement plutôt qu’au changement qui a donné son sens à la ligne.
  • Relancer blame sur le parent du commit signalé (git blame <hash>^ -- file) et répéter l’opération est la méthode fiable pour remonter jusqu’au changement d’origine ; --ignore-rev et --ignore-revs-file permettent d’ignorer automatiquement les commits parasites connus.
  • -w ignore les espaces, -M suit les lignes déplacées à l’intérieur d’un fichier (seuil par défaut : 20 caractères alphanumériques) et -C suit les lignes copiées depuis d’autres fichiers (seuil par défaut : 40), jusqu’à trois indicateurs -C élargissant progressivement la recherche.
  • Git 2.53 a ajouté --diff-algorithm à git blame, qui accepte patience, minimal, histogram ou myers, avec myers comme valeur par défaut.
  • Git 2.52 a introduit la commande expérimentale git last-modified, qui indique en un seul parcours le dernier commit ayant touché chaque chemin d’un répertoire.

Comment lire la sortie par défaut de git blame ?

Chaque ligne de la sortie par défaut de git blame comporte quatre champs, dans l’ordre : le hash abrégé du commit, le nom de l’auteur, la date de création et le numéro de ligne, suivis du contenu de la ligne. La section sur le format par défaut de la page de manuel énumère ces champs ; Git abrège le hash à sept chiffres hexadécimaux par défaut et laisse une colonne supplémentaire libre pour le caret qui signale un commit limite (les commits les plus anciens que blame a pu atteindre). Les dates s’affichent au format ISO, sauf indication contraire via --date ou blame.date.

git blame src/router.js
a1b2c3d4 (Jane Doe 2024-03-08 14:22:31 +0100 42)   return cache.get(key) ?? fetchRoute(key);

Lecture de gauche à droite : a1b2c3d4 est le commit, Jane Doe et l’horodatage constituent l’identité de l’auteur de ce commit, 42 est le numéro de ligne dans le fichier actuel, et tout ce qui suit la parenthèse fermante est la ligne elle-même.

Ce qu’il faut surtout comprendre à propos de ce hash, c’est ce qu’il n’est pas. Ce n’est pas le commit qui a introduit la logique. C’est le commit le plus récent dont le diff a touché la ligne, et dans une base de code truffée de formateurs, de linters et de refactorisations, il s’agit fréquemment d’un changement mécanique. Considérez le premier résultat de blame comme une piste, pas comme un verdict.

Comment restreindre git blame à une plage de lignes avec -L ?

git blame -L 40,60 -- src/router.js limite l’annotation aux lignes 40 à 60, et git blame -L :handleRoute -- src/router.js la limite au corps de la fonction dont le nom correspond à cette expression régulière. Ces deux formes sont documentées sous l’option -L, qui peut être indiquée plusieurs fois.

git blame -L 40,60 -- src/router.js
git blame -L :handleRoute -- src/router.js

La forme :funcname n’analyse pas votre langage. Elle repère les noms de fonctions de la même manière que git diff détermine ce qu’il doit afficher dans un en-tête de section, et vous pouvez ajuster ce comportement par type de fichier grâce à l’attribut diff dans gitattributes. Les deux bornes de la plage acceptent également des motifs /regex/, et la borne finale accepte des décalages +N : -L '/^function handleRoute/,+15' est donc également valide.

Comment ignorer les espaces et le code déplacé dans git blame ?

L’option -w indique à git blame d’ignorer les espaces lors de la comparaison des versions, de sorte qu’un reformatage portant uniquement sur l’indentation ne s’approprie plus les lignes qu’il a touchées. -M détecte les lignes déplacées à l’intérieur d’un même fichier, et -C élargit la recherche aux lignes provenant d’autres fichiers modifiés par le même commit ; la page de manuel indique des seuils de correspondance par défaut de 20 et 40 caractères alphanumériques respectivement.

SymptômeIndicateur
Ligne attribuée à une réindentation ou à un nettoyage d’espaces en fin de ligne-w
Ligne attribuée au commit qui a réorganisé le code à l’intérieur du fichier-M
Ligne provenant d’une copie ou d’un déplacement depuis un autre fichier-C (cumulable)
Ligne attribuée à un commit massif connu--ignore-rev <hash>
git blame -w -- src/router.js
git blame -M -- src/router.js
git blame -C -C -C -- src/router.js

Chaque -C supplémentaire élargit l’ensemble des fichiers dans lesquels git blame recherche les lignes copiées :

  • -C recherche dans les autres fichiers modifiés par ce même commit.
  • -C -C recherche en plus dans les fichiers touchés par le commit qui a ajouté celui-ci pour la première fois.
  • -C -C -C élargit encore la recherche, à des fichiers appartenant à n’importe quel commit.

Si plusieurs indicateurs -C comportent un seuil numérique, c’est le dernier qui l’emporte. Un renommage de fichier complet ne nécessite aucun indicateur : blame continue de suivre les lignes au travers du renommage de lui-même, et Git n’offre actuellement aucun moyen de désactiver ce comportement.

Comment retrouver le commit antérieur à un reformatage massif ?

Pour dépasser un commit mécanique, relancez blame sur le parent de ce commit, git blame <hash>^ -- src/router.js, et répétez l’opération jusqu’à ce que le commit affiché soit celui qui a réellement modifié le comportement de la ligne. Le suffixe ^ relève de la syntaxe standard gitrevisions pour désigner le premier parent : blame démarre donc à partir de l’état du fichier juste avant l’arrivée du commit parasite.

  1. Exécutez git blame -L 40,60 -- src/router.js et notez le hash figurant sur la ligne qui vous intéresse.
  2. Examinez le commit avec git show --stat <hash>. S’il s’agit d’un reformatage, d’un renommage ou d’un déplacement, poursuivez.
  3. Exécutez git blame -n <hash>^ -L 40,60 -- src/router.js. L’indicateur -n affiche le numéro de chaque ligne dans le commit d’origine, ce qui importe car les numéros de ligne varient d’une révision à l’autre et vous devrez peut-être réajuster -L au passage suivant.
  4. Reprenez à l’étape 2 jusqu’à ce que le commit affiché modifie effectivement le comportement de la ligne.
git blame -n a1b2c3d4^ -L 40,60 -- src/router.js

Lorsqu’un dépôt comporte des commits parasites connus, évitez le parcours manuel. --ignore-rev <hash> demande à git blame d’attribuer les lignes au-delà d’un commit donné, et --ignore-revs-file fait de même pour tout un fichier de hashes, écrits en entier, à raison d’un par ligne. Définissez blame.markIgnoredLines pour signaler les lignes réattribuées par un ? et blame.markUnblamableLines pour signaler par un * les lignes qui n’ont pas pu être réattribuées.

git blame --ignore-rev a1b2c3d4 -- src/router.js
git blame --ignore-revs-file .git-blame-ignore-revs -- src/router.js
git config blame.markIgnoredLines true

La démarche consistant à versionner cette liste sous le nom .git-blame-ignore-revs et à y faire pointer blame.ignoreRevsFile est traitée dans 5 Git Dotfiles Every Developer Should Know.

Essayer un autre algorithme de diff (Git 2.53 ou ultérieur)

Git 2.53 a ajouté --diff-algorithm à git blame, qui accepte patience, minimal, histogram ou myers (avec default comme alias de myers), myers étant la valeur par défaut. Cet ajout figure dans les notes de version de Git 2.53, et les valeurs acceptées sont énumérées sous l’option —diff-algorithm de la page de manuel.

Blame détermine quelles lignes du parent correspondent à quelles lignes de l’enfant en comparant les deux versions, et différents algorithmes apparient les lignes différemment. Lorsqu’un commit entremêle lignes modifiées et lignes inchangées, comme le font souvent les reformatages, un algorithme peut attribuer une ligne au reformatage tandis qu’un autre l’attribue au commit qui l’a écrite à l’origine.

git blame -L 40,60 -- src/router.js
git blame -L 40,60 --diff-algorithm=patience -- src/router.js

Aucun algorithme n’est documenté comme étant plus correct qu’un autre. Si l’attribution par défaut paraît invraisemblable, exécuter la même commande avec patience ou histogram ne coûte qu’une invocation supplémentaire et vous fournit un second avis à comparer.

Interroger un répertoire avec git last-modified

Git 2.52 a introduit git last-modified, qui indique le commit ayant modifié en dernier chaque chemin d’un répertoire, au cours d’un unique parcours de l’historique plutôt qu’avec un git log -1 par fichier ; la commande est marquée comme expérimentale et son comportement est susceptible d’évoluer. La page de manuel de git-last-modified mentionne ce statut expérimental dès sa ligne NAME et présente le format de sortie sous la forme <oid> TAB <path>, une ligne par chemin, avec un identifiant d’objet complet et sans auteur, date ni sujet.

git last-modified -r -- src/

Sans -r (ou sans une valeur --max-depth non nulle), vous n’obtenez que les entrées correspondant au pathspec lui-même, sans descente dans les sous-répertoires sous-jacents. Les renommages et les changements de mode comptent comme des modifications. La boucle par fichier qu’elle remplace reparcourt les mêmes commits une fois pour chaque fichier ; last-modified ne les parcourt qu’une seule fois. Elle répond à la question « qu’est-ce qui a changé récemment dans ce module », question distincte de « pourquoi cette ligne existe-t-elle », et il vaut la peine d’y recourir avant de commencer à passer des fichiers individuels au crible de blame.

Blame est une question, pas un verdict

La sortie de git blame désigne la dernière personne ayant touché une ligne, et ce nom n’est presque jamais la réponse dont vous avez besoin. Exécutez blame avec -L pour cibler, -w et -M/-C pour éliminer le bruit mécanique, puis remontez de parent en parent (ou maintenez un fichier d’exclusion) jusqu’à ce que le commit affiché porte un message qui explique la ligne. Une fois ce commit identifié, git show <hash> vous donne le diff et le raisonnement, ce qui constitue tout l’intérêt de l’exercice : comprendre pourquoi le code est là, afin de pouvoir le modifier sans reproduire l’incident qui l’y a placé au départ.

FAQ

Comment savoir qui a supprimé une ligne, puisque git blame ne montre que les lignes encore présentes ?

git blame ne dit rien des lignes supprimées ou écrasées, comme le signale sa page de manuel. Utilisez plutôt le pickaxe : git log -S'some text' -- src/router.js liste tous les commits ayant ajouté ou retiré cette chaîne, et l'ajout de -p affiche la suppression elle-même. Autre possibilité : git blame --reverse a1b2c3d..HEAD -- src/router.js parcourt l'historique vers l'avant à partir de ce commit et désigne la révision la plus récente dans laquelle chaque ligne était encore présente.

Pourquoi git blame affiche-t-il 00000000 et « Not Committed Yet » sur certaines lignes ?

Ces lignes comportent des modifications non validées. En l'absence d'argument de révision, git blame annote la copie du fichier présente dans la copie de travail : toute ligne différant de HEAD reçoit donc un hash composé uniquement de zéros et la mention « Not Committed Yet » à la place du nom de l'auteur. Validez ou remisez la modification, ou exécutez git blame HEAD -- src/router.js pour annoter la version validée et ignorer entièrement les modifications locales.

La vue blame de GitHub respecte-t-elle un fichier .git-blame-ignore-revs ?

Oui. GitHub applique automatiquement à sa vue blame un fichier nommé .git-blame-ignore-revs situé à la racine du dépôt, en utilisant le même mécanisme --ignore-revs-file que la ligne de commande, et affiche alors une bannière « Ignoring revisions ». Les lignes qui ne peuvent pas être réattribuées à un commit antérieur continuent d'afficher le commit ignoré. Ce fichier ne configure pas git en local : chaque développeur doit toujours exécuter git config blame.ignoreRevsFile .git-blame-ignore-revs.

Quelle est la différence entre git blame et git log -L ?

git blame indique un commit par ligne : le commit le plus récent l'ayant touchée dans une version donnée du fichier. git log -L 40,60:src/router.js retrace au contraire les lignes 40 à 60 à travers l'historique et affiche tous les commits les ayant modifiées, chacun accompagné du diff correspondant à cette plage, du plus récent au plus ancien. Utilisez blame pour identifier rapidement un suspect et log -L pour observer l'évolution des lignes. Les deux acceptent la forme :funcname, comme dans git log -L :handleRoute:src/router.js.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.