Хуки Git: что это и какие бывают
Хук git — это программа, которую git запускает в определённый момент операции: до того как создан коммит, после того как push дошёл до сервера, после переключения ветки. Это не плагин и не команда git. Это исполняемый файл с известным именем в папке, куда смотрит git, а код возврата — ответ, которого git ждёт: ноль значит «продолжай», всё остальное останавливает операцию там, где git позволяет хуку её остановить.
Хуками репозиторий поддерживает то, о чём договорились его авторы. Хук pre-commit не пропускает коммит с кодом, который не разбирается, хук post-commit пересобирает файл после того, как коммит уже существует, а хук pre-receive на сервере отклоняет push, который нарушает те же правила у всех. Как два локальных хука работают в паре и что пошло не так, когда они не сработались, — в статье pre-commit и post-commit вместе.
Что такое хук git
Git не разбирает содержимое хуков. Он ищет в папке хуков файл с известным именем и, если файл существует и исполняемый, запускает его с теми аргументами, стандартным вводом и окружением, которые относятся к этому моменту операции. Всё остальное — дело самого хука: bash, Python, PHP или собранный бинарник. Имя файла решает, когда хук запустится, а код возврата — что git сделает дальше.
- Имя — это триггер. pre-commit запускается до создания коммита, post-merge — после слияния. Файл с именем pre-commit.sh не запустится никогда: имя должно совпадать точно.
- Исполняемый бит — это выключатель. Хук без +x молча игнорируется. Это первое, что стоит проверить, когда хук «не работает».
- Ноль значит «продолжай». Ненулевой код возврата останавливает операцию там, где git позволяет хуку её остановить. После факта — post-commit, post-receive — код возврата уже ничего не меняет.
- Контекст приходит тремя путями. Аргументы (путь к файлу сообщения для commit-msg), стандартный ввод (ссылки, которые отправляются, для pre-push и pre-receive) и переменные окружения (GIT_DIR, GIT_INDEX_FILE, GIT_EDITOR).
- Рабочая папка локального хука — корень репозитория. Хуки push на сервере запускаются в $GIT_DIR, поэтому серверный хук не может рассчитывать на развёрнутое дерево файлов.
- Ни таймаута, ни песочницы нет. Хук, который ждёт, блокирует команду, и работает он с правами того, кто его запустил.
Где git ищет хуки
По умолчанию это папка .git/hooks внутри репозитория. git init наполняет её файлами .sample: по одному примеру на каждый хук, и ни один из них не включён. Достаточно переименовать файл и сделать его исполняемым, чтобы хук заработал.
ls .git/hooks
applypatch-msg.sample commit-msg.sample fsmonitor-watchman.sample post-update.sample pre-applypatch.sample pre-commit.sample pre-merge-commit.sample prepare-commit-msg.sample pre-push.sample pre-rebase.sample pre-receive.sample push-to-checkout.sample sendemail-validate.sample update.sample
Папку можно перенести настройкой core.hooksPath — так репозиторий держит хуки в отслеживаемой папке, а не внутри .git:
git config core.hooksPath .githooks
Настройка хранится в локальной конфигурации, поэтому в свежем клоне команду всё равно нужно выполнить. Отслеживаемая папка плюс одна команда настройки — самый простой способ поделиться хуками. Фреймворк — способ крупнее: фреймворк pre-commit ставит два файла хуков, которые вызывают его самого, а проверки хранит в YAML-файле рядом с кодом.
Включить, пропустить, отследить
Три команды покрывают почти всё ежедневное использование. Первая включает хук. Вторая пропускает хуки, которые охраняют коммит, — pre-commit, pre-merge-commit и commit-msg, — но не prepare-commit-msg и не хуки post-*, которые всё равно запустятся. Третья печатает каждый хук, который запускает git, с путём и аргументами, и отвечает на вопрос «а он вообще запускался?» раньше, чем вы начнёте отлаживать сам хук.
chmod +x .git/hooks/pre-commit
git commit --no-verify -m "WIP"
GIT_TRACE=1 git commit -m "Test"
Три строки хуков одного коммита, без отметок времени:
run-command.c:659 trace: run_command: GIT_EDITOR=: GIT_INDEX_FILE=.git/index .git/hooks/pre-commit run-command.c:659 trace: run_command: GIT_EDITOR=: GIT_INDEX_FILE=.git/index .git/hooks/commit-msg .git/COMMIT_EDITMSG run-command.c:659 trace: run_command: GIT_EDITOR=: GIT_INDEX_FILE=.git/index .git/hooks/post-commit
Хуки Git 2.43
В Git 2.43 двадцать восемь хуков. Читать их проще в том порядке, в котором git их запускает, а не по алфавиту, поэтому таблицы ниже идут за одним коммитом, одним push и одним обслуживанием репозитория.
Вокруг коммита.
| Хук | Когда запускается | Может остановить коммит |
|---|---|---|
| pre-commit | при git commit, до того как составлено сообщение | да, --no-verify его пропускает |
| pre-merge-commit | после удачного слияния, до коммита слияния | да, --no-verify его пропускает |
| prepare-commit-msg | после того как появилось сообщение по умолчанию, до открытия редактора | да, и --no-verify его не пропускает |
| commit-msg | после того как сообщение написано, получает свой файл аргументом | да, --no-verify его пропускает |
| post-commit | после того как объект коммита создан и HEAD сдвинулся | нет |
| pre-rebase | до начала git rebase | да, отклоняет rebase |
Вокруг рабочего дерева и ссылок.
| Хук | Когда запускается | Может остановить команду |
|---|---|---|
| post-checkout | после git checkout, git switch, git clone и git worktree add | нет, его код возврата становится кодом возврата команды |
| post-merge | после слияния или pull, но не после слияния с конфликтами | нет |
| post-rewrite | после того как git commit --amend и git rebase переписали коммиты | нет |
| reference-transaction | вокруг каждого обновления ссылки: prepared, committed, aborted | да, в состоянии prepared |
| post-index-change | после записи файла индекса | нет |
Вокруг патчей и почты.
| Хук | Когда запускается | Может остановить команду |
|---|---|---|
| applypatch-msg | при git am, на предложенном сообщении | да |
| pre-applypatch | при git am, после применения патча, до коммита | да |
| post-applypatch | при git am, после коммита | нет |
| sendemail-validate | до того как git send-email отправит патч | да |
| p4-changelist, p4-prepare-changelist, p4-post-changelist, p4-pre-submit | мост в Perforce: правка, подготовка, после отправки, до отправки | да, кроме p4-post-changelist |
Вокруг push. Один хук на клиенте, шесть на сервере.
| Хук | Где запускается | Что может |
|---|---|---|
| pre-push | клиент, до отправки объектов; ссылки приходят на стандартный ввод | отклонить весь push |
| pre-receive | сервер, один раз на push, до обновления любой ссылки | отклонить все ссылки в push |
| update | сервер, один раз на ссылку, до её обновления | отклонить эту одну ссылку |
| proc-receive | сервер, когда ссылку обрабатывает вспомогательная команда | отклонить или переписать эту ссылку |
| post-receive | сервер, после обновления всех ссылок | уведомить; отклонить не может |
| post-update | сервер, после post-receive, получает обновлённые ссылки аргументами | уведомить; отклонить не может |
| push-to-checkout | сервер, когда push нацелен на ветку, развёрнутую в непустом репозитории | отклонить или обновить дерево самому |
Обслуживание.
| Хук | Когда запускается | Что может |
|---|---|---|
| pre-auto-gc | до автоматической сборки мусора | отложить её |
| fsmonitor-watchman | связка между демоном fsmonitor и watchman | сообщить об изменённых путях |
| post-index-change | после записи файла индекса, получает два флага аргументами | уведомить |
Чего хук не может
- Хук post ничего не отменяет. К моменту запуска post-commit коммит уже существует. Упавший хук post-commit не делает git commit неудачным: команда всё равно завершается с кодом 0, и единственный след — текст, который хук напечатал.
- Хук — не граница безопасности. Клиентские хуки живут на машине того, кто коммитит, а он может их удалить, отредактировать или передать --no-verify. Обязательными для всех остаются только серверные хуки — и сервер, который отклоняет force push и коммиты без подписи.
- Клон не приносит хуков. .git/hooks лежит внутри .git, а он не часть репозитория. Каждый клон начинается с примеров и больше ни с чем.
- Хук pre-commit может изменить коммит. Коммит собирается из индекса после того, как хук вернул управление, поэтому хук, который выполняет git add, добавляет эти файлы в создаваемый коммит. Так форматтеры коммитят собственный результат и так же хук добавляет файл, который никто не собирался коммитить.
- Хук видит индекс, а не рабочее дерево. Версии одного и того же файла в индексе и на диске — разное содержимое, и хуку достаётся версия из индекса. Что это значит на практике, показано в статье хук pre-commit.
- Хуки не переносимы сами по себе. Bash-хук на Linux — не тот же файл, который запустится на Windows без Git Bash, а хуку, который вызывает php, нужен PHP на машине, где делают коммит.
Клиент и сервер
Важное разделение — не pre и post, а локальное и удалённое. Всё до pre-push включительно работает на машине, где делают коммит: быстро, приватно и необязательно. Всё начиная с pre-receive работает на сервере, после того как объекты туда доехали, и тот, кто отправляет, пропустить это не может. Правило, которое должно выполняться для каждого коммита, живёт на сервере; правило, которое экономит автору лишний круг, — на клиенте. Большинство репозиториев запускают одну и ту же проверку в обоих местах: локальный хук ради скорости, серверный ради гарантии.
Фреймворк pre-commit
Писать одни и те же проверки в каждом репозитории быстро надоедает, поэтому большинство проектов берут фреймворк: он держит проверки в YAML-файле, ставит файлы хуков и запускает инструменты в изолированных окружениях. pre-commit — тот фреймворк, который использует этот сайт. Он ставит и .git/hooks/pre-commit, и .git/hooks/post-commit, а проверки перечислены в .pre-commit-config.yaml в корне репозитория.
У фреймворка есть поведение, которого у обычных хуков нет, и именно оно стало причиной случая, описанного в статье про рабочий процесс: перед каждой стадией он сохраняет все незакоммиченные изменения в ~/.cache/pre-commit/patch<timestamp>-<pid>, выполняет git checkout -- ., чтобы проверки видели чистое рабочее дерево, и после них накладывает патч обратно. Если наложить не удалось, работа не потеряна — файл патча остаётся на диске, — но в рабочем дереве её уже нет.
Статьи раздела
| Хук pre-commit: что он проверяет, что видит и как его пропустить | |
| Хук post-commit: реакции, которые не могут отменить коммит | |
| pre-commit и post-commit вместе: проверка и реакция |