Автоматизируйте свой рабочий процесс: практическое руководство по клиентским хукам Git
Используйте клиентские хуки Git для быстрых локальных проверок, общей настройки, правил сообщений коммитов и более безопасной автоматизации после слияния.
Автоматизируйте свой рабочий процесс: практическое руководство по клиентским хукам Git
Клиентские хуки Git — это небольшие скрипты, которые запускаются на вашем компьютере, когда Git достигает определенных точек в рабочем процессе. Хук pre-commit запускается перед созданием коммита. Хук commit-msg запускается после того, как вы написали сообщение, но до того, как Git его примет. Хук post-merge запускается после завершения слияния. При правильном использовании хуки позволяют на раннем этапе выявлять скучные ошибки: забытое форматирование, сломанные сгенерированные файлы, отсутствующие установки зависимостей или сообщения коммитов, не соответствующие соглашениям вашей команды.
Важное ограничение заключается в том, что клиентские хуки являются локальными. Они не передаются автоматически вместе с репозиторием при его клонировании. Это делает их отличными для быстрой обратной связи и локального удобства, но слабыми как единственный уровень контроля для командного правила. Если проверка действительно защищает основную ветку, поместите ее также в CI или серверное правило.
В каждом репозитории есть каталог хуков в .git/hooks:
ls .git/hooks
Новый репозиторий обычно содержит файлы-образцы, такие как pre-commit.sample. Хук-образец ничего не делает, пока вы не создадите исполняемый файл без суффикса .sample:
cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
Хуки могут быть скриптами на shell, Python, Ruby, Node или любыми другими, которые может выполнить ваш компьютер. Первая строка должна указывать на интерпретатор:
#!/usr/bin/env bash
Для большинства команд лучшим долгосрочным подходом является не ручное редактирование .git/hooks на каждом ноутбуке. Храните скрипты хуков в репозитории, затем настройте Git на использование этого каталога:
git config core.hooksPath .githooks
mkdir -p .githooks
Теперь хук в .githooks/pre-commit может быть закоммичен и проверен, как обычный код проекта. Каждому разработчику все равно потребуется настройка core.hooksPath, но установку можно добавить в скрипт начальной загрузки или задокументировать в процессе онбординга.
Полезный хук Pre-Commit
Хороший хук pre-commit должен быть быстрым и сфокусированным. Если он занимает две минуты на каждый коммит, люди будут обходить его с помощью git commit --no-verify, и хук станет шумом. Оставляйте полные наборы тестов для CI, если только проект не настолько мал, что они действительно выполняются быстро.
Вот практический хук на shell, который проверяет только проиндексированные файлы. Это различие имеет значение. У вас могут быть незавершенные изменения в рабочем дереве, которые вы пока не хотите тестировать. Коммит должен оцениваться по тому, что проиндексировано.
Создайте .githooks/pre-commit:
#!/usr/bin/env bash
set -u
changed_files=$(git diff --cached --name-only --diff-filter=ACMR)
if [ -z "$changed_files" ]; then
exit 0
fi
if git diff --cached --check; then
:
else
echo "Исправьте ошибки пробелов перед коммитом."
exit 1
fi
secret_matches=$(git diff --cached --name-only --diff-filter=ACMR | xargs grep -nE 'AKIA[0-9A-Z]{16}|BEGIN RSA PRIVATE KEY' 2>/dev/null || true)
if [ -n "$secret_matches" ]; then
echo "Возможный секрет найден в проиндексированных файлах:"
echo "$secret_matches"
exit 1
fi
python_files=$(printf '%s\n' "$changed_files" | grep '\.py$' || true)
if [ -n "$python_files" ]; then
printf '%s\n' "$python_files" | while IFS= read -r file; do
[ -f "$file" ] || continue
python3 -m py_compile "$file" || exit 1
done
fi
exit 0
Этот хук делает три скромных действия: позволяет Git обнаруживать ошибки пробелов, проверяет проиндексированные файлы на наличие пары очевидных шаблонов секретов и компилирует измененные файлы Python. Это не замена настоящему сканеру секретов или набору тестов. Это быстрый предохранитель.
Одна распространенная ошибка — использование grep для имен файлов вместо содержимого файлов. Этот сломанный шаблон проверяет только, содержит ли путь TODO, а не содержит ли его файл:
git diff --cached --name-only | grep TODO
Если вы хотите блокировать комментарии TODO, проверьте проиндексированный diff вместо этого:
if git diff --cached -U0 | grep -E '^\+.*TODO:'; then
echo "Найдены проиндексированные комментарии TODO."
exit 1
fi
Но даже в этом случае будьте осторожны. Некоторые команды ответственно используют комментарии TODO. Блокировка каждого TODO может быть скорее раздражающей, чем полезной.
Хуки для сообщений коммитов
Хук commit-msg получает путь к временному файлу сообщения коммита в качестве первого аргумента. Это делает его полезным для таких правил, как "каждый коммит должен начинаться с ID задачи" или "используйте Conventional Commits".
Небольшой пример:
#!/usr/bin/env bash
set -u
message_file="$1"
first_line=$(head -n 1 "$message_file")
if printf '%s' "$first_line" | grep -Eq '^(feat|fix|docs|test|refactor|chore)(\(.+\))?: .+'; then
exit 0
fi
echo "Сообщение коммита должно выглядеть так: fix(api): handle empty token"
exit 1
Это полезно, когда заметки о релизе или журналы изменений генерируются из коммитов. Это менее полезно, когда ваша команда делает squash-слияния и переписывает заголовки PR в любом случае. Настройте хук под рабочий процесс, который вы действительно используете.
Хуки Post-Merge
Хук post-merge лучше всего подходит для локальной очистки после изменения вашего рабочего дерева. Классический пример — обновление зависимостей после изменения файла блокировки.
#!/usr/bin/env bash
set -u
previous_head="HEAD@{1}"
if git diff --name-only "$previous_head" HEAD | grep -Eq '(^package-lock\.json$|^pnpm-lock\.yaml$|^yarn\.lock$)' ; then
if command -v npm >/dev/null 2>&1 && [ -f package-lock.json ]; then
echo "Файл блокировки изменен; запуск npm install."
npm install
fi
fi
if git diff --name-only "$previous_head" HEAD | grep -q '^\.gitmodules$'; then
echo "Конфигурация подмодуля изменена; синхронизация подмодулей."
git submodule sync --recursive
git submodule update --init --recursive
fi
Этот хук не должен делать неожиданных изменений. Если он устанавливает зависимости, выводите, что он делает. Если установка не удалась, сообщите разработчику, как восстановиться. Хук, который молча изменяет рабочее дерево, трудно доверять.
Совместное использование хуков без создания беспорядка
Есть три распространенных способа делиться хуками.
Самый простой — core.hooksPath, когда репозиторий содержит .githooks/, а настройка указывает Git использовать его. Это прозрачно и не требует дополнительного менеджера пакетов.
Проекты на JavaScript часто используют Husky, потому что он интегрируется с процессами установки npm, pnpm или yarn. Это может быть хорошим решением, когда каждый участник уже использует инструментарий Node.
Многие команды со смешанными языками используют фреймворк pre-commit. Он устанавливает и запускает хуки, определенные в .pre-commit-config.yaml, с фиксированными версиями для таких инструментов, как форматтеры, линтеры и проверки файлов. Он добавляет еще один инструмент, но решает проблему "как установить одинаковые хуки везде?" лучше, чем вики-страница.
Чего я избегаю, так это ручного копирования больших скриптов в .git/hooks. Никто их не проверяет, никто не знает, какая версия установлена, а отладка превращается в личную археологию.
Отладка хуков
Когда хук не запускается, проверьте это по порядку:
git config --get core.hooksPath
ls -l .git/hooks .githooks 2>/dev/null
Если core.hooksPath установлен, Git игнорирует .git/hooks и использует настроенный каталог. Если файл хука не является исполняемым в macOS или Linux, Git не запустит его:
chmod +x .githooks/pre-commit
Когда хук запускается, но таинственно завершается с ошибкой, добавьте временную трассировку:
set -x
pwd
env | sort
Хуки запускаются из корня репозитория при обычном использовании Git, но GUI-клиенты и IDE могут выявить различия в пути или окружении. Используйте command -v toolname внутри хука, прежде чем предполагать, что линтер или менеджер пакетов доступен.
Также помните о переключателе обхода:
git commit --no-verify
Это само по себе не является дырой в безопасности; так работает Git. Это еще одна причина, по которой серьезный контроль должен быть в CI или правилах защищенных веток.
Разумная политика хуков
Используйте хуки для проверок, которые являются быстрыми, детерминированными и легкими для объяснения. Форматирование проиндексированных файлов, выявление ошибок пробелов, проверка сообщений коммитов и напоминание разработчикам об установке зависимостей — хорошие кандидаты. Избегайте хуков, которые требуют доступа к сети, занимают много времени или зависят от хрупкого локального состояния.
Если хук блокирует коммит, его сообщение должно точно указывать, что не удалось и как это исправить. "Хук не удался" — недостаточно. Разработчику в середине слияния или производственного исправления нужна четкая следующая команда.
Клиентские хуки Git работают лучше всего, когда они ощущаются как полезное ограждение, а не как местная бюрократия. Держите их маленькими, версионированными, а окончательную власть оставьте в CI.
Сохраняйте хуки дружелюбными во время чрезвычайных ситуаций
Хуки должны помогать во время обычной работы, не загоняя кого-то в ловушку во время срочного исправления. Это означает, что каждый блокирующий хук нуждается в четком сообщении об ошибке и реалистичном запасном выходе. Git уже предоставляет --no-verify для хуков коммита и пуша, но ваша команда все равно должна решить, когда обход допустим. Производственное исправление отличается от пропуска форматирования, потому что разработчик спешит.
Хорошее сообщение хука говорит, что не удалось, где не удалось и что запустить дальше:
echo "ESLint не удался на проиндексированных файлах JavaScript."
echo "Запустите: npm run lint -- --fix"
exit 1
Плохое сообщение говорит только failed или выгружает страницы вывода инструмента без контекста. Люди учатся игнорировать такие хуки.
Если хук изменяет файлы, будьте особенно осторожны. Форматтеры могут быть полезны в pre-commit, но они также могут создать путаницу, когда изменяют неиндексированные части файла. Многие команды предпочитают проверять форматирование в хуке и позволять разработчику запускать форматтер вручную. Другие используют инструменты, которые форматируют только проиндексированные фрагменты. Выберите одно поведение и задокументируйте его в репозитории, а не в чате, который исчезает.
Для команд проверяйте изменения хуков, как код приложения. Хук может замедлить каждый коммит, раскрыть детали окружения в журналах или сломать участников на Windows, если он предполагает поведение только для Bash. Если в вашем проекте есть участники на Windows, тестируйте хуки в Git Bash или используйте кроссплатформенный запускатель хуков. Если в вашем проекте есть контейнеры или оболочки разработки, рассмотрите возможность запуска хуков в той же среде, что и приложение, чтобы все использовали одни и те же версии инструментов.
Лучшие хуки почти невидимы, когда все в порядке, и очень конкретны, когда что-то не так. Это стандарт, к которому нужно стремиться.
Версионируйте хуки как код продукта
Скрипт хука становится частью опыта разработчика. Если он сломается, это почувствует каждый участник. Держите скрипты маленькими, называйте вспомогательные функции понятно и избегайте хитрых трюков shell, когда можно обойтись простой командой. Если хук вырастает больше, чем на экран или два, переместите реальную логику в проверенный скрипт проекта и позвольте хуку вызывать этот скрипт.
Например, вместо встраивания длинной процедуры линтинга в .githooks/pre-commit, вызовите:
./scripts/check-staged-files.sh
Этот скрипт может быть запущен разработчиками, хуками и CI. Это также означает, что разработчик может воспроизвести ошибку, не притворяясь, что делает коммит. Воспроизводимость — это разница между полезным хуком и таинственным локальным препятствием.
Фиксируйте версии инструментов, где это возможно. Хук, который вызывает то, что black, eslint или prettier оказывается первым в PATH, может вести себя по-разному на разных машинах. Локальные зависимости проекта, файлы блокировки, контейнеры или менеджеры версий делают вывод хука более предсказуемым.
Наконец, ограничьте хуки репозиторием. Глобальные хуки звучат удобно, но они часто удивляют вас месяцы спустя, когда несвязанный репозиторий начинает выдавать ошибки из-за старого личного правила. Используйте глобальные хуки только для действительно личных предпочтений, а не для командной политики.
Еще одно последнее практическое правило: никогда не позволяйте хукам быть единственным местом, где существует команда. Если хук проверяет проиндексированные файлы Python, держите эту команду также в скрипте или запускателе задач. Разработчики должны иметь возможность запустить ту же проверку намеренно, до того, как Git их прервет.
Для проектов с открытым исходным кодом предполагайте, что у участников может не быть установлен полный инструментарий. Хук, который завершается с дружественным сообщением о настройке, — это нормально. Хук, который выдает трассировку стека из-за отсутствующего локального бинарника, кажется сломанным. Проверяйте предварительные условия перед запуском более тяжелых команд и указывайте людям на команду настройки, используемую проектом.
Также подумайте о частичных коммитах. Многие опытные разработчики индексируют только часть файла. Хуки, которые форматируют весь файл, могут случайно включить неиндексированную работу в коммит. Если ваша команда часто использует частичные коммиты, предпочитайте проверки, которые читают проиндексированный diff, или инструменты, предназначенные для проиндексированного содержимого.
Если хук постоянно обходится, относитесь к этому как к обратной связи. Либо проверка слишком медленная, сообщение об ошибке неясно, либо правило должно быть в CI, а не в локальном пути коммита. Устраните трение, а не обвиняйте разработчиков в использовании обхода, предоставленного Git.