Première rédaction de cet article le 26 septembre 2011
Dernière mise à jour le 27 septembre 2011
Ceux qui utilisent un VCS pour stocker des documents structurés (par exemple du XML) ont souvent le problème d'éditeurs trop intelligents qui reformattent brutalement le document, lui conservant son sens sous-jacent (encore heureux !) mais changeant complètement la représentation textuelle. Les VCS typiques, conçus pour du texte, croient alors que le document est différent et annonçent des changements qui ne sont pas réellement importants. Une des solutions à ce problème est de canonicaliser les documents et de demander au VCS de vérifier que cette canonicalisation a bien été faite. Comment on fait avec le VCS Subversion ?
Le problème n'est pas spécifique avec XML. Si une équipe de
développeurs C utilise
indent (ou le vieux logiciel cb) pour
pretty-printer leur code,
mais qu'ils ont mis des options différentes dans leur
~/.indent.pro
(au passage, voici le mien), chaque
commit va engendrer des diff
énormes mais non significatifs. Mais le problème est plus fréquent
avec XML, où les auteurs utilisent rarement des éditeurs de
texte et passent en général par un logiciel spécialisé,
qui n'hésite pas à changer le fichier considérablement. C'est
typiquement le cas avec le format SVG, qu'on
édite rarement à la main. Si un membre de l'équipe utilise
Inkscape et l'autre
Sodipodi, les diffs entre
leurs commits ne seront guère utilisables. C'est un
problème que connait le projet CNP3,
enregistré dans la bogue #24. Ce problème a
aussi été discuté sur
StackOverflow.
Bien sûr, le problème n'est pas purement technique. Il y a aussi une part de politique : mettre d'accord les développeurs, fixer des règles. Mais la technique permet-elle ensuite de s'assurer qu'elles sont respectées ?
Avec le VCS Subversion, oui, c'est possible,
et c'est même documenté. Le
principe est d'avoir un script dit pre-commit
,
que le serveur Subversion exécutera avant le
commit et qui, si le code de
retour indique une erreur, avortera le
commit. Voici un exemple, avec un
pre-commit
qui teste qu'on soumet du
XML bien formé :
% svn commit -m 'Bad XML' bad Sending bad Transmitting file data .svn: Commit failed (details follow): svn: Commit blocked by pre-commit hook (exit code 1) with output: /tmp/bad:1: parser error : Opening and ending tag mismatch: x line 1 and y <x a="3">toto</y> ^
Vous pouvez voir ce script en pre-commit-check-xml-well-formed.sh
. Il est très inspiré du
script d'exemple (très bien documenté) fourni avec Subversion. Il utilise
xmllint pour déterminer si le fichier XML est
bien formé. Sinon, xmllint se termine avec un code de retour différent
de zéro, et, à cause de set -e
, le script entier
se termine sur une erreur, ce qui empêche le commit
d'un fichier incorrect.
Si le dépôt Subversion est en
/path/example/repository
, le script doit être
déposé dans le sous-répertoire hooks/
du dépôt,
sous le nom de pre-commit
. Il doit être
exécutable (autrement, vous verrez une mystérieuse erreur sans
message, avec un code de retour de 255).
Plus ambitieux, un script qui teste que le XML soumis est sous
forme canonique (cette forme est normalisée dans un texte du W3C). Pour cela, il va canonicaliser le fichier
soumis (avec xmllint --c14n
) et, si cela donne un
fichier différent de celui soumis, c'est que ce dernier n'était pas
sous forme canonique. Le commit est alors rejeté :
% svn commit -m 'Not canonical XML' bad Sending bad Transmitting file data .svn: Commit failed (details follow): svn: Commit blocked by pre-commit hook (exit code 3) with output: File "bad" is not canonical XML
Voici un exemple du travail de xmllint pour canonicaliser du XML :
% cat complique.xml <?xml version="1.0" encoding="utf-8"?> <toto > <truc a="1" >Machin C </truc >café</toto> % xmllint --c14n complique.xml <toto> <truc a="1">Machin C </truc>café</toto>
Le script de pre-commit
est en pre-commit-check-xml-canonical.sh
.
On peut se dire qu'il serait plus simple que le script canonicalise le fichier, qu'il l'était avant ou pas, épargnant ainsi ce travail aux auteurs. Mais Subversion ne permet pas de modifier un fichier lors d'un commit (cela impliquerait de changer la copie de travail de l'utilisateur). On ne peut qu'accepter ou refuser le commit.
Rien n'interdit d'utiliser le même principe avec du code, par
exemple en C. On peut envisager de tester si le
code est « propre » en le testant avec splint
dans le pre-commit
. Ou bien on peut voir si le
code C a été correctement formaté en le reformatant avec
indent et les bonnes options, et voir ainsi s'il
était correct.
Et avec les DVCS ? Par leur nature même, ils rendent plus difficile la vérification du respect de règles analogues à celles-ci. Je ne connais pas de moyen de faire ces vérifications avec les DVCS existants. Il faudrait le faire lors d'un push/pull car un dépôt n'a aucune raison de faire confiance aux vérifications faites lors du commit par un autre dépôt. J'ai inclus à la fin de cet article des explications fournies par des lecteurs, pour d'autres VCS. Il me manque encore git. Un volontaire ?
Enfin, tout ceci ne résout pas tous les problèmes, par exemple pour l'édition SVG citée plus haut : les éditeurs SVG comme Inkscape aiment bien rajouter aussi des éléments XML à eux et, là, le problème du diff est plus délicat. Des outils comme Scour deviennent alors nécessaires, pour nettoyer le SVG.
D'autres exemples de scripts pre-commit
sont
fournis avec Subversion en http://svn.apache.org/repos/asf/subversion/trunk/contrib/hook-scripts/
. Parmi
eux, on note un programme
écrit en Python (rien n'oblige les scripts
pre-commit
à être en
shell), qui vérifie un certain nombre de
propriétés (configurées dans son enforcer.conf
)
sur des sources Java.
Pour le cas du VCS distribué darcs, Gabriel Kerneis propose : « On peut créer un patch qui définit un test. Si les autres développeurs appliquent ce patch, alors darcs exécutera automatiquement le test à chaque fois qu'ils voudront enregistrer un nouveau patch :
darcs setpref test 'autoconf && ./configure && make && make test' darcs record -m "Add a pre-record test"
Mais comme vous le notez, cela ne prémunit pas d'un envoi de patch qui ne passe pas le test. Sur le répertoire central de référence (à supposer qu'il y en ait un), ou chacun sur son propre répertoire de travail, peut aussi ajouter un hook qui vérifiera que les patchs sont corrects avant de les appliquer :
# à vérifier que si le prehook échoue, le apply échoue bien - je ne # l'ai jamais utilisé en pratique echo 'apply prehook make test' >> _darcs/prefs/defaults echo 'apply run-prehook' >> _darcs/prefs/defaults
Il est même possible d'effectuer la canonicalisation à l'aide de
prehook
:
echo 'record prehook canonicalize.sh' >> _darcs/prefs/defaults echo 'record run-prehook'
Voir les détails des options prehook (et posthook). Malheureusement (ou heureusement ?), il n'est pas possible de distribuer les prehooks et les posthooks sous forme de patch, comme avec setpref. Mais on peut toujours ajouter un test avec setpref qui vérifie si les prehooks sont présents et échoue sinon... »
Pour Mercurial, André Sintzoff explique :
« Il y a aussi des hooks qui sont notamment décrits
dans
la documentation.
Le precommit hook permet de contrôler avant le commit comme avec Subversion
Comme tu le fais remarquer, c'est local à l'utilisateur donc pas
obligatoire.
La solution est donc de se baser sur les hooks changegroup
et
incoming
qui contrôlent l'arrivée de nouveaux changesets dans un
dépôt via pull ou push.
Il suffit donc de mettre un hook changegroup
avec la vérification qui va bien.
Bien entendu, il vaut mieux pour l'utilisateur d'utiliser un precommit
hook pour contrôler sur son dépôt local. »
Enfin, pour git, le bon point de départ
semble être http://book.git-scm.com/5_git_hooks.html
.
Merci beaucoup à Gabriel Kerneis et André Sintzoff pour leurs contributions.
Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)
Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)