Automatisez votre flux de travail : un guide pratique des hooks côté client Git

Utilisez les hooks côté client Git pour des vérifications locales rapides, une configuration partagée, des règles de message de commit et une automatisation post-merge plus sûre.

Automatisez votre flux de travail : un guide pratique des hooks côté client Git

Les hooks côté client Git sont de petits scripts qui s'exécutent sur votre machine lorsque Git atteint certains points d'un flux de travail. Un hook pre-commit s'exécute avant la création d'un commit. Un hook commit-msg s'exécute après que vous ayez écrit le message mais avant que Git ne l'accepte. Un hook post-merge s'exécute après la fin d'une fusion. Bien utilisés, les hooks détectent rapidement les erreurs ennuyeuses : formatage oublié, fichiers générés cassés, installations de dépendances manquantes ou messages de commit qui ne respectent pas la convention de votre équipe.

La limitation importante est que les hooks côté client sont locaux. Ils ne voyagent pas automatiquement avec le dépôt lorsque quelqu'un le clone. Cela les rend excellents pour un retour rapide et une commodité locale, mais faibles comme seule couche d'application pour une règle d'équipe. Si une vérification protège vraiment la branche principale, mettez-la aussi dans l'IC ou une règle côté serveur.

Chaque dépôt a un répertoire hooks sous .git/hooks :

ls .git/hooks

Un nouveau dépôt contient généralement des fichiers d'exemple tels que pre-commit.sample. Un hook d'exemple ne fait rien tant que vous ne créez pas un fichier exécutable sans le suffixe .sample :

cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Les hooks peuvent être des scripts shell, des scripts Python, des scripts Ruby, des scripts Node, ou tout ce que votre machine peut exécuter. La première ligne doit pointer vers l'interpréteur :

#!/usr/bin/env bash

Pour la plupart des équipes, le meilleur modèle à long terme n'est pas de modifier manuellement .git/hooks sur chaque ordinateur portable. Stockez les scripts de hook dans le dépôt, puis configurez Git pour utiliser ce répertoire :

git config core.hooksPath .githooks
mkdir -p .githooks

Maintenant, un hook à .githooks/pre-commit peut être commité et révisé comme du code de projet normal. Chaque développeur a toujours besoin du paramètre core.hooksPath, mais la configuration peut être ajoutée à un script d'amorçage ou documentée dans l'intégration.

Un Hook Pre-Commit Utile

Un bon hook pre-commit doit être rapide et ciblé. S'il prend deux minutes à chaque commit, les gens le contourneront avec git commit --no-verify, et le hook deviendra du bruit. Réservez les suites de tests complètes pour l'IC, sauf si le projet est suffisamment petit pour qu'elles soient vraiment rapides.

Voici un hook shell pratique qui vérifie uniquement les fichiers stagés. Cette distinction est importante. Vous pouvez avoir du travail non terminé dans votre arbre de travail que vous ne voulez pas encore tester. Le commit doit être jugé sur ce qui est stagé.

Créez .githooks/pre-commit :

#!/usr/bin/env bash
set -u

changed_files=$(git diff --cached --name-only --diff-filter=ACMR)

if [ -z "$changed_files" ]; then
  exit 0
fi

if git diff --cached --check; then
  :
else
  echo "Corrigez les erreurs d'espacement avant de commiter."
  exit 1
fi

secret_matches=$(git diff --cached --name-only --diff-filter=ACMR | xargs grep -nE 'AKIA[0-9A-Z]{16}|BEGIN RSA PRIVATE KEY' 2>/dev/null || true)
if [ -n "$secret_matches" ]; then
  echo "Secret possible trouvé dans les fichiers stagés :"
  echo "$secret_matches"
  exit 1
fi

python_files=$(printf '%s\n' "$changed_files" | grep '\.py$' || true)
if [ -n "$python_files" ]; then
  printf '%s\n' "$python_files" | while IFS= read -r file; do
    [ -f "$file" ] || continue
    python3 -m py_compile "$file" || exit 1
  done
fi

exit 0

Ce hook fait trois choses modestes : il laisse Git détecter les erreurs d'espacement, vérifie les fichiers stagés pour quelques motifs de secret évidents, et compile les fichiers Python modifiés. Ce n'est pas un remplacement pour un véritable scanner de secrets ou une suite de tests. C'est un fil d'alarme rapide.

Une erreur courante est d'utiliser grep sur les noms de fichiers au lieu du contenu des fichiers. Ce motif cassé vérifie seulement si le chemin contient TODO, pas si le fichier le contient :

git diff --cached --name-only | grep TODO

Si vous voulez bloquer les commentaires TODO, inspectez le diff stagé à la place :

if git diff --cached -U0 | grep -E '^\+.*TODO:'; then
  echo "Commentaires TODO stagés trouvés."
  exit 1
fi

Même alors, soyez prudent. Certaines équipes utilisent les commentaires TODO de manière responsable. Bloquer chaque TODO peut être plus ennuyeux qu'utile.

Hooks de Message de Commit

