pre-commit — небольшой инструмент, который запускает проверки автоматически перед каждым коммитом. Он не заменяет CI, а переносит дешёвые ошибки на самое раннее место: разработчик получает замечание через секунду, а не через десять минут в конвейере. В статье — установка, рабочий конфиг для Python, shell и инфраструктурных файлов, локальные хуки, обновление версий и подключение к CI.
Зачем нужен pre-commit, если есть CI
Хуки Git сами по себе неудобны: их нужно хранить в репозитории, устанавливать в каждый клон и обновлять вручную. pre-commit решает три задачи: хранит конфигурацию в .pre-commit-config.yaml, изолирует окружение каждого хука и запускает проверки только по изменённым файлам. В результате пробелы, сломанный YAML, забытый отладочный вывод и секреты отсекаются до того, как попадут в историю.
Инструмент написан на Python, но проверяет любые файлы. Хуки для Go, Rust, JavaScript и shell работают через собственные окружения и не требуют устанавливать всё в системный Python. Это важно в смешанных репозиториях, где рядом лежат скрипты и фронтенд.
Установка и первый запуск
Ставьте pre-commit через pipx, чтобы не смешивать его с зависимостями проекта. В Debian и Ubuntu пакет также доступен, но версия в репозитории может отставать. После установки выполните pre-commit install в корне клона: будет создан файл .git/hooks/pre-commit, который вызывает проверки.
pipx install pre-commit
cd ~/projects/myapp
pre-commit install
pre-commit --version
Проверить работу до первого коммита можно вручную. Ключ —all-files прогоняет хуки по всему репозиторию, а —files ограничивает список конкретными путями. Если конфигурации ещё нет, образец выводит команда pre-commit sample-config, но лучше сразу написать свою. Ключ —install-hooks заранее скачивает окружения, а —hook-type commit-msg пригодится, если вы проверяете формат сообщений коммита. В репозитории с субмодулями установите хук и в них.
pre-commit run --all-files
pre-commit run --files src/app.py scripts/deploy.sh
pre-commit validate-config .pre-commit-config.yaml
Базовый конфиг
Каждый репозиторий описывается блоком repo и rev. Значение rev — тег или коммит, а не ветка main: ветка делает сборку невоспроизводимой. В примере ниже собраны безопасные проверки, которые почти не дают ложных срабатываний.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
args: [--maxkb=1024]
- id: detect-private-key
- id: mixed-line-ending
args: [--fix=lf]
Не включайте всё подряд. Каждый лишний хук увеличивает время коммита и раздражает команду. Начните с пяти-семи проверок, затем добавляйте те, что ловят реальные ошибки в вашем проекте.
Python, shell и форматирование
Для Python удобна связка Ruff и pre-commit-hooks: линтер и форматтер запускаются в изолированном окружении, а версия фиксируется в конфиге. Shell-скрипты проверяет shellcheck, а для Dockerfile и Markdown есть отдельные хуки. Не смешивайте линтер и форматтер в одном проходе, если форматтер меняет файлы: pre-commit сам сообщит, что файлы исправлены, и попросит повторить коммит.
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck
args: [--severity=warning]
Хуки форматирования по умолчанию меняют файлы и возвращают ненулевой код. Это нормальное поведение: просмотрите diff, добавьте изменения через git add и повторите коммит. Не используйте git commit —no-verify как привычку — так вы обходите и действительно полезные проверки.
Локальные хуки и тесты
Если готового хука нет, опишите локальный. language: system означает, что команда уже установлена в окружении разработчика; pass_filenames: false запрещает передавать имена файлов, что нужно для тестов и сборки. Тип files помогает не запускать проверку на каждом коммите в документацию.
- repo: local
hooks:
- id: unit-tests
name: fast unit tests
entry: pytest -q tests/unit
language: system
pass_filenames: false
types: [python]
- id: compose-config
name: validate compose file
entry: docker compose config -q
language: system
files: ^docker-compose\.ya?ml$
Медленные проверки выносите в отдельный этап. pre-commit поддерживает stages: укажите manual и запускайте такой хук командой pre-commit run —hook-stage manual. Это удобно для интеграционных тестов, которые не должны тормозить каждый коммит.
pre-commit run --hook-stage manual --all-files
pre-commit autoupdate
pre-commit clean
pre-commit gc
autoupdate поднимает rev до последних тегов, но не проверяет совместимость. После обновления прогоните все хуки и просмотрите diff конфига: иногда новая версия меняет правила и требует правок в коде. Команда clean удаляет окружения хуков, а gc чистит неиспользуемые репозитории.
pre-commit в CI
В конвейере достаточно установить инструмент и запустить проверку по всем файлам. Кэш окружений хуков ускоряет повторные сборки, но не должен скрывать изменения конфигурации. Если хук требует Docker, убедитесь, что в runner есть демон, иначе проверка упадёт не из-за кода.
python -m pip install pre-commit
pre-commit run --all-files --show-diff-on-failure
git diff --exit-code
Флаг —show-diff-on-failure печатает изменения, которые внесли автоисправляющие хуки. Последняя команда страхует от случая, когда хук поправил файл, но вернул нулевой код. В GitLab CI те же команды помещаются в job с образом python и кэшем каталога ~/.cache/pre-commit.
Типичные ошибки
- rev указывает на ветку, и сборка ломается без изменений в коде;
- хуки запускаются по всей файловой системе из-за отсутствия exclude;
- локальный хук требует программу, которой нет у коллег;
- секреты уже попали в историю — pre-commit не чистит прошлые коммиты;
- проверка занимает минуты, и разработчики начинают обходить её.
Для исключений используйте exclude с якорем на каталог, например ^vendor/. Секреты, найденные detect-private-key, нужно не просто удалить из файла, а отозвать и заменить: история Git остаётся доступной.
Итог
pre-commit окупается в командах любого размера, но особенно в небольших: он делает стиль и базовые проверки частью процесса, а не поводом для споров на ревью. Настройте минимальный набор хуков, зафиксируйте версии, добавьте быстрые тесты и продублируйте запуск в CI. Тогда локальные проверки экономят время, а конвейер остаётся последней линией защиты. Начните с check-yaml и trailing-whitespace, а расширяйте конфиг по мере появления реальных ошибок.