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 :
- Recupere la liste des fichiers stages
- Filtre par les patterns (
*.ts,*.css) - Lance les commandes sur ces fichiers uniquement
- Si une commande echoue (ESLint trouve une erreur), le commit est bloque
- 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