Хуки Git: автоматизация на клиенте и сервере

Рейтинг: 70.2% · 15 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

Хуки Git: автоматизация на клиенте и сервере

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере (вы здесь)
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Знакомая боль: ты в сотый раз ловишь в код-ревью замечание "ты забыл прогнать линтер" или "опять закоммитил console.log и сломанный отступ". Хуже - когда в main улетает коммит с сообщением "fix" или, не дай бог, с приватным ключом в .env. Всё это ловится не глазами ревьюера, а машиной - в момент, когда ты жмёшь commit или push. Механизм для этого встроен в Git с самого начала и называется git hooks. Это урок про то, как git хуки реально работают под капотом, чем клиентские отличаются от серверных, почему их нельзя просто положить в репозиторий и как это обходят в 2026 году.

Что такое git hooks и где они физически лежат

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

Лежат они по умолчанию в .git/hooks. Загляни туда в любом репозитории:

Код: Выделить всё

$ ls -1 .git/hooks
applypatch-msg.sample
commit-msg.sample
post-update.sample
pre-applypatch.sample
pre-commit.sample
pre-push.sample
prepare-commit-msg.sample
update.sample
Видишь суффикс .sample? Это примеры от Git, и они намеренно "сломаны" - с таким именем Git их не запустит. Чтобы активировать хук, убираешь .sample и делаешь файл исполняемым. Язык любой: bash, python, ruby - всё, что система умеет запускать по шебангу (первая строка #!/bin/sh или #!/usr/bin/env python3). Git не парсит содержимое, он просто его выполняет.

Код: Выделить всё

$ cd .git/hooks
$ mv pre-commit.sample pre-commit
$ chmod +x pre-commit
Самое важное про .git/hooks: этот каталог НЕ попадает под версионирование. Папка .git целиком вне дерева, которое отслеживает Git. Из этого следует жёсткое практическое правило: хук, который ты написал у себя, не уедет к коллеге через git push. Он остаётся только на твоей машине. Это сделано осознанно - представь, что любой склонированный репозиторий мог бы при первом же коммите выполнить произвольный код автора. Это была бы дыра размером с грузовик. Поэтому распространение хуков - отдельная задача, к которой вернёмся.

Изображение

Клиентские хуки: pre-commit, commit-msg, pre-push и друзья

Клиентские хуки срабатывают на твоей машине вокруг локальных операций. Разберём рабочую четвёрку.

pre-commit - запускается ДО того, как Git начнёт формировать коммит, ещё до ввода сообщения. Сюда вешают линт, автоформат, проверку на отладочный мусор. Хук видит то, что в индексе (staging area). Классическая ошибка - проверять рабочую копию, а не индекс: если ты сделал git add файла, потом поправил его, в коммит уйдёт версия из индекса, а линт ты прогнал по другой. Правильно - проверять именно staged-версию. Минимальный pre-commit, отбивающий отладочные принты:

Код: Выделить всё

#!/bin/sh
# блокируем коммит, если в индексируемых .js есть console.log
files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.js$')
[ -z "$files" ] && exit 0
if git diff --cached -- $files | grep -q 'console\.log'; then
  echo "Найден console.log в staged-изменениях. Убери и попробуй снова." >&2
  exit 1
fi
exit 0
Ключевое здесь - git diff --cached: мы смотрим именно индекс. Выход 1 печатает сообщение в stderr и срывает коммит.

prepare-commit-msg - запускается после pre-commit, но до открытия редактора. Получает аргументами путь к файлу с черновиком сообщения. Тут удобно подставить шаблон: номер задачи из имени ветки, префикс типа коммита, заготовку. Текст, который хук запишет в файл, ты увидишь уже в редакторе.

commit-msg - запускается после того, как ты ввёл сообщение, и получает путь к файлу с этим сообщением. Это место для валидации формата. В 2026 стандарт де-факто - Conventional Commits: сообщение вида type(scope): описание, где type из закрытого списка (feat, fix, docs, refactor, test, chore и так далее). Зачем такая дисциплина - потому что из этих префиксов инструменты semantic-release и release-please автоматически собирают CHANGELOG и поднимают версию по SemVer. Сломанный формат рушит автоматику, поэтому ловим его прямо тут:

Код: Выделить всё

#!/bin/sh
# commit-msg: первый аргумент - путь к файлу сообщения
pattern='^(feat|fix|docs|style|refactor|perf|test|chore)(\(.+\))?: .{1,}'
if ! grep -qE "$pattern" "$1"; then
  echo "Сообщение не по Conventional Commits." >&2
  echo "Пример: feat(auth): добавить вход по SSH-ключу" >&2
  exit 1
fi
pre-push - последний рубеж перед отправкой на сервер. Тут уместны вещи потяжелее: прогон тестов, проверка, что не пушишь напрямую в main. Хук получает на stdin строки с тем, что и куда летит: локальный ref, локальный sha, удалённый ref, удалённый sha. Это позволяет, например, запретить push веток с незакоммиченным WIP. Не вешай сюда десятиминутный прогон тестов - люди начнут обходить хук, и об этом ниже.

Серверные хуки: pre-receive, update, post-receive

Серверные хуки живут на голом (bare) репозитории, куда народ пушит. Их принципиальное отличие от клиентских - их нельзя обойти. Клиентский хук на твоей машине; хочешь - снёс его. Серверный исполняется на сервере, который ты не контролируешь, и потому это настоящий enforcement, а не вежливая просьба.

pre-receive - срабатывает один раз на весь push, ДО применения изменений. На stdin получает по строке на каждый обновляемый ref: старый sha, новый sha, имя ref. Если хук вернёт ненулевой код - отклоняется ВЕСЬ push целиком, ни одна ветка не обновится. Сюда вешают политику: запрет force-push в защищённые ветки, требование подписи, проверку, что автор коммита из разрешённого домена.

update - почти то же, но вызывается ОТДЕЛЬНО для каждого обновляемого ref и получает три аргумента: имя ref, старый sha, новый sha. Разница тонкая, но важная: с update можно принять обновление одной ветки и отклонить другой в рамках одного push. pre-receive - "всё или ничего", update - поштучно.

post-receive - срабатывает ПОСЛЕ того, как изменения уже приняты. Повлиять на исход не может, его код выхода игнорируется. Зато это идеальная точка для побочных эффектов: дёрнуть CI, задеплоить, отправить уведомление в чат, обновить тикет. На stdin - те же строки old new ref, что и у pre-receive.

Код: Выделить всё

#!/bin/sh
# post-receive: триггерим деплой только при пуше в main
while read oldrev newrev ref; do
  if [ "$ref" = "refs/heads/main" ]; then
    echo "main обновлён, запускаю деплой..."
    /opt/deploy/run.sh "$newrev" &
  fi
done
На практике в 2026 серверную логику чаще держат не в этих хуках напрямую, а в платформенных правилах: GitHub repository rulesets (которые в GA заменяют classic branch protection), GitLab push rules, Merge Queue. Но под капотом у self-hosted решений и Gitea/Forgejo это всё те же receive-хуки.

Почему хуки не версионируются и как это обойти: core.hooksPath и фреймворки

Вернёмся к главной проблеме: .git/hooks не уезжает с push, значит у каждого в команде свой набор хуков, то есть по факту никакого. Есть три способа это починить, по возрастанию цивилизованности.

Способ 1: core.hooksPath. Эта настройка переопределяет каталог хуков. Кладёшь хуки в обычную папку внутри репозитория, например .githooks/, версионируешь её как любой код, а каждый разработчик один раз выполняет:

Код: Выделить всё

$ git config core.hooksPath .githooks
Теперь Git ищет хуки не в .git/hooks, а в .githooks - и эта папка отслеживается. Файлы должны быть исполняемыми (git add сохраняет бит исполнимости). Минус один: разовую команду config всё равно надо выполнить на каждой машине, забыл - хуки не работают. Этот шаг обычно прячут в make setup или в npm-скрипт postinstall.

Способ 2: фреймворк pre-commit - стандарт 2026. Несмотря на имя, это не про хук pre-commit, а про целый менеджер хуков на Python (ставится через pip или uv). Идея: ты не пишешь bash-скрипты руками, а декларативно описываешь нужные проверки в версионируемом файле .pre-commit-config.yaml. Сами проверки тянутся из готовых репозиториев-источников по pinned-версии, что делает прогон воспроизводимым у всех.

Код: Выделить всё

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-merge-conflict
      - id: detect-private-key
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.0
    hooks:
      - id: ruff
После git clone каждый делает один раз pre-commit install - и фреймворк сам прописывает себя в .git/hooks/pre-commit. Обрати внимание на хук detect-private-key: это та самая защита, которая ловит приватный ключ ДО коммита. Запустить по всему репозиторию вручную:

Код: Выделить всё

$ pre-commit run --all-files
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
detect private key.......................................................Passed
ruff.....................................................................Failed
- hook id: ruff
- files were modified by this hook
Разбор вывода: первые три проверки прошли, ruff упал, потому что сам поправил файлы (автофикс), и просит пересмотреть и заново добавить их в индекс. Именно так фреймворк сочетает проверку и автоформат - падение тут не ошибка, а "я кое-что поправил, проверь".

Способ 3: husky/lefthook - для JS-команд. husky прописывает core.hooksPath в свою папку и активируется через npm-скрипт prepare, так что после npm install хуки уже на месте. lefthook - быстрая альтернатива на Go с YAML-конфигом и параллельным запуском. Выбор между pre-commit, husky и lefthook чаще про стек: Python/смешанный - pre-commit, чистый Node - husky или lefthook.

Способ 4 (новинка Git 2.54, апрель 2026): хуки прямо в конфиге. Раньше core.hooksPath был "всё или ничего" - одна общая папка на репозиторий. Git 2.54 добавил конфиг-секции hook, где можно объявить несколько команд на одно событие, и они выполнятся в порядке появления в конфиге:

Код: Выделить всё

[hook "linter"]
  event = pre-commit
  command = ~/bin/linter
[hook "no-leaks"]
  event = pre-commit
  command = ~/bin/leak-detector
Отключить конкретный, не удаляя - hook.no-leaks.enabled = false. Важно: легаси-скрипты из .git/hooks при этом продолжают работать и запускаются последними. Команда git hook list покажет, что реально отработает на событии. Фича свежая - в больших командах ядро пока остаётся за pre-commit-фреймворком, но знать про неё нужно.

Bypass через --no-verify и когда так делать нельзя

У клиентских хуков есть аварийный обход. Флаг --no-verify (короткий -n) у commit и push отключает pre-commit, commit-msg и pre-push:

Код: Выделить всё

$ git commit -m "wip" --no-verify
$ git push --no-verify
Когда это нормально: горящий хотфикс ночью, когда линтер падает на не относящемся к фиксу файле, и чинить его сейчас некогда. Когда это табу: обход commit-msg ради кривого сообщения, обход detect-private-key, обход на CI. Главная мысль - клиентский хук не гарантия, а удобство. Любую клиентскую проверку, которая ОБЯЗАНА выполняться, дублируй на сервере (pre-receive) или в CI, где --no-verify бессилен. Архитектурно это так: клиентские хуки дают быструю обратную связь и снижают шум, серверные хуки и CI обеспечивают жёсткие гарантии. Только сервер - источник истины.
Правило большого пальца: если проверку можно обойти флагом и от этого пострадает кто-то кроме тебя - она должна жить и на сервере тоже.
Типичные грабли
  • Хук не запускается, потому что нет бита исполнимости. chmod +x обязателен; на проверках в CI бит часто теряется - храни хуки исполняемыми в индексе.
  • Неправильный шебанг. На системах без python3 в PATH хук с #!/usr/bin/env python3 молча падает. Для максимальной переносимости - #!/bin/sh.
  • pre-commit проверяет рабочее дерево, а не индекс. Всегда git diff --cached, иначе ловишь то, что не коммитишь, и пропускаешь то, что коммитишь.
  • Тяжёлый pre-push с долгими тестами - люди массово переходят на --no-verify, и хук перестаёт что-либо значить. Тяжёлое - в CI.
  • core.hooksPath включён, но забыли выполнить git config на новой машине - хуки тихо не работают. Прячь установку в setup-скрипт.
  • Перенос rev в .pre-commit-config.yaml забыли запинить - и у всех разные версии линтера, прогоны не воспроизводятся. Фиксируй rev и обновляй через pre-commit autoupdate.
Мини-лаба: ставим страж формата коммитов руками

Повтори по шагам, это пять минут:

Код: Выделить всё

$ mkdir hook-lab && cd hook-lab
$ git init -b main
$ printf '#!/bin/sh\npat="^(feat|fix|docs|chore): .+"\n' > .git/hooks/commit-msg
$ printf 'grep -qE "$pat" "$1" || { echo "bad msg" >&2; exit 1; }\n' >> .git/hooks/commit-msg
$ chmod +x .git/hooks/commit-msg
$ echo hi > a.txt && git add a.txt
$ git commit -m "added file"
bad msg
$ git commit -m "feat: add a.txt"
[main (root-commit) ...] feat: add a.txt
Первый коммит отбит - сообщение не по формату. Второй прошёл. Теперь убедись, что обход работает (и подумай, почему это плохо для commit-msg): git commit -m "broken" --no-verify пройдёт. Бонус: вынеси этот хук в .githooks/commit-msg, выполни git config core.hooksPath .githooks и проверь, что после удаления .git/hooks/commit-msg проверка всё равно работает - теперь она версионируется.

Контрольные вопросы
  • Почему хук, лежащий в .git/hooks, не попадёт к коллеге через git push, и какие три способа это обойти ты знаешь?
  • В чём разница между серверными хуками pre-receive и update при пуше сразу нескольких веток?
  • Какой хук и по какому аргументу валидирует сообщение коммита, и при чём тут Conventional Commits и semantic-release?
  • Что делает --no-verify, какие хуки он НЕ может отключить и почему обязательные проверки дублируют на сервере?
Итог

Хук - это исполняемый файл, который Git дёргает на событие, а код выхода решает, продолжать или нет. Клиентские (pre-commit, commit-msg, pre-push, prepare-commit-msg) дают быструю обратную связь, но обходятся через --no-verify. Серверные (pre-receive, update, post-receive) - настоящий enforcement и точка для CI/деплоя. Каталог .git/hooks не версионируется, поэтому в 2026 хуки распространяют через core.hooksPath, фреймворк pre-commit с .pre-commit-config.yaml (стандарт де-факто), husky/lefthook для JS, а с Git 2.54 - ещё и через конфиг-секции hook. Главный принцип: клиент для удобства, сервер для гарантий.
👍3 ❤️2 🔥3 😄 🤔1
Аватара пользователя
raspberrypro
Сообщения: 1
Зарегистрирован: 29 май 2026, 16:31

Re: Хуки Git: автоматизация на клиенте и сервере

Сообщение raspberrypro »

Дошло наконец почему мои хуки коллега не видел - папка .git же не коммитится. Перевел все на core.hooksPath, спасибо.
👍 ❤️ 🔥1 😄 🤔2
Аватара пользователя
scalacoder
Сообщения: 1
Зарегистрирован: 17 май 2026, 07:44

Re: Хуки Git: автоматизация на клиенте и сервере

Сообщение scalacoder »

А detect-private-key реально ключи ловит? Один раз чуть .env с токеном не запушил, поставлю pre-commit на всякий.
👍2 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Псевдонимы и кастомизация .gitconfig
Следующая глава →
Подпись коммитов и тегов: SSH, GPG и Sigstore

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: Подпись коммитов в Git (SSH, GPG)Git hooks: хуки и автоматизация

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя