Що таке Git hooks?
Git hooks - це скрипти, які Git запускає автоматично в конкретних точках свого процесу: перед комітом, перед пушем, після злиття гілок.
Теорія
TL;DR
- Hooks - як паспортний контроль в аеропорту: Git зупиняється в конкретному місці, запускає твій скрипт, і якщо він повертає ненульовий код виходу, операція скасовується.
- Hooks зберігаються в
.git/hooks/і працюють тільки локально. Колеги їх не отримують при клонуванні. - Для локальних перевірок до 10 секунд (lint, тести) - hooks. Для обов'язкового enforcement в команді - CI/CD.
git commit --no-verifyобходить pre-commit hooks, тому вони не є рівнем безпеки.- Husky дозволяє версіонувати hooks разом з кодом і автоматично ділитися ними в команді.
Швидкий приклад
# .git/hooks/pre-commit
#!/bin/sh
# Запускається перед кожним комітом на цій машині
if ! npm run lint; then
echo "Lint провалився - виправ перед комітом"
exit 1 # Ненульовий код скасовує коміт
fiСпочатку зроби файл виконуваним: chmod +x .git/hooks/pre-commit. Без цього Git ігнорує файл без будь-якого попередження, і коміт проходить так, ніби hook взагалі не існує.
Як Git запускає hooks
Git шукає у .git/hooks/ виконуваний файл з назвою, що відповідає події - наприклад, pre-commit. При виконанні git commit Git запускає підпроцес shell. Код виходу 0 означає «продовжити», будь-що інше скасовує операцію. Hooks успадковують змінні середовища Git, зокрема $GIT_DIR.
Чотири основні фази:
- pre: запускається до дії (
pre-commit,pre-push) і може заблокувати її - during: змінює поточну операцію (
prepare-commit-msg,commit-msg) - post: запускається після завершення дії (
post-commit,post-merge) і нічого не може скасувати - server-side: на віддаленому сервері (
pre-receive,post-receive), потрібен доступ до сервера
Клієнтські vs серверні hooks
Клієнтські hooks лежать у .git/hooks/ на локальній машині. Серверні hooks знаходяться на самому remote-сервері, наприклад у self-hosted GitLab або Gitea. GitHub-репозиторії не дають прямого доступу до серверних hooks.
Важливий наслідок: будь-хто в команді може обійти клієнтський hook, видаливши файл або запустивши git commit --no-verify. Для справжнього enforcement використовуй GitHub Actions або CI-пайплайн.
Коли використовувати
- Pre-commit лінтинг: ловити проблеми зі стилем до того як вони потраплять в репо
- Pre-push тести: запускати тест-сьют перед пушем
- Валідація commit message: перевіряти формат conventional commits через
commit-msg - Post-merge оновлення залежностей: автоматично запускати
npm installпісля пулу, якщоpackage.jsonзмінився
Hooks не підходять для перевірок, що тривають більше 10 секунд, або для правил, які команда не повинна мати можливості обійти.
Ділимось hooks через Husky
Директорія .git/ не комітиться в репо. Тому hooks не передаються при клонуванні. Husky вирішує це, вказуючи Git на версіоновану директорію .husky/.
# Встановлення і ініціалізація Husky
npm install husky --save-dev
npx husky install # Створює директорію .husky/
# Додаємо pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"// .lintstagedrc.json
{
"*.{js,ts,jsx,tsx}": ["eslint --fix", "git add"]
}Husky v9 спрощує це ще більше: використовує git config core.hooksPath .husky/ напряму, замість npm lifecycle скриптів з v4.
Типові помилки
Забутий chmod +x:
# Файл існує, але не є виконуваним
.git/hooks/pre-commitGit ігнорує файл без жодного повідомлення про помилку. Коміт проходить, нічого не запускається, і ти витрачаєш час здогадуючись чому hook не працює. Виправлення: chmod +x .git/hooks/pre-commit. Це трапляється майже з кожним розробником вперше.
Хардкод шляхів:
#!/bin/sh
node_modules/.bin/eslint . # Зламається на свіжому клоніКраще: npx eslint . або npm run lint. Хардкодований шлях до node_modules падає одразу після git clone, коли залежності ще не встановлено.
Спроба скасувати дію в post-hook:
# post-commit
npm run notify-slack || exit 1 # exit 1 тут не має ефектуPost-hooks запускаються після того як операція вже виконана. Ненульовий код виходу там ні на що не впливає. Щоб заблокувати дію, використовуй відповідний pre-hook.
Очікування що hooks запустяться в CI: GitHub Actions клонує репо з нуля і не отримує локальних hooks. Hooks - це інструмент локальної розробки. Якщо хочеш ті ж самі перевірки в CI, налаштуй їх окремо у workflow-файлі.
Де зустрічається
- Husky + lint-staged (60k+ зірок на GitHub): запускає ESLint тільки на staged-файлах, стандарт у React і Next.js репо
- Lefthook: YAML-конфігурація, поширений у Ruby/Rails проектах на GitLab
- pre-commit.com framework: керування hooks для кількох мов, використовується в Airbnb і проекті dask
- commit-msg hook: перевірка формату conventional commits перед збереженням повідомлення
Follow-up питання
Q: Як обійти hook в екстреній ситуації?
A: git commit --no-verify. Це пропускає pre-commit і commit-msg hooks. Корисно для WIP-комітів або коли зламаний hook блокує всю команду.
Q: Яка різниця між pre-commit і commit-msg hooks?
A: pre-commit запускається до того як Git відкриває редактор для повідомлення. commit-msg запускається після того як ти написав повідомлення, але до збереження коміту. Для перевірки коду - pre-commit, для перевірки формату повідомлення - commit-msg.
Q: Чому hooks не запускаються в CI?
A: CI-runner клонує репо з нуля. .git/hooks/ існує тільки локально і не є частиною версійного контролю. Hooks є лише на тих машинах, де їх явно налаштували.
Q: Чим Husky v9 відрізняється від v4?
A: Husky v4 зберігав конфіг у package.json і встановлював hooks через npm lifecycle скрипти. v9 використовує git config core.hooksPath .husky/ напряму і вимагає явного виклику husky install. Менше магії, простіше налагодження.
Q: Якщо є і pre-commit, і prepare-commit-msg, який запускається першим?
A: Git запускає їх у фіксованому порядку: спочатку pre-commit, потім prepare-commit-msg, потім commit-msg, потім post-commit. Цей порядок не можна змінити. Якщо обидва hooks змінюють один і той же стан, потрібно враховувати послідовність вручну або використовувати Lefthook для координації.
Приклади
Базовий: лінтинг тільки staged файлів
#!/bin/sh
# .git/hooks/pre-commit (потрібен chmod +x)
# Перевіряє тільки staged JS файли, не весь проект
changed_files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.js$')
for file in $changed_files; do
npx eslint "$file" || exit 1
doneЦе швидше ніж eslint . на великих репо, бо перевіряє лише файли у поточному коміті. Якщо будь-який файл провалюється, цикл виходить з кодом 1 і Git скасовує коміт. На чистому проекті без staged JS файлів цикл виконується нуль разів і завершується з кодом 0.
Середній рівень: Husky + lint-staged в Next.js проекті
# .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged// .lintstagedrc.json
{
"*.{js,ts,jsx,tsx}": ["eslint --fix", "git add"],
"*.{css,scss}": ["stylelint --fix", "git add"]
}lint-staged автоматично виправляє проблеми і повторно додає виправлені файли в стейдж. Якщо файл не вдається виправити автоматично, коміт скасовується. Файл .husky/pre-commit відстежується Git, тому кожен розробник після npm install отримує ті ж самі перевірки завдяки скрипту prepare, який запускає husky install.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.