Git avance - 06 - Git hooks : automatiser avant chaque commit

Les git hooks pour automatiser le linting, les tests et la validation des commits. Pre-commit, commit-msg et les outils.

06 - Git hooks : automatiser avant chaque commit

Ce que tu vas apprendre

  • Ce que sont les git hooks et lesquels sont utiles
  • Husky et lefthook pour versionner tes hooks
  • lint-staged pour ne checker que ce qui a change
  • commitlint pour enforcer les conventional commits

Prerequisites


Le problème de la discipline

Tu as défini des regles : conventional commits, code formate avec Prettier, pas de console.log en prod. Ton équipe est d'accord. Tout le monde hoche la tête en meeting.

Deux semaines plus tard, la moitie des commits sont "update stuff" et le code est formate a moitie. Pas par mauvaise volonte. Par oubli, par pression, par habitude.

La discipline individuelle ne scale pas. L'automatisation, si.

Les git hooks

Git a un système de hooks intégré. Des scripts qui se lancent automatiquement a certains moments du workflow. Ils vivent dans .git/hooks/.

bashls .git/hooks/
# applypatch-msg.sample  post-update.sample     pre-push.sample
# commit-msg.sample      pre-applypatch.sample  pre-rebase.sample
# fsmonitor-watchman.sample  pre-commit.sample  prepare-commit-msg.sample

Les fichiers .sample sont des exemples desactives. Pour activer un hook, tu supprimes l'extension .sample et tu rends le fichier executable. Le hook se lance automatiquement.

Les hooks les plus utiles pour notre workflow :

  • pre-commit : se lance avant que le commit soit créé. Si le script retourne un code non-zero, le commit est annule. Parfait pour le linting et le formatage.
  • commit-msg : se lance apres que tu aies écrit ton message. Recoit le message en argument. Parfait pour valider le format conventional commits.
  • pre-push : se lance avant le push. Parfait pour lancer les tests.

Le problème du .git/hooks

Le dossier .git/hooks/ n'est pas versionne. C'est local a ta machine. Si tu créés un hook pre-commit genial, tes collegues ne l'auront pas. Et si tu leur dis de copier le fichier a la main... retour au problème de la discipline.

C'est pour ca qu'on utilise des outils qui versionnent les hooks.

Husky : la solution npm

Husky est l'outil le plus populaire. Il créé des hooks dans un dossier .husky/ qui est versionne dans git.

bashbun add -D husky
bunx husky init

Ca créé un dossier .husky/ avec un hook pre-commit par défaut. Tu le modifies :

bash# .husky/pre-commit
bun run lint-staged
bash# .husky/commit-msg
bunx commitlint --edit $1

Quand un collegue clone le repo et fait bun install, Husky s'installe automatiquement (via le script prepare dans package.json). Il n'a rien a faire.

lefthook : l'alternative rapide

lefthook est écrit en Go, donc pas de dépendance Node. Il est plus rapide que Husky et la config est en YAML.

bash# Installation
bun add -D lefthook
bunx lefthook install
yaml# lefthook.yml
pre-commit:
  parallel: true
  commands:
    lint:
      glob: "*.{ts,tsx}"
      run: bunx eslint {staged_files}
    format:
      glob: "*.{ts,tsx,json,css}"
      run: bunx prettier --check {staged_files}

commit-msg:
  commands:
    validate:
      run: bunx commitlint --edit {1}

L'avantage de lefthook : parallel: true lance le lint et le format en parallèle. Sur un gros projet, ca fait une différence notable. Et la config YAML est plus lisible qu'un script bash.

Pour mes projets perso, j'utilise Husky (plus de docs, plus d'exemples). Pour un projet d'équipe avec beaucoup de hooks, lefthook est le meilleur choix.

lint-staged : ne checker que le nécessaire

Lancer ESLint sur tout le projet a chaque commit, c'est lent. Si ton projet a 200 fichiers et que tu en as modifie 3, checker les 200 est du gaspillage.

lint-staged ne lance les commandes que sur les fichiers stages (ceux dans git add).

bashbun add -D lint-staged
json// package.json
{
  "lint-staged": {
    "*.{ts,tsx}": [
      "prettier --write",
      "eslint --fix"
    ],
    "*.css": [
      "prettier --write"
    ]
  }
}

Le hook pre-commit appelle lint-staged :

bash# .husky/pre-commit
bunx lint-staged

Quand tu fais git commit, lint-staged :

  1. Recupere la liste des fichiers stages
  2. Filtre par les patterns (*.ts, *.css)
  3. Lance les commandes sur ces fichiers uniquement
  4. Si une commande echoue (ESLint trouve une erreur), le commit est bloque
  5. Si tout passe, les fichiers reformates sont re-stages automatiquement

Résultat : le commit ne passe que si le code est propre. Et ca prend 2 secondes au lieu de 30.

commitlint en action

Tu as vu commitlint dans l'article sur les conventional commits. Voici comment le brancher avec un hook :

bashbun add -D @commitlint/cli @commitlint/config-conventional
json// .commitlintrc.json
{
  "extends": ["@commitlint/config-conventional"]
}
bash# .husky/commit-msg
bunx commitlint --edit $1

A partir de la, un commit avec un message mal formate est refuse :

bashgit commit -m "fixed the bug"
# commitlint: subject may not be empty
# commitlint: type may not be empty
# husky - commit-msg script failed (code 1)

git commit -m "fix(auth): handle expired token"
# OK

Mes juniors ont deteste ca la première semaine. La deuxieme semaine, ils tapaient les bons messages sans reflechir. La troisieme, ils m'ont remercie. L'habitude se créé par la contrainte, pas par la bonne volonte.

pre-push : les tests avant le push

Le hook pre-push lance des commandes avant que le push parte vers le remote. C'est l'endroit pour lancer les tests.

bash# .husky/pre-push
bun test

Attention : si ta suite de tests prend 5 minutes, chaque push prendra 5 minutes. Ca devient vite insupportable. Mon compromis :

  • pre-commit : lint + format (rapide, 2-3 secondes)
  • commit-msg : validation du message (instantane)
  • pre-push : tests unitaires rapides (< 30 secondes)
  • La suite de tests complète tourne dans la CI GitLab

Ne mets pas tout dans pre-commit. J'ai vu des équipes avec un pre-commit qui lance ESLint, Prettier, les tests unitaires, les tests d'intégration et le type-check. Résultat : 45 secondes par commit. Tout le monde finit par faire git commit --no-verify et les hooks ne servent plus a rien.

Ma config complète

Voici ce que j'utilise sur paltemps.fr et mes autres projets :

json// package.json (extraits pertinents)
{
  "scripts": {
    "prepare": "husky"
  },
  "devDependencies": {
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0",
    "@commitlint/cli": "^19.0.0",
    "@commitlint/config-conventional": "^19.0.0"
  },
  "lint-staged": {
    "*.{ts,tsx}": ["prettier --write", "eslint --fix"],
    "*.{json,css,md}": ["prettier --write"]
  }
}
bash# .husky/pre-commit
bunx lint-staged
bash# .husky/commit-msg
bunx commitlint --edit $1

Pas de hook pre-push pour l'instant. La CI rattrape ce que le local ne vérifié pas. C'est un équilibre entre rapidité locale et fiabilité CI.


Article précédent : 05 - Résoudre les conflits Article suivant : 07 - Reflog et recovery

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.