Un hook commit-msg reçoit le chemin du fichier de message de commit temporaire comme premier argument. Cela le rend utile pour des règles comme "chaque commit doit commencer par un ID de ticket" ou "utiliser les Conventional Commits."

Un petit exemple :

#!/usr/bin/env bash
set -u

message_file="$1"
first_line=$(head -n 1 "$message_file")

if printf '%s' "$first_line" | grep -Eq '^(feat|fix|docs|test|refactor|chore)(\(.+\))?: .+'; then
  exit 0
fi

echo "Le message de commit devrait ressembler à : fix(api): handle empty token"
exit 1

Ceci est utile lorsque les notes de version ou les changelogs sont générés à partir des commits. C'est moins utile lorsque votre équipe fait des squash merges et réécrit les titres des PR de toute façon. Adaptez le hook au flux de travail que vous utilisez réellement.

Hooks Post-Merge

Un hook post-merge est idéal pour le nettoyage local après que votre arbre de travail a changé. L'exemple classique est l'actualisation des dépendances après qu'un fichier de verrouillage a changé.

#!/usr/bin/env bash
set -u

previous_head="HEAD@{1}"

if git diff --name-only "$previous_head" HEAD | grep -Eq '(^package-lock\.json$|^pnpm-lock\.yaml$|^yarn\.lock$)'; then
  if command -v npm >/dev/null 2>&1 && [ -f package-lock.json ]; then
    echo "Fichier de verrouillage modifié ; exécution de npm install."
    npm install
  fi
fi

if git diff --name-only "$previous_head" HEAD | grep -q '^\.gitmodules$'; then
  echo "Configuration des sous-modules modifiée ; synchronisation des sous-modules."
  git submodule sync --recursive
  git submodule update --init --recursive
fi

Ce hook ne devrait pas faire de changements surprenants. S'il installe des dépendances, affichez ce qu'il fait. Si l'installation échoue, dites au développeur comment récupérer. Un hook qui modifie silencieusement l'arbre de travail est difficile à faire confiance.

Partager les Hooks Sans Faire de Désordre

Il existe trois façons courantes de partager les hooks.

La plus simple est core.hooksPath, où le dépôt contient .githooks/ et la configuration définit Git pour l'utiliser. C'est transparent et ne nécessite pas d'autre gestionnaire de paquets.

Les projets JavaScript utilisent souvent Husky car il s'intègre avec les flux d'installation npm, pnpm ou yarn. Cela peut être un bon choix lorsque chaque contributeur utilise déjà la chaîne d'outils Node.

De nombreuses équipes multilingues utilisent le framework pre-commit. Il installe et exécute les hooks définis dans .pre-commit-config.yaml, avec des versions épinglées pour des outils tels que les formateurs, les linters et les vérifications de fichiers. Cela ajoute un autre outil, mais cela résout le problème "comment installer les mêmes hooks partout ?" mieux qu'une page wiki.

Ce que j'évite, c'est de copier manuellement de gros scripts dans .git/hooks. Personne ne les révise, personne ne sait quelle version est installée, et le débogage devient une archéologie personnelle.

Déboguer les Hooks

Lorsqu'un hook ne s'exécute pas, vérifiez dans cet ordre :

git config --get core.hooksPath
ls -l .git/hooks .githooks 2>/dev/null

Si core.hooksPath est défini, Git ignore .git/hooks et utilise le répertoire configuré. Si le fichier hook n'est pas exécutable sur macOS ou Linux, Git ne l'exécutera pas :

chmod +x .githooks/pre-commit

Lorsqu'un hook s'exécute mais échoue mystérieusement, ajoutez un traçage temporaire :

set -x
pwd
env | sort

Les hooks s'exécutent depuis la racine du dépôt dans une utilisation normale de Git, mais les clients GUI et les IDE peuvent exposer des différences de chemin ou d'environnement. Utilisez command -v toolname à l'intérieur du hook avant de supposer qu'un linter ou un gestionnaire de paquets est disponible.

N'oubliez pas non plus le commutateur de contournement :

git commit --no-verify

Ce n'est pas une faille de sécurité en soi ; c'est ainsi que Git fonctionne. C'est une autre raison pour laquelle une application sérieuse appartient à l'IC ou aux règles de branche protégée.

Une Politique de Hook Sensée

Utilisez les hooks pour des vérifications rapides, déterministes et faciles à expliquer. Formater les fichiers stagés, détecter les erreurs d'espacement, valider les messages de commit et rappeler aux développeurs d'installer les dépendances sont de bons candidats. Évitez les hooks qui nécessitent un accès réseau, prennent beaucoup de temps ou dépendent d'un état local fragile.

Si un hook bloque un commit, son message doit dire exactement ce qui a échoué et comment le corriger. "Hook échoué" ne suffit pas. Un développeur en pleine fusion ou correction urgente a besoin d'une commande claire pour la suite.

Les hooks côté client Git fonctionnent mieux lorsqu'ils ressemblent à une barrière de sécurité utile plutôt qu'à une bureaucratie locale. Gardez-les petits, gardez-les versionnés, et gardez l'autorité finale dans l'IC.

