Les hooks Git

formation-git

Git est un système de versionnage bien pratique pour plusieurs raisons. Les plus communes sont probablement la souplesse avec laquelle il est possible de cloner et repartager n'importe quel dépôt, sa notion de branches très fonctionnelle grâce aux commandes avancées que la solution propose (commits interactifs, rebase, stash et bien-sûr : merge), ou simplement le fait qu'il s'agisse d'une des technologies la plus populaire, donc la plus supportée. Une raison secondaire, mais potentiellement aussi puissante réside dans son système de hooks (« crochets », en français) autorisant le couplage avec des applications externes.

Concrètement, ce procédé nous permet d'automatiser des tâches annexes au versionnage, mais dépendant de celui-ci. Par exemple, un outil de revue de code susceptible de rejeter un commit s'il ne respecte pas certains standards, ou encore de procéder à un déploiement quand une branche évolue.

D'un point de vue technique, cela fonctionne comme le patron de conception « observeur ». C'est-à-dire que nous avons Git qui émet des signaux à certains stades de l'avancement de son exécution. À nous de nous y greffer pour lancer des tâches annexes.

Avant de lister les crochets disponibles, il reste à comprendre la différence d'usage entre ceux sur le serveur, et les autres côté client. Ces premiers sont les seuls par lesquels le passage est imposé, car l'utilisateur aura toujours la possibilité de modifier ou de désactiver les hooks à son niveau. De plus, ils ne sont pas transférés en clonant le dépôt, ce qui rend leurs partages plus fastidieux. Ces derniers sont donc à réserver aux cas où le développeur veut, localement, procéder à un couplage. C'est au serveur qu'on devra mettre en place les procédures automatisées de déploiement ou de revue de code propre à l'intégralité du projet.

Une fois cette distinction faite, nous pouvons à présent passer dans le listing des crochets disponibles :

Côté client :

  • Pre-commit : revue automatique du code à commiter.
  • Prepare-commit-msg : génération du message de commit.
  • Commit-msg : validation du message de commit.
  • Post-commit : notifications.
  • Applypatch-msg : validation du message du patch.
  • Pre-applypatch : validation du contenu d'un patch.
  • Post-applypatch : notification de l'application d'un patch.
  • Pre-rebase : avant d'effectuer un rebase.
  • Post-checkout : après avoir changé de branche.
  • Post-merge : après avoir mergé.

 

Côté serveur :

  • Pre-receive : valide ou non la poussée : vérification de la qualité du code, que l'utilisateur ne modifie pas de fichiers interdits, etc.
  • Post-receive : notification par emails, au logiciel d'intégration continue, ou encore mise à jour du bug tracker, etc.
  • Update : équivalent au crochet post-receive, mais avec une segmentation par branche.

Ceux-ci sont présents dans le répertoire .git/hooks de votre dépôt de travail, et directement dans hooks pour un dépôt bare. Ils consistent simplement en une série de fichiers shell ayant le même intitulé que le hook qu'il représente. Par défaut, des exemples sont fournis. Il suffit de retirer le .sample au nom pour les activer.

Voyons à présent comment implémenter ces choses.

Hook côté client

Pour nos tests, nous allons concevoir un petit script vérifiant que nous effectuons bien un retour à la ligne entre l'accolade ouvrante d'une classe ou d'une fonction, et sa signature.

Voici le code à commiter :

<?php
function helloWord () {
    echo ”Hello word !” ;
}

La création de cette révision devrait donc être empêchée. Comment mettre en place ce contrôle ? La première étape consiste à définir au niveau de quel hook se greffer. Vous avez trouvé ? Il s'agit du pre-commit, car c'est le contenu même que nous voulons mettre à l'épreuve. Il nous reste plus qu'à écrire le script de test :

#! /bin/sh
set −e
modif=‘git diff −−cached ‘

printf %s ”$modif ” | while IFS= read −r line; do
    if [[ $line =~ ˆ\+.∗(function | class).∗\{ ]] ; then
        echo ” !reject : $line ”
        echo ” !reject : jump a line for the brace! ”
        exit 1
    fi
done

exit 0

La première chose à constater dans ce script se situe dans les codes de retour : 0 est l'unique valeur signalant à Git de continuer l'exécution. Les autres stopperont directement la création du commit. Nous avons le même comportement pour tous les hooks préfixés par « pre ».

Il nous reste plus qu'à tester :

$ git add test.php
$ git status -s
  M test.php
$ git commit -m "My new function."
  !reject: +function helloWord() {
  !reject: jump a line for the brace!
$ git status -s
  M test.php
$ git commit -m "My new function." --no-verify
  [master 2d29740] My new function.
  1 file changed, 1 insertion(+),
  1 deletion(-)
$ git status -s
$

Nous apercevons le rejet par Git ainsi que l'affichage des retours de la commande echo présente dans notre script shell. L'exécution semble donc adéquate. 🙂 On notera aussi qu'il est bien facile de passer au travers du filet via l'option --no-verify, ce qui confirme bien que le seul objectif ici est d'aider le développeur, et non de le restreindre. Ce comportement nous permet de réaliser très rapidement des hooks comme celui ci-dessus, qui sera fonctionnel presque partout, mais qui risquera tout de même de poser problème dans quelques rares cas, par exemple le jour ou l'on voudra publier un one-liner, ou une fonction anonyme en JavaScript. Dans cette circonstance, il nous suffira de le sauter plutôt que de perdre des heures pour le rendre compatible avec la subtilité du moment.

Hook côté serveur

Essayons à présent de vérifier le retour à la ligne avant l'accolade ouvrante au niveau du serveur. Une version incomplète ressemblerait à cela :

#! /bin/sh
set −e
validation () {
    newcontent=‘git show $2 ‘
    printf %s ”$newcontent” | while IFS= read −r line; do
        if [[ $line =~ ˆ\+.∗(function | class).∗\{ ]]; then
            echo ”!reject: $line ”
            echo ”!reject: jump a line for the brace!”
            exit 1
        fi
    done
}

while read oldrev newrev refname ; do
    validation $oldrev $newrev $refname
done

exit 0

C'est un peu plus complexe, car ce crochet possède plus de données à traiter que précédemment. Je vous invite à consulter la documentation disponible sur Internet pour comprendre le fonctionnement en détail, mais sachez que cet exemple est incomplet : il ne vérifie que les plus jeunes enfants des branches poussées. En effet, si plusieurs révisions sont envoyées, seule la dernière sera effectivement testée. Nous nous contenterons de ça pour ce cas d'école.

Avec le même fichier PHP que précédemment, voici la « conversation » avec Git qui en retourne :

$ git commit -m "My commit." --no-verify
  [master ee73c01] My commit.
  1 file changed, 1 insertion(+), 2 deletions(-)
$ git push
  Décompte des objets: 3, fait.
  Delta compression using up to 4 threads.
  Compression des objets: 100% (3/3), fait.
  Ecriture des objets: 100% (3/3), 349 bytes | 0 bytes/s, fait.
  Total 3 (delta 0), reused 0 (delta 0)
  remote: !reject: +function helloWord() {
  remote: !reject: jump a line for the brace!
  To /tmp/tests/bare
  ! [remote rejected] master -> master (pre-receive hook declined)
  error: impossible de pousser des références vers ’/tmp/tests/bare’
$

Il nous sera impossible de forcer l'acceptation de ce commit par le serveur.

Cette petite introduction sur les hooks Git est à présent terminée. J'espère qu'elle vous inspirera pour vos flux de travaux, en les rendant plus solides et plus simples à gérer !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *