Hooks et automatisation#
Git ne se limite pas à un outil de suivi de versions : il offre également un mécanisme d”automatisation intégré grâce aux hooks. Les hooks sont des scripts qui s’exécutent automatiquement à des moments précis du workflow Git — avant un commit, après un push, lors d’un merge, etc. Ils permettent d’insérer des vérifications, des transformations ou des notifications à chaque etape clé du cycle de vie d’un dépôt.
L’intérêt des hooks est considérable dans un contexte professionnel. Ils permettent d’imposer des standards de qualité (formatage du code, absence de secrets dans les fichiers commités), de valider le format des messages de commit, d’empêcher les pushes vers des branches protégées, ou encore de déclencher automatiquement des pipelines de déploiement. Ce chapitre présente les différents types de hooks, leur mise en place, et les outils modernes qui facilitent leur gestion au sein d’une équipe.
Qu’est-ce qu’un hook ?#
Définition 56 (Hook Git)
Un hook est un script exécutable place dans le répertoire .git/hooks/ d’un dépôt Git. Git exécute automatiquement ce script lorsqu’un évènement spécifique se produit (par exemple, juste avant la création d’un commit, après un push, lors d’un checkout). Le nom du fichier détermine l’évènement auquel le hook est associe : pre-commit, post-commit, pre-push, etc. Si le hook retourne un code de sortie non nul (exit code != 0), l’opération Git associée est annulée (pour les hooks de type pre-).
Chaque dépôt Git est initialisé avec un ensemble de hooks d’exemple dans .git/hooks/. Ces fichiers portent l’extension .sample et ne sont donc pas actifs. Examinons leur contenu :
%%bash
cd /tmp && rm -rf demo-hooks && mkdir demo-hooks && cd demo-hooks && git init
git config user.email "demo@example.com" && git config user.name "Demo"
ls .git/hooks/
Dépôt Git vide initialisé dans /tmp/demo-hooks/.git/
applypatch-msg.sample
commit-msg.sample
fsmonitor-watchman.sample
post-update.sample
pre-applypatch.
sample
pre-commit.sample
pre-merge-commit.sample
prepare-commit-msg.sample
pre-push.sample
pre-rebas
e.sample
pre-receive.sample
push-to-checkout.sample
sendemail-validate.sample
update.sample
Chaque fichier .sample contient un script d’exemple commenté. Pour activer un hook, il suffit de créer un fichier exécutable portant le nom de l’évènement (sans l’extension .sample). Le script peut être écrit dans n’importe quel langage (Bash, Python, Perl, etc.) è condition de commencer par le shebang approprié (#!/bin/bash, #!/usr/bin/env python3, etc.).
Remarque 57
Les hooks résident dans le répertoire .git/hooks/, qui fait partie du répertoire .git/ — lequel n’est jamais versionné. Par conséquent, les hooks ne sont pas partagés automatiquement lorsqu’un collaborateur clone le dépôt. Pour distribuer des hooks au sein d’une équipe, plusieurs approches existent : utiliser un framework comme pre-commit ou husky, maintenir un répertoire scripts/hooks/ versionné dans le projet avec des instructions d’installation, ou configurer core.hooksPath pour pointer vers un répertoire versionné.
Hooks côté client#
Les hooks côté client s’exécutent sur la machine du développeur, dans le contexte de son dépôt local. Ils couvrent les opérations courantes : commit, push, checkout, merge, rebase.
pre-commit#
Définition 57 (Hook pre-commit)
Le hook pre-commit s’exécute avant la création d’un commit, après que l’utilisateur a lancé git commit mais avant que Git n’ouvre l’éditeur de message. Si le script retourne un code de sortie non nul, le commit est annulé. Ce hook est typiquement utilisé pour vérifier la qualité du code : linting, formatage automatique, détection de secrets ou de fichiers interdits dans la zone de staging.
prepare-commit-msg#
Définition 58 (Hook prepare-commit-msg)
Le hook prepare-commit-msg s’exécute après la création du message de commit par défaut, mais avant l’ouverture de l’éditeur. Il reçoit en argument le chemin du fichier contenant le message temporaire. Ce hook permet de pré-remplir le message de commit avec des informations contextuelles : numéro de ticket extrait du nom de branche, template standardisé, ou préfixe automatique.
commit-msg#
Définition 59 (Hook commit-msg)
Le hook commit-msg s’exécute après que l’utilisateur a rédigé son message de commit. Il reçoit en argument le chemin du fichier contenant le message. Si le script retourne un code non nul, le commit est annulé. Ce hook est idéal pour valider le format du message de commit — par exemple, vérifier qu’il respecte la convention Conventional Commits (feat:, fix:, docs:, etc.) ou qu’il contient une référence à un ticket.
pre-push#
Définition 60 (Hook pre-push)
Le hook pre-push s’exécute avant l’envoi des commits vers un dépôt distant, après la vérification des références distantes mais avant le transfert des objets. Il reçoit le nom et l’URL du remote en arguments, ainsi que les références à pousser sur l’entrée standard. Ce hook permet d’exécuter des tests avant le push, ou d’empêcher le push vers des branches protégées (par exemple, interdire un push direct sur main).
post-commit#
Définition 61 (Hook post-commit)
Le hook post-commit s’exécute après la création réussie d’un commit. Il ne reçoit aucun argument et ne peut pas annuler le commit (celui-ci est déjà enregistré). Ce hook est utilisé pour des actions de notification : afficher un message de confirmation, envoyer une alerte, mettre à jour un tableau de bord.
post-checkout#
Définition 62 (Hook post-checkout)
Le hook post-checkout s’exécute après un git checkout ou git switch réussi. Il reçoit trois arguments : le SHA du HEAD précédent, le SHA du nouveau HEAD, et un indicateur (1 si c’est un checkout de branche, 0 si c’est un checkout de fichier). Ce hook est utile pour réinstaller les dépendances si le fichier package-lock.json ou requirements.txt a changé entre les deux branches, ou pour nettoyer des fichiers générés.
Démonstration : un hook pre-commit en action#
Exemple 17 (Hook pre-commit : interdire les commentaires TODO)
Créons un hook pre-commit qui empêche de commiter des fichiers contenant le mot TODO. Ce type de vérification garantit que les marqueurs de travail temporaire ne se retrouvent pas dans l’historique du projet.
Préparons le dépòt et créons le hook :
%%bash
cd /tmp && rm -rf demo-hooks && mkdir demo-hooks && cd demo-hooks && git init
git config user.email "demo@example.com" && git config user.name "Demo"
# Créer le hook pre-commit
cat > .git/hooks/pre-commit << 'HOOK'
#!/bin/bash
# Hook pre-commit : interdit les commentaires TODO dans les fichiers stagés
files=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$files" ]; then
exit 0
fi
if echo "$files" | xargs grep -l "TODO" 2>/dev/null; then
echo "ERREUR : des commentaires TODO ont été trouvés dans les fichiers stagés."
echo "Veuillez les résoudre avant de commiter."
exit 1
fi
exit 0
HOOK
chmod +x .git/hooks/pre-commit
echo "Hook pre-commit installé."
ls -la .git/hooks/pre-commit
Dépôt Git vide initialisé dans /tmp/demo-hooks/.git/
Hook pre-commit installé.
-rwxr-xr-x 1 loc loc 405 15 mars 21:25 .git/hooks/pre-commit
Créons d’abord un fichier propre et vérifions que le commit fonctionne normalement :
%%bash
cd /tmp/demo-hooks
# Un fichier sans TODO : le commit doit réussir
echo "def calculer(x):" > module.py
echo " return x * 2" >> module.py
git add module.py
git commit -m "Ajout du module de calcul"
echo ""
echo "=== Commit réussi ==="
git log --oneline
[main (commit racine) 93bd90c] Ajout du module de calcul
1 file changed, 2 insertions(+)
create mo
de 100644 module.py
=== Commit réussi ===
93bd90c Ajout du module de calcul
Maintenant, ajoutons un fichier contenant un commentaire TODO :
%%bash
cd /tmp/demo-hooks
# Un fichier avec TODO : le commit doit être rejeté
echo "def traiter(data):" > traitement.py
echo " # TODO : implémenter la validation" >> traitement.py
echo " return data" >> traitement.py
git add traitement.py
git commit -m "Ajout du traitement" 2>&1 || true
echo ""
echo "=== Le commit a été bloqué par le hook ==="
git log --oneline
traitement.py
ERREUR : des commentaires TODO ont été trouvés dans les fichiers stagés.
Veuillez les résoudre
avant de commiter.
=== Le commit a été bloqué par le hook ===
93bd90c Ajout du module de calcul
Le hook a détecté le commentaire TODO dans traitement.py et a empéché la création du commit. L’historique ne contient toujours qu’un seul commit. Pour procéder, le développeur doit soit résoudre le TODO, soit contourner le hook avec git commit --no-verify (à utiliser avec parcimonie).
%%bash
cd /tmp/demo-hooks
# Corriger le fichier en supprimant le TODO
echo "def traiter(data):" > traitement.py
echo " if not data:" >> traitement.py
echo " raise ValueError('Données invalides')" >> traitement.py
echo " return data" >> traitement.py
git add traitement.py
git commit -m "Ajout du traitement avec validation"
echo ""
echo "=== Commit réussi après correction ==="
git log --oneline
[main ffdf00f] Ajout du traitement avec validation
1 file changed, 4 insertions(+)
create mode 100
644 traitement.py
=== Commit réussi après correction ===
ffdf00f Ajout du traitement avec validation
93bd90c Ajout du module de calcul
Démonstration : un hook commit-msg#
Illustrons maintenant un hook commit-msg qui impose le format Conventional Commits :
%%bash
cd /tmp/demo-hooks
# Créer le hook commit-msg
cat > .git/hooks/commit-msg << 'HOOK'
#!/bin/bash
# Hook commit-msg : vérifie le format Conventional Commits
MSG=$(cat "$1")
PATTERN="^(feat|fix|docs|style|refactor|test|chore|ci|perf)(\(.+\))?: .{1,}"
if ! echo "$MSG" | grep -qE "$PATTERN"; then
echo "ERREUR : le message de commit ne respecte pas le format Conventional Commits."
echo "Format attendu : <type>(<scope>): <description>"
echo "Types valides : feat, fix, docs, style, refactor, test, chore, ci, perf"
echo "Message recu : $MSG"
exit 1
fi
exit 0
HOOK
chmod +x .git/hooks/commit-msg
# Tentative avec un message invalide
echo "nouvelle ligne" >> module.py
git add module.py
git commit -m "modification du module" 2>&1 || true
echo ""
echo "--- Le message invalide a été rejeté ---"
ERREUR : le message de commit ne respecte pas le format Conventional Commits.
Format attendu : <type
>(<scope>): <description>
Types valides : feat, fix, docs, style, refactor, test, chore, ci, perf
Me
ssage recu : modification du module
--- Le message invalide a été rejeté ---
%%bash
cd /tmp/demo-hooks
# Tentative avec un message valide
git commit -m "feat(calcul): ajouter le support des nombres négatifs"
echo ""
echo "=== Commit accepté avec message Conventional Commits ==="
git log --oneline
[main 4ec6519] feat(calcul): ajouter le support des nombres négatifs
1 file changed, 1 insertion(+
)
=== Commit accepté avec message Conventional Commits ===
4ec6519 feat(calcul): ajouter le support des nombres négatifs
ffdf00f Ajout du traitement avec validation
93bd90c Ajout du module de calcul
Remarque 58
L’option git commit --no-verify (ou -n) permet de contourner les hooks pre-commit et commit-msg. Cette option doit être réservée à des situations exceptionnelles (corrections urgentes, travail en cours temporaire). Dans un workflow d’équipe, il est préférable de configurer les hooks pour qu’ils soient suffisamment rapides et pertinents afin que personne ne soit tente de les désactiver.
Hooks côté serveur#
Les hooks côté serveur s’exécutent sur le dépôt distant (le serveur hébergeant le dépôt). Ils permettent d’appliquer des politiques centralisées que les développeurs ne peuvent pas contourner.
pre-receive#
Définition 63 (Hook pre-receive)
Le hook pre-receive s’exécute sur le serveur avant d’accepter un push. Il reçoit sur l’entrée standard la liste des références mises à jour (ancienne valeur, nouvelle valeur, nom de la référence). Si le script retourne un code non nul, l’intégralité du push est rejetée. Ce hook est utilisé pour imposer des règles globales : interdire les pushes forcés, vérifier la signature des commits, refuser les commits trop volumineux.
update#
Définition 64 (Hook update)
Le hook update est similaire à pre-receive, mais il s’exécute une fois par branche mise à jour lors du push. Il reçoit trois arguments : le nom de la référence, l’ancien SHA, et le nouveau SHA. Contrairement à pre-receive, il permet de rejeter sélectivement certaines branches tout en acceptant les autres dans le même push.
post-receive#
Définition 65 (Hook post-receive)
Le hook post-receive s’exécute sur le serveur après l’acceptation complète du push. Il reçoit les mêmes informations que pre-receive sur l’entrée standard. Ce hook ne peut pas annuler le push (les données sont déjà intégrées). Il est typiquement utilisé pour déclencher des actions automatisées : lancement d’un pipeline CI/CD, envoi de notifications (email, Slack), mise à jour d’un déployement automatique, synchronisation avec d’autres dépôts.
Remarque 59
Dans la pratique, les hooks côté serveur sont rarement écrits manuellement. Les plateformes d’hébergement (GitHub, GitLab, Bitbucket) proposent des mécanismes équivalents via leur interface : branch protection rules sur GitHub, push rules et server hooks sur GitLab, webhooks pour déclencher des actions externes. Ces mécanismes sont plus simples à configurer, à maintenir et à auditer que des scripts personnalisés sur le serveur.
Le framework pre-commit#
La gestion manuelle des hooks pose plusieurs problèmes : les hooks ne sont pas versionnés, chaque développeur doit les installer manuellement, et leur maintenance est fastidieuse. Le framework pre-commit résout ces difficultés.
Définition 66 (Framework pre-commit)
pre-commit est un outil Python (installable via pip install pre-commit) qui gère l’installation et l’exécution de hooks Git à partir d’un fichier de configuration versionné dans le dépôt : .pre-commit-config.yaml. Il s’appuie sur un vaste écosystème de hooks pré-écrits (linting, formatage, vérification de sécurité) et garantit que tous les membres de l’équipe utilisent les mêmes vérifications, dans les mêmes versions.
La configuration se fait via un fichier .pre-commit-config.yaml à la racine du projet :
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 24.1.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
Ce fichier déclare trois sources de hooks :
pre-commit-hooks : une collection de vérifications génériques (espaces en fin de ligne, fichiers YAML valides, fichiers volumineux).
black : le formateur de code Python, qui reformate automatiquement les fichiers stagés.
flake8 : un linter Python qui détecte les erreurs de style et les problèmes potentiels.
L’installation se fait en deux commandes :
pip install pre-commit
pre-commit install
La commande pre-commit install crée un hook .git/hooks/pre-commit qui délègue l’exécution au framework. Désormais, à chaque git commit, les hooks définis dans .pre-commit-config.yaml s’exécutent automatiquement sur les fichiers stagés.
Remarque 60
Le framework pre-commit n’exécute les vérifications que sur les fichiers effectivement stagés (git add), et non sur l’ensemble du dépôt. Cette approche le rend rapide même dans les grands projets : seuls les fichiers modifiés sont analysés. Pour exécuter les hooks sur l’ensemble du dépôt (par exemple dans un pipeline CI), on utilise la commande pre-commit run --all-files.
Exemple 18 (Workflow typique avec pre-commit)
Voici le déroulement d’un commit dans un projet utilisant pre-commit :
Le développeur modifie des fichiers et les ajoute à la zone de staging avec
git add.Il lance
git commit -m "feat: ajouter la validation".Le framework
pre-commitintercepte l’opération et exécute chaque hook sur les fichiers stagés.Si black reformate un fichier, le hook échoue et le développeur doit re-stager le fichier reformaté puis relancer le commit.
Si flake8 détecte une erreur, le hook échoue et le développeur doit corriger le problème.
Une fois tous les hooks satisfaits, le commit est crée normalement.
Ce mécanisme garantit que chaque commit respecte les standards du projet, sans effort supplémentaire de la part du développeur.
Visualisation : déclenchement des hooks dans le workflow Git#
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlim(-0.5, 14.5)
ax.set_ylim(-0.5, 7.5)
ax.axis('off')
ax.set_title("Déclenchement des hooks dans le workflow Git", fontsize=14, fontweight='bold', pad=15)
# Couleurs
c_action = '#2c3e50'
c_hook_pre = '#e74c3c'
c_hook_post = '#27ae60'
c_step = '#2980b9'
# Etapes du workflow (de gauche à droite)
steps = [
(1.0, 5.5, "git add", c_step),
(4.0, 5.5, "git commit", c_step),
(8.5, 5.5, "Commit cree", c_step),
(12.0, 5.5, "git push", c_step),
]
for (x, y, label, color) in steps:
box = patches.FancyBboxPatch(
(x - 0.9, y - 0.4), 1.8, 0.8,
boxstyle="round,pad=0.15", linewidth=2,
edgecolor=color, facecolor=color, alpha=0.15
)
ax.add_patch(box)
border = patches.FancyBboxPatch(
(x - 0.9, y - 0.4), 1.8, 0.8,
boxstyle="round,pad=0.15", linewidth=2,
edgecolor=color, facecolor='none'
)
ax.add_patch(border)
ax.text(x, y, label, ha='center', va='center',
fontsize=11, fontweight='bold', color=color)
# Flèches entre étapes
arrow_kw = dict(arrowstyle='->', color=c_action, lw=2)
ax.annotate('', xy=(3.1, 5.5), xytext=(1.9, 5.5), arrowprops=arrow_kw)
ax.annotate('', xy=(7.6, 5.5), xytext=(5.8, 5.5), arrowprops=arrow_kw)
ax.annotate('', xy=(11.1, 5.5), xytext=(9.4, 5.5), arrowprops=arrow_kw)
# Hooks pre-commit (entre git commit et commit créé)
hooks_pre = [
(5.5, 3.8, "pre-commit"),
(7.0, 3.8, "prepare-\ncommit-msg"),
(8.5, 3.8, "commit-msg"),
]
for (x, y, label) in hooks_pre:
box = patches.FancyBboxPatch(
(x - 0.65, y - 0.45), 1.3, 0.9,
boxstyle="round,pad=0.1", linewidth=1.5,
edgecolor=c_hook_pre, facecolor=c_hook_pre, alpha=0.12
)
ax.add_patch(box)
border = patches.FancyBboxPatch(
(x - 0.65, y - 0.45), 1.3, 0.9,
boxstyle="round,pad=0.1", linewidth=1.5,
edgecolor=c_hook_pre, facecolor='none'
)
ax.add_patch(border)
ax.text(x, y, label, ha='center', va='center',
fontsize=8, fontweight='bold', color=c_hook_pre)
# Flèche de git commit vers hooks
ax.annotate('', xy=(5.5, 4.25), xytext=(5.0, 5.1),
arrowprops=dict(arrowstyle='->', color=c_hook_pre, lw=1.5, linestyle='--'))
# Flèches entre hooks pre
ax.annotate('', xy=(6.35, 3.8), xytext=(6.15, 3.8), arrowprops=arrow_kw)
ax.annotate('', xy=(7.85, 3.8), xytext=(7.65, 3.8), arrowprops=arrow_kw)
# Label "Peuvent bloquer"
ax.text(7.0, 2.7, "Peuvent bloquer le commit\n(exit code != 0)",
ha='center', va='center', fontsize=9, fontstyle='italic', color=c_hook_pre,
bbox=dict(boxstyle='round,pad=0.3', facecolor='#fadbd8', edgecolor=c_hook_pre, alpha=0.8))
# Hook post-commit
ax.text(9.5, 4.2, "post-commit", ha='center', va='center',
fontsize=8, fontweight='bold', color=c_hook_post,
bbox=dict(boxstyle='round,pad=0.2', facecolor='#d5f5e3', edgecolor=c_hook_post))
ax.annotate('', xy=(9.5, 4.5), xytext=(9.2, 5.1),
arrowprops=dict(arrowstyle='->', color=c_hook_post, lw=1.5, linestyle='--'))
# Hook pre-push (avant push)
ax.text(12.0, 3.8, "pre-push", ha='center', va='center',
fontsize=9, fontweight='bold', color=c_hook_pre,
bbox=dict(boxstyle='round,pad=0.2', facecolor='#fadbd8', edgecolor=c_hook_pre))
ax.annotate('', xy=(12.0, 4.1), xytext=(12.0, 5.1),
arrowprops=dict(arrowstyle='->', color=c_hook_pre, lw=1.5, linestyle='--'))
# Légende
legend_pre = patches.Patch(facecolor='#fadbd8', edgecolor=c_hook_pre, label='Hooks bloquants (pre-*)')
legend_post = patches.Patch(facecolor='#d5f5e3', edgecolor=c_hook_post, label='Hooks informatifs (post-*)')
legend_step = patches.Patch(facecolor=c_step, edgecolor=c_step, alpha=0.15, label='Operations Git')
ax.legend(handles=[legend_pre, legend_post, legend_step], loc='lower left',
fontsize=9, framealpha=0.9)
plt.show()
Résumé#
Le tableau suivant récapitule les hooks les plus courants, leur point de déclenchement et leurs usages typiques :
Hook |
Déclenchement |
Bloquant |
Usages typiques |
|---|---|---|---|
|
Avant la création du commit |
Oui |
Linting, formatage, détection de secrets |
|
Après le message par défaut, avant l’éditeur |
Oui |
Ajout automatique de numéro de ticket |
|
Après la rédaction du message |
Oui |
Validation du format (Conventional Commits) |
|
Après la création du commit |
Non |
Notifications, statistiques |
|
Avant l’envoi vers le remote |
Oui |
Tests, protection de branches |
|
Après un checkout ou switch |
Non |
Installation de dépendances |
|
Serveur, avant d’accepter le push |
Oui |
Politiques centralisées, rejets |
|
Serveur, une fois par branche |
Oui |
Règles par branche |
|
Serveur, après acceptation du push |
Non |
CI/CD, déploiement, notifications |
Remarque 61
Les hooks constituent un pilier de l”intégration continue locale. En combinant des hooks pre-commit (via le framework pre-commit) avec des hooks commit-msg pour le format des messages, on obtient un filet de sécurité qui détecte les problèmes au plus tôt, avant même qu’ils n’atteignent le dépôt distant. Cela réduit le bruit dans les revues de code et accélère le cycle de développement. Les hooks côté serveur complètent ce dispositif en imposant des règles que les développeurs ne peuvent pas contourner, même en utilisant --no-verify.