Хуки git — скрипты, охраняющие твой процесс
Хуки git — скрипты в .git/hooks, срабатывающие на событиях: pre-commit (линт), commit-msg (формат сообщения), pre-push (тесты) и серверный pre-receive. Ненулевой код выхода прерывает операцию. Хуки не коммитятся, поэтому команды делят их через husky или lefthook.
У каждой команды есть правило, которое нарушают: «запускай линтер перед коммитом», «тесты должны проходить перед push», «сообщения коммитов следуют конвенции». Напоминать об этом на код-ревью уже поздно — плохой коммит уже существует. У git есть встроенный способ навязать эти правила ровно в тот момент, когда они важны: хуки — скрипты, которые git автоматически запускает вокруг коммитов, push и слияний. Хук, выходящий с ненулевым кодом, наглухо останавливает операцию.
К концу этого урока ты сможешь назвать ключевые клиентские хуки, написать pre-commit-хук и объяснить, почему команды берут husky или lefthook, чтобы делить хуки, которые сам git не коммитит.
После этого урока ты сможешь объяснить, что такое хук git, положить скрипт в .git/hooks, назвать роли pre-commit, commit-msg и pre-push, объяснить, почему хуки по умолчанию не разделяются, и описать, как это решают husky/lefthook и core.hooksPath.
Хук — это скрипт, который git запускает автоматически, когда происходит определённое событие. Хуки живут в .git/hooks. Git кладёт туда примеры файлов с суффиксом .sample (отключены); убери суффикс и сделай файл исполняемым, чтобы активировать хук:
ls .git/hooks # pre-commit.sample, commit-msg.sample, pre-push.sample, ...
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit # должен быть исполняемым, чтобы запуститьсяСкрипт может быть любым исполняемым — shell, Node, Python — лишь бы у него было правильное имя и корректный shebang.
Ненулевой код выхода прерывает операцию — это и есть весь механизм принуждения. Git запускает хук и проверяет его код выхода. Выход 0 означает «продолжай»; любой ненулевой выход означает «стоп, не выполняй это действие». pre-commit, который находит ошибку линтинга и выходит с 1, отменяет коммит до того, как тот создан:
#!/bin/sh
# .git/hooks/pre-commit — заблокировать коммит, если линтинг провалился
npm run lint || {
echo "Линтинг провалился — коммит прерван. Исправь ошибки и попробуй снова." >&2
exit 1
}Поэтому хук — это шлюз, а не просто уведомление: плохой коммит вообще не появляется на свет.
Три клиентских хука, которые ты будешь использовать чаще всего. Они срабатывают в разных точках потока коммита/push:
pre-commit— выполняется перед созданием коммита. Используй для линта, форматирования или быстрых проверок. Прерывание здесь означает, что ничего не закоммичено.commit-msg— получает путь к файлу сообщения; используй для навязывания формата вроде Conventional Commits (feat:,fix:). Отклоняй неправильные сообщения.pre-push— выполняется перед отправкой коммитов на удалённый репозиторий. Используй для более медленных шлюзов вроде набора тестов, чтобы сломанный код никогда не попадал в общую ветку.
#!/bin/sh
# .git/hooks/commit-msg — требовать префикс Conventional Commits
grep -qE '^(feat|fix|docs|refactor|test|chore)(\(.+\))?: ' "$1" || {
echo "Сообщение коммита должно начинаться с типа, напр. 'feat: ...'." >&2
exit 1
}▸Почему это работает
Зачем и pre-commit, и pre-push? Они меняют скорость на тщательность. pre-commit запускается на каждом коммите, поэтому должен быть быстрым — линтинг нескольких staged-файлов занимает секунду. pre-push запускается куда реже (только когда ты реально делишься работой), поэтому может позволить себе медленный, но решающий шлюз вроде полного набора тестов. Поставить набор тестов в pre-commit сделало бы каждый коммит мучительным и подтолкнуло бы всех его обходить; разделение быстрых/медленных проверок по двум событиям держит каждую терпимой.
Хуки живут в .git, поэтому по умолчанию они НЕ коммитятся и не разделяются. Каталог .git/hooks — это часть метаданных твоего локального репозитория, а не отслеживаемых файлов — клонирование репозитория не приносит его хуки. Это значит, что хук, который ты настроил, защищает только твою машину; коллега, клонирующий проект, не получает ничего из этого. Для командного правила локальный хук бесполезен.
Команды делят хуки через менеджер вроде husky или lefthook, посредством core.hooksPath. Решение — держать определения хуков в отслеживаемом каталоге и направить git на него. Настройка git core.hooksPath перенаправляет хуки из .git/hooks в версионируемую папку:
git config core.hooksPath .githooks # использовать закоммиченный каталогИнструменты вроде husky и lefthook автоматизируют ровно это: они хранят хуки в репозитории, а шаг установки (часто на npm install) прописывает core.hooksPath, так что каждый клон получает те же шлюзы. Теперь правило путешествует вместе с проектом, а не живёт на одном ноутбуке.
Добавляем линт-шлюз, который получает вся команда.
Ты хочешь, чтобы каждый коммит линтился, на машине каждого коллеги. Сырой .git/hooks/pre-commit защитил бы только тебя, поэтому ты используешь отслеживаемый каталог.
mkdir .githooks
cat > .githooks/pre-commit <<'EOF'
#!/bin/sh
npm run lint:staged || {
echo "Линтинг провалился — исправь проблемы перед коммитом." >&2
exit 1
}
EOF
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
git add .githooks/pre-commit && git commit -m "chore: add shared pre-commit lint hook"Теперь хук — отслеживаемый файл. Но есть подвох: core.hooksPath — это локальная конфигурация, не коммитится, поэтому свежий клон всё равно требует той самой строки git config, чтобы запуститься. Именно этот разрыв закрывают husky и lefthook — их шаг установки прописывает core.hooksPath автоматически (например, через prepare-скрипт на npm install), так что новый участник выполняет npm install и мгновенно получает шлюз без ручной настройки. Проверь шлюз, добавив в staging файл с ошибкой линтинга и закоммитив: git запускает твой скрипт, тот выходит с 1, и коммит отклоняется с твоим сообщением.
▸Частая ошибка
Хуки — это удобство, а не граница безопасности. Любой может пропустить клиентский хук через git commit --no-verify (или git push --no-verify), а участник без установленного хука не запускает его вообще. Поэтому никогда не полагайся на клиентский хук как на единственную гарантию — подкрепляй критические правила серверным принуждением (хук pre-receive или проверки CI на удалённом репозитории), которое участник не может обойти. Клиентские хуки ловят ошибки рано; серверные шлюзы делают их обязательными.
Ты пишешь хук pre-commit в .git/hooks/pre-commit, который линтит твой код. Почему коллега, клонирующий репозиторий, НЕ получает эту проверку?
Хук git — это скрипт в .git/hooks, который git автоматически запускает на событии; ненулевой выход прерывает операцию, делая хуки настоящим шлюзом. Клиентские рабочие лошадки — pre-commit (быстрые проверки вроде линтинга), commit-msg (навязать формат сообщения) и pre-push (более медленные шлюзы вроде тестов), а серверные pre-receive/update — обязательный запасной рубеж. Поскольку хуки живут в .git, они по умолчанию не коммитятся и не разделяются, поэтому команды используют husky или lefthook плюс core.hooksPath, чтобы версионировать хуки и устанавливать их на каждом клоне — помня, что --no-verify делает клиентские хуки рекомендательными, а не принудительными. Дальше ты увидишь встроенного помощника, который делает повторное разрешение конфликтов автоматическим: rerere.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.