Automatiser avec les Git Hooks
Les hooks Git sont de petits scripts exécutés automatiquement par Git à des moments précis du cycle de vie. Ils « accrochent » des comportements utiles à votre flux : garde‑fous qualité, conventions, automatisations (lint, tests rapides, etc.).
En bref : votre première ligne de défense contre les « oups ». Il existe deux familles : côté client (sur votre machine) et côté serveur (sur le serveur Git). Ici, on se concentre sur le client - ceux que vous utiliserez au quotidien.
Hooks côté client : votre assistant personnel
Ces hooks vivent dans .git/hooks/ de votre dépôt local. Par défaut, Git y met des fichiers .sample. Pour activer un hook, supprimez l’extension .sample et rendez le script exécutable.
Partager, c’est soigner : versionnez vos hooks
Le dossier .git/ n’est pas versionné : vos beaux hooks ne seront pas partagés par défaut. La solution moderne : stocker vos hooks dans un dossier versionné (par exemple .githooks/ à la racine) et dire à Git de les chercher là.
Configuration pour un dépôt :
git config core.hooksPath .githooks/Ou globalement pour tous vos dépôts :
git config --global core.hooksPath ~/.githooks/Ensuite, vous pouvez committer vos hooks et les partager.
Hooks client utiles :
pre-commit: aprèsgit commitmais avant l’éditeur. Parfait pour lancer un linter/formatter ou des tests rapides. Un code de sortie non nul annule le commit.prepare-commit-msg: juste avant l’ouverture de l’éditeur ; permet d’ajouter/modifier automatiquement le message.commit-msg: après la fermeture de l’éditeur ; reçoit le message en paramètre - idéal pour valider le format (p. ex. Conventional Commits).post-commit: une fois le commit créé ; utile pour des notifications ou tâches de suivi.pre-push: avant d’envoyer vos commits ; dernière chance de lancer la suite de tests.
Passons à la pratique avec deux exemples concrets.
Exemple 1 : Linter avant de committer
Pire que d’attendre une CI en échec pour une babiole ? Cette hook pre-commit lance ESLint sur les fichiers JS/TS indexés et annule le commit en cas d’erreur.
Enregistrez sous pre-commit et rendez‑le exécutable (chmod +x pre-commit).
#!/bin/bash
set -e
# Fichiers JS/TS indexés
staged_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(js|jsx|ts|tsx)$' || true)
if [[ -z "$staged_files" ]]; then
echo "Aucun fichier JS/TS indexé, on saute ESLint."
exit 0
fi
echo "ESLint sur les fichiers indexés..."
# Lancer ESLint (lint only)
if ! npx eslint $staged_files; then
echo "ESLint a trouvé des erreurs. Corrigez avant de committer."
exit 1
fi
echo "ESLint OK."
exit 0Exemple 2 : Faire respecter les Conventional Commits
Cette hook commit-msg formate/valide automatiquement vos messages selon notre standard Conventional Commits.
- Devine intelligemment le type (
feat,fix, etc.) - À défaut, se base sur le nom de branche
- Récupère l’ID ticket depuis le nom de branche et l’ajoute en scope
Sauvegardez‑la en commit-msg dans votre dossier de hooks :
#!/bin/sh
# Couleurs
RED='\033[0;31m'
YELLOW='\033[0;33m'
BLUE='\033[0;36m'
NC='\033[0m'
has_warning=false
# Branche courante
branch_name=$(git symbolic-ref --short HEAD 2>/dev/null)
# Schéma de nommage de branche
branch_regex='^(task|bugfix|feature|hotfix|build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)\/([A-Za-z0-9]{1,6}-[0-9]{1,6})((_|-)(\w|-|_)*)?'
if [ -z "$branch_name" ]; then
echo -e "${YELLOW}WARNING: HEAD détachée - impossible d’inférer la branche/ID ticket${NC}"
exit 0
fi
if [ "$branch_name" = "main" ]; then
echo -e "${RED}ERROR: Vous êtes sur main. Basculez sur votre branche !${NC}"
exit 1
fi
if ! echo "$branch_name" | grep -qE "$branch_regex"; then
echo -e "${RED}ERROR: Le nom de branche ne respecte pas le schéma attendu.${NC}"
echo "Au minimum : feat/, feature/, bugfix/, fix/, build/, chore/, ci/, docs/, perf/, refactor/, revert/, style/ ou test/ + ID ticket."
echo "Vous pouvez ajouter une courte description après l’ID (tiret/underscore)."
exit 1
fi
# Regex Conventional Commits
commit_regex="^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\((([A-Za-z0-9]{1,6}-[0-9]{1,6})|((([0-9]+).([0-9]+).([0-9]+))))\))(!)?(:\s.*)?|^(Merge \w+)|(Initial commit$)|(Notes added by \'git notes add\')"
commit_msg_file=$1
commit_msg=$(cat "$commit_msg_file")
if echo "$commit_msg" | grep -qE "$commit_regex"; then
exit 0
fi
echo -e "${YELLOW}WARNING: Message non conforme à Conventional Commits${NC}"
branch_type=$(echo "$branch_name" | sed -nE "s/$branch_regex/\1/p")
ticket_id=$(echo "$branch_name" | sed -nE "s/$branch_regex/\2/p")
echo -e "${BLUE}Scope ($ticket_id) inféré depuis le nom de branche.${NC}"
existing_type=$(echo "$commit_msg" | grep -oE "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)")
description=$(echo "$commit_msg" | sed -E "s/^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|task|feature|bugfix|fix|hotfix)(\([0-9]{1,6}\))?:\s*//")
if [ -z "$existing_type" ]; then
prefix=$(echo "$commit_msg" | grep -oE "(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|task|feature|bugfix|fix|hotfix)" | head -n 1)
if [ -z "$prefix" ]; then prefix="$branch_type"; fi
case $prefix in
task|feature) prefix="feat" ;;
bugfix|fix|hotfix) prefix="fix" ;;
esac
echo -e "${BLUE}Type <$prefix> inféré depuis branche/texte.${NC}"
commit_msg="${prefix}(${ticket_id}): $description"
else
echo -e "${BLUE}Type <$existing_type> inféré depuis le message.${NC}"
commit_msg="${existing_type}(${ticket_id}): $description"
fi
if ! echo "$commit_msg" | grep -qE "$commit_regex"; then
echo -e "${RED}FATAL: impossible de corriger automatiquement le message${NC}"
exit 1
fi
echo -e "${BLUE}Nouveau message :${NC} $commit_msg"
echo "$commit_msg" > "$commit_msg_file"
echo -e "${BLUE}Vérifiez que le message correspond bien à votre intention.${NC}"
exit 0Ce hook suppose un format de ticket type Jira. Adaptez le groupe suivant dans vos regex ($branch_regex/$commit_regex) si besoin :
[A-Za-z0-9]{1,6}-[0-9]{1,6}Hooks côté serveur
Ces hooks tournent sur le serveur Git (droits admin requis). Sur des offres hébergées (GitHub/GitLab), ce n’est généralement pas disponible ; en self‑hosted, vous avez notamment :
- pre-receive : à la réception des données d’un push, avant enregistrement - idéal pour rejeter des commits non conformes
- update : comme
pre-receive, mais une fois par branche poussée - post-receive : après enregistrement - parfait pour des notifications… ou déclencher un pipeline 😄
Appliquer Conventional Commits côté serveur
À sauvegarder en pre-receive sur le serveur :
#!/bin/sh
# Rejeter les pushes contenant des commits dont le message
# n’adhère pas à Conventional Commits
set -e
zero_commit='0000000000000000000000000000000000000000'
msg_regex="^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\((([A-Za-z0-9]{1,6}-[0-9]{1,6})|((([0-9]+).([0-9]+).([0-9]+))))\))(!)?(:\s.*)?|^(Merge \w+)|(Initial commit$)|(Notes added by \'git notes add\')"
while read -r oldrev newrev refname; do
# Suppression de branche/tag : ignorer
[ "$newrev" = "$zero_commit" ] && continue
# Calcul de l’intervalle
[ "$oldrev" = "$zero_commit" ] && range="$newrev" || range="$oldrev..$newrev"
for commit in $(git rev-list "$range" --not --all); do
if ! git log --max-count=1 --format=%B $commit | grep -iqE "$msg_regex"; then
echo "ERROR:"
echo "ERROR: Push rejeté :"
echo "ERROR: $commit dans ${refname#refs/heads/}"
echo "ERROR: n’est pas conforme à Conventional Commits."
echo "ERROR: Corrigez le message et poussez à nouveau."
echo "ERROR: https://www.conventionalcommits.org/fr/v1.0.0/"
echo "ERROR"
exit 1
fi
done
done