Un meilleur rendu de Git diff pour Ruby
Si vous utilisez Git en ligne de commande, vous connaissez bien la sortie diff qui se décompose en morceaux (hunks) comme ci-dessous :
@@ -231,7 +231,7 @@ class TimeCalc
case
when from.class == Date
[coerce_date(from, to), to]
- when to.class == Date
+ when to.instance_of?(Date)
[from, coerce_date(to, from)]
else
[from, to.public_send("to_#{from.class.downcase}")]
La première ligne (commençant par @@
) est connue sous le nom de « hunk
header » et est là pour nous aider à comprendre où a eu lieu le changement. Elle
nous donne les numéros de ligne pour le changement (les numéros entre les paires
d’arobases), mais aussi une description textuelle du contexte dans lequel le
changement s’est produit, dans cet exemple class TimeCalc
. Git essaie de
comprendre ce contexte, qu’il s’agisse d’une fonction, d’un module ou d’une
définition de classe. Pour les langages de type C, il est assez bon à ce niveau.
Mais pour l’exemple Ruby ci-dessus, il n’a pas réussi à nous montrer le contexte
immédiat, qui est en fait une méthode appelée coerce_classes
. C’est parce que
Git n’est pas capable de reconnaître la syntaxe Ruby pour une définition de
méthode, qui ici serait def coerce_classes
.
Ainsi, ce que nous voulons vraiment voir, c’est :
@@ -270,7 +270,7 @@ def coerce_classes(from, to)
case
when from.class == Date
[coerce_date(from, to), to]
- when to.class == Date
+ when to.instance_of?(Date)
[from, coerce_date(to, from)]
else
[from, to.public_send("to_#{from.class.downcase}")]
Et il n’y a pas qu’avec Ruby où Git a du mal à trouver le bon contexte de clôture. De nombreux autres langages de programmation et formats de fichiers sont également perdants en ce qui concerne le contexte de l’en-tête.
Heureusement, il est non seulement possible de configurer une expression rationnelle personnalisée spécifique à votre langage pour aider Git à mieux s’orienter, mais il existe même un ensemble de modèles prédéfinis pour de nombreux langages et formats dans Git. Tout ce que nous avons à faire est de dire à Git quels modèles utiliser pour nos extensions de fichiers.
Nous pouvons le faire en définissant un fichier .gitattributes
à l’intérieur
de notre dépôt. Dans ce fichier, on associera les extensions de fichiers Ruby au
modèle de diff prédéfini pour Ruby :
*.gemspec diff=ruby
*.rake diff=ruby
*.rb diff=ruby
Certains projets open source définissent leur propre fichier .gitattributes
.
Il y en a un dans Rails. Il y en a même
un dans les sources Git qui défini les
modèles de diff pour Perl et Python.
Configurer un fichier .gitattributes global
Au lieu d’ajouter un fichier .gitattributes
à chaque dépôt, nous pouvons en
configurer global. Il suffit de créer ce fichier dans votre répertoire
personnel, de le remplir avec tous les formats de fichiers qui vous intéressent
et de le déclarer dans votre configuration globale de Git :
❯ git config --global core.attributesfile ~/.gitattributes
On pourrait se demander pourquoi, s’il existe des modèles de diff prédéfinis, doit-on les déclarer nous-même ? Ça semble être une étape superflue dont on se passerait bien !
Oui mais voilà, tout n’est pas si simple ! Si vous avez dans votre dépôt un
fichier *.c
, faut-il lui appliquer le modèle de diff du langage C ou de C++ ?
Si c’est un fichier *.m
, est-ce de l’Objective-C ou du Matlab ? Ah, vous
voyez, vous aussi vous êtes perdu ! Eh bien c’est parce qu’on ne peut pas se
fier à la seule extension d’un fichier pour définir le modèle à appliquer, que
les développeurs de Git ont décidé de ne pas arbitrer à notre place. Et si vous
voulez revivre cette intense discussion, vous la retrouverez sur ce fil.
Modèle de diff pour Rspec
Comme annoncé plus tôt, il est aussi possible de définir un modèle de diff personnalisé. Cela peut être utile pour Rspec par exemple, de manière à prendre en considération son propre DSL.
Cela se fait en deux étapes. Premièrement, nous allons ajouter une ligne
spécifique à nos fichiers de tests Rspec dans le fichier .gitattributes
:
*.rb diff=ruby
*._spec.rb diff=rspec
Ensuite, il nous faut définir ce nouveau modèle dans $GIT_DIR/config
au sein
de notre projet ou dans $HOME/.gitconifg
:
[diff "rspec"]
xfuncname = "^[ \t]*((RSpec|describe|context|it|before|after|around|feature|scenario)[ \t].*)$"
On utilise dans l’exemple ci-dessus l’instruction xfuncname
introduite plus
récemment que funcname
et qui,
contrairement à son ainée, supporte les expressions rationnelles étendues ; ce
qui n’est pas pour nous déplaire tant la syntaxe résultante s’en trouve épurée !
Et si vous n’êtes pas à l’aise avec les regex, on a quelques
articles sur le sujet ;)
Un contexte étendu
Ainsi configuré, une option méconnue de git diff
dévoile alors tout son
potentiel ; il s’agit de -W
ou --function-context
. C’est une option
similaire à celle de git grep
et qui élargit le contexte des hunks de
manière à montrer toute la fonction environnante. Ce contexte que l’on
qualifierait de « naturel » peut permettre de mieux comprendre les changements
effectués.
Si on reprend notre exemple, ça donnerait ceci :
❯ git diff -W
@@ -269,10 +269,10 @@ def zone(tm)
def coerce_classes(from, to)
case
when from.class == Date # not is_a?(Date), it will catch DateTime
[coerce_date(from, to), to]
- when to.class == Date
+ when to.instance_of?(Date)
[from, coerce_date(to, from)]
else
[from, to.public_send("to_#{from.class.downcase}")].then(&method(:coerce_zones))
end
end
Les plus attentifs d’entre vous auront repéré un petit couac…
À l’épreuve des balles ?
Pas tout à fait… il peut arriver qu’un faux se glisse dans les résultats. C’est notamment le cas, si l’on veut prendre un exemple, de cet extrait :
❯ git diff
@@ -268,9 +268,9 @@ def zone(tm)
def coerce_classes(from, to)
case
- when from.class == Date # not is_a?(Date), it will catch DateTime
+ when from.instance_of?(Date)
[coerce_date(from, to), to]
- when to.class == Date
+ when to.instance_of?(Date)
[from, coerce_date(to, from)]
else
[from, to.public_send("to_#{from.class.downcase}")]
On se serait attendu à voir affiché def coerce_classes(from, to)
ou class
Diff
plutôt que def zone(tm)
, puisque notre changement n’a pas lieu au sein
de cette méthode. On peut néanmoins obtenir le résultat escompté en jouant sur
le nombre de lignes présentées autour des lignes modifiées grâce à l’option -U
ou --unified
.
❯ git diff -U1
@@ -270,5 +270,5 @@ def coerce_classes(from, to)
case
- when from.class == Date # not is_a?(Date), it will catch DateTime
+ when from.instance_of?(Date)
[coerce_date(from, to), to]
- when to.class == Date
+ when to.instance_of?(Date)
[from, coerce_date(to, from)]
On comprend alors que Git recherche le contexte autour du hunk et non pas strictement autour des lignes modifiées. Malgré cela l’utilisation de modèles de diff reste néanmoins utile et pertinente dans la grande majorité des cas !
Ressources
- Git attributes
- Better Git diff output for Ruby, Python, Elixir, Go and more
- Git attributes examples
- Where does the excerpt in the git diff hunk header come from?