Git Hooks ou la revanche du crochet Git
Par Delicious Insights • Publié le 14 avr. 2017

Cet article est également disponible en anglais.

Au-delà des commandes et du cycle de gestion usuel des révisions, Git nous permet des traitements complémentaires à certaines commandes afin d'épauler les utilisateurs.

Cet article est un complément à la documentation officielle et à la page de manuel.

Le principe

L’objectif des hooks est simple : il s’agit de fournir des enrobages à certaines commandes pour permettre l’injection de scripts (Bash, Node.js, Perl, Python, PHP…). De cette manière nous pouvons automatiser et donc fiabiliser une partie du travail côté client (machine de l’utilisateur) mais aussi côté serveur.

  • Ils sont "par défaut" présents dans chaque répertoire projet dans le sous-répertoire .git/hooks.
  • Leur nom est conventionnel et chaque fichier de script doit être exécutable (chmod +x .git/hooks/…).
  • Leur emplacement fait qu’ils peuvent être supprimés ou désactivés par l’utilisateur sur un poste local.

Ainsi les hooks locaux n’ont pas une vocation de contrôle, mais plutôt de soutien.

À cela s’ajoute le fait qu’un utilisateur peut demander à ce que certains hooks ne soient pas déclenchés, avec l’option --no-verify (pre-commit et commit-msg seulement).

Git fournit à chaque initialisation de projet un ensemble de scripts de démonstration possédant l’extension .sample (ex. : .git/hooks/pre-commit.sample). Pour les activer, il vous suffit de retirer cette extension (ex. : .git/hooks/pre-commit).

Bloquant … ou pas !

Un hook peut être bloquant, c’est-à-dire qu’il peut interrompre la commande qu’il enrobe.

Par convention, tout hook avant commande pre-[commande] est bloquant. Comme décrit précédemment, certains sont cependant contournables en utilisant l’option --no-verify : pre-commit et commit-msg.

Les seuls vraiment bloquants sont :

  • Côté client : prepare-commit-msg, pre-rebase, pre-apply-patch, pre-push, pre-auto-gc
  • Côté serveur : pre-receive, update

Le code de sortie des scripts bloquants est analysé pour savoir si oui on non le traitement doit être interrompu. On ne réinvente pas la roue, on utilise les codes de sorties standards qu’on peut résumer ici à :

  • 0 (zéro) : tout est OK, on ne bloque pas,
  • ≥1 : erreur, on bloque le traitement.

Par exemple, si nous produisons une sortie exit 1 dans un script de pre-commit alors le commit ne sera pas effectué.

Voici un exemple concret d’utilisation :

Un script bloquant tout commit tant que

  • des marqueurs de conflits persistent,
  • des chaînes de caractères TODO ou FIXME sont detectée.
#! /bin/bash
# Si vous rencontrez une erreur du type `declare: -A: invalid option`
# c’est qu’il vous faut mettre à jour votre version bash à v4.
# Pour Mac OS, regardez ici : http://clubmate.fi/upgrade-to-bash-4-in-mac-os-x/

# Hash utilisant la clé comme expression de recherche (Regex) et la valeur
# associée comme message d’erreur
declare -A PATTERNS;
PATTERNS['^[<>|=]{4,}']="Vous avez des marqueurs de conflits qui traînent";
PATTERNS['TODO|FIXME']="Vous avez des tâches non terminées (FIXME/TODO)";

# Déclare un tableau d’erreurs vide
declare -a errors;

# Boucle sur les noms de fichiers présents dans le `stage`/`index` et
# vérifie que leur contenu contient les chaînes listées dans PATTERNS.
# Filtre uniquement sur les fichiers ajoutés (A), copiés (C), modifiés (M).
for file in $(git diff --staged --name-only --diff-filter=ACM); do
  for elem in ${!PATTERNS[*]} ; do
    { git show :0:"$file" | grep -Eq ${elem}; } || continue;
    errors+=("${PATTERNS[${elem}]} in ${file}…");
  done
done

# Affiche les erreurs dans la console.
# Utilise la synthèse vocale si disponible pour énoncer les messages.
author=$(git config --get user.name)
for error in "${errors[@]}"; do
  echo -e "\[\033[1;31m\]${error}\[\033[0m\]"
  # Seulement sur Mac OS : synthèse vocale
  which -s say && say -v Samantha -r 250 "$author $error"
done