Gardez les Hooks Amicaux Pendant les Urgences

Les hooks doivent aider pendant le travail normal sans piéger quelqu'un lors d'une correction urgente. Cela signifie que chaque hook bloquant a besoin d'un message d'échec clair et d'une porte de sortie réaliste. Git fournit déjà --no-verify pour les hooks de commit et de push, mais votre équipe doit toujours décider quand le contournement est acceptable. Une correction urgente de production est différente du saut de formatage parce qu'un développeur est pressé.

Un bon message de hook dit ce qui a échoué, où cela a échoué et quoi exécuter ensuite :

echo "ESLint a échoué sur les fichiers JavaScript stagés."
echo "Exécutez : npm run lint -- --fix"
exit 1

Un mauvais message dit seulement échoué ou déverse des pages de sortie d'outil sans contexte. Les gens apprennent à ignorer ce genre de hook.

Si le hook modifie des fichiers, soyez particulièrement prudent. Les formateurs peuvent être utiles dans pre-commit, mais ils peuvent aussi créer de la confusion lorsqu'ils modifient des parties non stagées d'un fichier. De nombreuses équipes préfèrent vérifier le formatage dans le hook et laisser le développeur exécuter le formateur manuellement. D'autres utilisent des outils qui formatent uniquement les hunks stagés. Choisissez un comportement et documentez-le dans le dépôt, pas dans un fil de discussion qui disparaît.

Pour les équipes, révisez les changements de hook comme du code d'application. Un hook peut ralentir chaque commit, divulguer des détails d'environnement dans les journaux ou casser les contributeurs sur Windows s'il suppose un comportement Bash uniquement. Si votre projet a des contributeurs Windows, testez les hooks dans Git Bash ou utilisez un exécuteur de hook multiplateforme. Si votre projet a des conteneurs ou des shells de développement, envisagez d'exécuter les hooks dans le même environnement que l'application afin que tout le monde utilise les mêmes versions d'outils.

Les meilleurs hooks sont presque invisibles quand tout va bien et très spécifiques quand quelque chose ne va pas. C'est la norme à viser.

Versionnez les Hooks Comme du Code de Produit

Un script de hook fait partie de l'expérience développeur. S'il casse, chaque contributeur le ressent. Gardez les scripts petits, nommez les fonctions d'aide clairement et évitez les astuces shell astucieuses quand une commande simple ferait l'affaire. Si un hook dépasse un écran ou deux, déplacez la logique réelle dans un script de projet testé et laissez le hook appeler ce script.

Par exemple, au lieu d'intégrer une longue routine de lint dans .githooks/pre-commit, appelez :

./scripts/check-staged-files.sh

Ce script peut être exécuté par les développeurs, les hooks et l'IC. Cela signifie également qu'un développeur peut reproduire l'échec sans faire semblant de faire un commit. La reproductibilité est la différence entre un hook utile et un obstacle local mystérieux.

Épinglez les versions d'outils lorsque vous le pouvez. Un hook qui appelle quel que soit black, eslint ou prettier qui se trouve en premier dans PATH peut se comporter différemment selon les machines. Les dépendances locales au projet, les fichiers de verrouillage, les conteneurs ou les gestionnaires de versions rendent la sortie du hook plus prévisible.

Enfin, gardez les hooks limités au dépôt. Les hooks globaux semblent pratiques, mais ils vous surprennent souvent des mois plus tard lorsqu'un dépôt non lié commence à échouer à cause d'une ancienne règle personnelle. Utilisez les hooks globaux uniquement pour des préférences vraiment personnelles, pas pour une politique d'équipe.

Une dernière règle pratique : ne laissez jamais les hooks être le seul endroit où une commande existe. Si le hook vérifie les fichiers Python stagés, gardez cette commande aussi dans un script ou un exécuteur de tâches. Les développeurs devraient pouvoir exécuter la même vérification intentionnellement, avant que Git ne les interrompe.

Pour les projets open-source, supposez que les contributeurs n'ont peut-être pas encore votre chaîne d'outils complète installée. Un hook qui échoue avec un message de configuration amical est acceptable. Un hook qui lance une trace de pile à partir d'un binaire local manquant semble cassé. Vérifiez les prérequis avant d'exécuter des commandes plus lourdes et dirigez les gens vers la commande de configuration utilisée par le projet.

Pensez aussi aux commits partiels. De nombreux développeurs expérimentés ne stagent qu'une partie d'un fichier. Les hooks qui formatent tout le fichier peuvent accidentellement tirer du travail non stagé dans le commit. Si votre équipe utilise souvent des commits partiels, préférez les vérifications qui lisent le diff stagé ou les outils conçus pour le contenu stagé.

Si un hook est constamment contourné, traitez cela comme un retour. Soit la vérification est trop lente, le message d'échec n'est pas clair, soit la règle appartient à l'IC plutôt qu'au chemin de commit local. Corrigez la friction plutôt que de blâmer les développeurs pour avoir utilisé le contournement que Git fournit.