Инструменты

Pre-commit хуки: как ловить проблемы в коде до коммита, а не после

Знакомая цепочка: коммит запушен, CI зажёг красный крест из-за забытого console.log или несоответствия форматированию, ревьюер оставил комментарий «поправь линтер» вместо того, чтобы обсуждать логику. Каждая такая мелочь по отдельности — секундное дело, но пройдя через пуш, ожидание CI и раунд ревью, она превращается в минуты и часы простоя. Pre-commit хуки решают это не дисциплиной «не забывай проверять», а тем, что просто не дают закоммитить код, который явно нарушает правила — раньше, чем он вообще покинет вашу машину.

Что такое git-хук на самом деле

У каждого git-репозитория есть скрытая папка .git/hooks — в ней лежат скрипты-заглушки для разных событий жизненного цикла коммита: pre-commit запускается перед созданием коммита, commit-msg — после того как вы ввели сообщение, но до его сохранения, pre-push — перед отправкой на удалённый репозиторий. Если сделать файл .git/hooks/pre-commit исполняемым и вернуть из него ненулевой код завершения, git просто остановит коммит и покажет вывод скрипта. Это встроенный, ничем не зависимый механизм — он есть в любом git-репозитории без единой внешней библиотеки.

Проблема в том, что папка .git не попадает в репозиторий — она локальная для каждой копии проекта. Значит, хук, добавленный руками, не доедет до остальной команды: каждый разработчик должен будет создать его у себя сам, и рано или поздно кто-то этого не сделает. Именно эту проблему решают фреймворки для хуков поверх голого git.

Framework поверх git-хуков

Самый распространённый универсальный вариант — Python-инструмент pre-commit, который работает с любым языком проекта, не только с Python. Идея простая: конфигурация хуков хранится в файле .pre-commit-config.yaml прямо в репозитории, а сам инструмент при установке (pre-commit install) кладёт в .git/hooks/pre-commit тонкий скрипт, который читает этот файл и запускает перечисленные проверки.

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-merge-conflict
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.5.0
    hooks:
      - id: ruff

Для JavaScript/TypeScript-проектов похожую роль чаще играет связка husky + lint-staged: husky отвечает за сам факт запуска хука, а lint-staged сужает проверку только до файлов, которые реально попали в коммит, а не гоняет линтер по всему репозиторию. Разница в экосистеме, но принцип общий: конфиг лежит в репозитории и коммитится вместе с кодом, поэтому у всей команды хуки настраиваются одной командой установки, а не вручную у каждого.

Какие проверки туда стоит класть, а какие — нет

Соблазн — засунуть в pre-commit всё, что только можно проверить. На практике это быстро убивает саму идею: если хук перед каждым коммитом занимает минуту-две, разработчики начнут коммитить реже и крупнее, а потом искать способ обойти проверку. Разумная граница проходит по скорости и детерминированности:

А вот полный прогон тестов, сборка проекта или проверки, которые ходят в сеть или в базу данных, в pre-commit обычно не место — для них лучше подходит pre-push хук (коммитить всё равно приходится часто, а пушить — реже) или, ещё правильнее, CI, где долгая проверка никого не блокирует локально.

Хук, который выполняется дольше пары секунд, довольно быстро перестаёт быть проверкой и становится источником git commit --no-verify в мышечной памяти команды — а обойти можно и то, что действительно стоило бы поймать.

Типичные грабли на практике

Первая и самая частая проблема — именно обход через флаг --no-verify. Технически он должен быть аварийным люком для редких ситуаций, но если хуки медленные или слишком капризные, он превращается в привычку, и вся система проверок молча перестаёт работать для части коммитов. Лечится это не запретом флага (git не даёт его убрать), а тем, чтобы хуки оставались быстрыми и редко ложно-положительными — тогда обходить их просто не хочется.

Вторая — расхождение локальной и CI-конфигурации: если pre-commit проверяет одно, а CI-пайплайн — немного другое (другая версия линтера, другие правила), разработчик получает зелёный хук локально и красный CI на сервере, что подрывает доверие к самой идее локальных проверок. Стоит держать одну и ту же конфигурацию и версии инструментов в обоих местах, а CI использовать как финальный контроль, а не как источник новых правил.

Третья — установка хуков не автоматизирована. Если pre-commit install или npx husky install нужно помнить запустить руками после клонирования репозитория, часть команды просто забудет это сделать. Практичное решение — привязать установку к обычному шагу настройки проекта, например к npm install через prepare-скрипт в package.json, чтобы хуки появлялись сами собой, без отдельного шага, который легко пропустить.

В сумме pre-commit хуки — не замена код-ревью и не замена CI, а более дешёвый и быстрый первый рубеж: они ловят механические проблемы там, где это стоит секунды, а не минуты ожидания сборки и не время ревьюера. Если в проекте регулярно повторяются одни и те же комментарии в ревью про форматирование или забытые отладочные строки — это ровно тот сигнал, что пора автоматизировать эту часть, а не полагаться на память каждого разработчика.

← Все статьи
Я люблю Алину Цой (Билялову)