# S’il existe au moins une erreur, arrête la création du commit
if [ ${#errors[@]} -ne 0 ]; then
  exit 1
fi

Côté client/machine locale

Il s’agit des hooks disponibles uniquement sur les machines des utilisateurs.
Ceux-ci ne sont pas partagés au sein d’un projet, et il n’existe malheureusement à l’heure actuelle aucune option de type hooks.shared=true permettant ce comportement.

Deux alternatives permettent néanmoins de produire en partie ce comportement :

Initialisation du dépôt via un gabarit de projet

Lorsque vous initialisez votre dépôt projet, vous pouvez dire à Git d’utiliser un gabarit d'arborescence de projet type grâce à l'option --template=<template directory> :

  • Lors de la création du projet : git init --template=<template directory>
  • Lors du clone : git clone --template=<template directory>

Cette approche vous permet de définir plusieurs gabarits et de charger celui qui vous intéresse à l’initialisation ou à la récupération d’un projet.

Utilisation d’un répertoire déporté de hooks

Depuis Git 2.9 on peut définir par projet ou au global un paramétre de configuration permettant de spécifier le chemin du répertoire contenant les hooks : core.hooksPath=<hooks directory>.

Ainsi on peut gérer un projet de hooks centralisé et partagé et aussi gérer communément les hooks nécessaires à une équipe. On limite la maintenance et les erreurs, et on économise le temps que nous devions passer précedemment à copier/coller ceux-ci !

Les hooks disponibles sont les suivants :

  • Autour des commits :
    • pre-commit : avant la création du commit, avant même l’édition du message (ex. : linting, tests unitaires courts) ;
    • prepare-commit-msg : avant la création du commit, au moment où l’on va avoir la main sur l’édition du message (ex. : permet l’injection d’un message pré-calculé) ;
    • commit-msg : juste avant la création du commit, mais après l’édition du message (ex. : contrôle du contenu du message et réécriture à la volée) ;
    • post-commit : une fois le commit créé (ex. : notification) ;
  • Autour de l’application d’un patch (hooks lancés autour de la commande git am) :
    • applypatch-msg : avant l’application du patch (vérification du message de patch) ;
    • pre-applypatch : après l’application du patch, mais avant la création du commit (ex. : validation du contenu du patch appliqué) ;
    • post-applypatch : une fois le patch appliqué et le commit créé (ex. : notification de l’auteur du patch) ;
  • Autres opérations :
    • pre-rebase : avant le démarrage de la commande git rebase (ex. : interdire le rebase de la branche master ou de commits déjà poussés sur le serveur) ;
    • post-checkout : après l’exécution d’un git checkout et ses appels implicites, par exemple lors d'un rebase ou à la fin d’un git clone (ex. : paramétrage de l’environnement de travail associé, énoncé de la branche lors du déplacement sur une branche) ;
    • post-merge : après l’exécuton réussie de git merge (ex. : vérification de la présence de marqueurs de conflits suite à une mauvaise fusion) ;
    • post-rewrite : invoqué par des commande de "ré-écriture" des révisions (git commit --amend, git rebase) ;
    • pre-auto-gc : lors de l’appel automatique au garbage collector (permet donc d’intervenir avant la suppression de certains objets et certaines références) ;
    • pre-push : intervient juste avant l’envoi de nouvelles révisions et/ou objets vers un serveur (ex. : lancement des tests unitaires et validation/invalidation de l’envoi).

Coté serveur

Nous n’avons pas toujours la main sur les hooks côté serveur.

En particulier si vous utilisez une solution graphique de serveur Git en mode SaaS (ex. : GitHub, GitLab, BitBucket) vous n’aurez pas la main sur votre serveur et devrez passer par des systèmes d’API ou de plugins.

Dans les autres cas de figures (vous avez la main sur le serveur), vous pouvez insérer vos propres scripts comme bon vous semble.

Les hooks disponibles sont les suivants :

  • pre-receive : avant réception des références/objets (ex. : vérification des droits de l’utilisateurs sur le projet) ;
  • update : avant réception des références/objets par branche (ex. : vérification des droits par branche) ;
  • post-update : après réception des références par branche (ex. : notification d’utilisateurs tiers pour effectuer une revue de code) ;
  • post-receive : après réception de l’ensemble des références (ex. : lancement de l’intégration continues/tests d’intégration, déploiement automatique).

Préférez post-receive, plus complet que post-update : il possède les anciennes références et options liées au push (notamment utilisé chez GitHub pour les notifications).

Schéma classique de hooks

Voici un schéma illustrant les hooks les plus fréquemment utilisés et leur cycle d'intervention entre client et serveur.

Schéma classique des hooks

Exemple de workflow intégrant les hooks

Dans un contexte de création de révisions du code source d’un logiciel, nous souhaitons que certaines règles soient respectées :

  • Qualité des commits :
    • La syntaxe du code doit respecter des conventions (code linting) : pre-commit ;
    • Les modifications ne doivent pas contenir de marqueurs de conflits (fusion mal traitée) : pre-commit ;
    • Les tests unitaires ne doivent pas remonter d’erreur : pre-commit ;
    • Les messages de commits doivent suivre une convention particulière : prepare-commit-msg, commit-msg ;
  • Respect des droits sur dépôts et branches : un utilisateur ne doit pas pouvoir publier ses révisions sur un dépôt ou une branche sur lesquels il n’a pas les droits : update ;
  • Chaîne de build :
    • Chaque branche, pour pouvoir être fusionnée, doit être soumise à :
      • des validations automatiques (CI/Intégration Continue) : update ;
      • des validations de non-régression (ex. : analyse du pourcentage minimum attendu de couverture de test) : update ;
      • des validations manuelles (revue de code, tests fonctionnels) : non automatique, mais semblable à du post-receive ;
    • Le déploiement sur des environnements stables ("intégration", "production") doit être partiellement ou totalement automatisé : post-update ou post-receive.

En résumé…

Vous l’aurez compris, les hooks sont un mécanisme périphérique de Git vous ouvrant les portes de certaines automatisations.

De nombreux outils sont disponibles en "surcouche" et vous évitent de devoir gérer vos scripts manuellement (principalement côté serveur).

Soyez donc curieux et cherchez ce que proposent vos outils pour vous permettre de gagner en temps et en qualité sur vos projets.

En savoir plus…

Pour vous aider à aller plus loin, voici quelques liens vers des compléments de documentation et outils :