Skip to content

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 :

shell
git config core.hooksPath .githooks/

Ou globalement pour tous vos dépôts :

shell
git config --global core.hooksPath ~/.githooks/

Ensuite, vous pouvez committer vos hooks et les partager.

Hooks client utiles :

  • pre-commit : après git commit mais 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).

bash
#!/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 0

Exemple 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 :

shell
#!/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 0

Ce hook suppose un format de ticket type Jira. Adaptez le groupe suivant dans vos regex ($branch_regex/$commit_regex) si besoin :

shell
[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 :

shell
#!/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