linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it works
  • Уроки
  • База знаний
  • Собеседование
Часть IV — Совместная работа

$ глава 14 · 50 минут

Hooks: автоматизация на стороне Git

Hook в Git - это скрипт, который запускается на определённое событие: «перед коммитом», «после коммита», «до push'а», «при приёме push'а на сервере». Простой механизм, который позволяет автоматизировать всё, что не должно зависеть от дисциплины разработчика.

Hooks - это не магия. Это просто исполняемые файлы в .git/hooks/, которые Git запускает с определёнными аргументами и проверяет код возврата. Понимание этого делает работу с ними прозрачной.

Эта глава - про то, какие hooks бывают, что хорошо в них делать (и не делать), как они управляются на уровне команды через специальные фреймворки, и про разницу client-side и server-side hooks.

14.1 Hook - это скрипт

В каждом репозитории есть .git/hooks/ с шаблонами:

.git/hooks/
  pre-commit.sample
  commit-msg.sample
  pre-push.sample
  post-merge.sample
  ...

.sample - это «выключенные». Чтобы hook сработал, нужно:

  1. Снять расширение .sample (или создать новый файл без него).
  2. Сделать исполняемым (chmod +x .git/hooks/pre-commit).
  3. Написать туда любой исполняемый код: bash, python, что угодно - лишь бы первая строка shebang указывала, чем это запустить.

Минимальный pre-commit hook:

bash
#!/usr/bin/env bash
# .git/hooks/pre-commit
if grep -r "TODO" --include="*.py" src/; then
    echo "Found TODO in code. Commit blocked."
    exit 1
fi

Git вызывает этот скрипт перед каждым коммитом. Если он возвращает 0 - коммит проходит. Если ненулевой - коммит отменяется. Это весь протокол.

.git/hooks/ - локальные

Hooks не клонируются. Когда коллега делает git clone, директория .git/hooks/ у него пустая (точнее, с теми же .sample-шаблонами). Это значит:

  • Hook нельзя «коммитнуть». Если ты добавил скрипт в .git/hooks/, его никто, кроме тебя, не получит.
  • Hook у каждого разработчика свой. Один может включить проверку перед коммитом, другой - нет.

Это by design: hooks - это локальная политика разработчика, не политика репозитория. Чтобы политика была общей, есть отдельные инструменты (см. секцию про pre-commit framework ниже).

14.2 Client-side hooks: до и после локальных операций

Самые полезные client-side hooks:

pre-commit

Запускается перед git commit, после git commit -m "...", но до создания коммита. Если возвращает ненулевой код - коммит не создаётся.

Типичное использование:

  • Запустить linter / formatter и отказать в плохом коде.
  • Проверить, что нет TODO/FIXME в чувствительных файлах.
  • Запретить коммит секретов (с помощью detect-secrets, см. secret-scanning).
bash
#!/usr/bin/env bash
# Запустить ruff на изменённых .py файлах
files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
if [ -n "$files" ]; then
    ruff check $files || exit 1
fi

commit-msg

Запускается после ввода сообщения коммита. На входе - путь к файлу с сообщением. Может:

  • Проверить формат (например, conventional-commits).
  • Дополнить сообщение (добавить ID тикета из имени ветки).
  • Отказать, если сообщение пустое или нарушает правила.
bash
#!/usr/bin/env bash
msg_file=$1
first_line=$(head -1 "$msg_file")
if ! echo "$first_line" | grep -qE '^(feat|fix|chore|docs|refactor|test)(\(.+\))?: .+'; then
    echo "Commit message must start with feat/fix/chore/... See [[conventional-commits]]."
    exit 1
fi

pre-push

Запускается перед git push. На входе - имя remote и URL. Может:

  • Не дать запушить, если локальные тесты падают.
  • Заблокировать push в защищённые ветки (main).
bash
#!/usr/bin/env bash
protected_branch="main"
current=$(git symbolic-ref --short HEAD)
if [ "$current" = "$protected_branch" ]; then
    echo "Direct push to $protected_branch is not allowed."
    exit 1
fi

post-merge, post-checkout, post-rewrite

«After»-hooks: запускаются после операции. Не могут отменить - операция уже произошла. Полезны для «привести окружение в порядок»:

  • post-merge - обновить зависимости, если изменился package.json/requirements.txt.
  • post-checkout - пересобрать assets, если переключили ветку.
bash
#!/usr/bin/env bash
# post-merge: переустановить deps при изменении requirements.txt
if git diff HEAD@{1} HEAD --name-only | grep -q '^requirements\.txt$'; then
    pip install -r requirements.txt
fi

Эти hooks не могут отменить операцию, но могут спасти от ситуации «забыл переустановить deps после смены ветки, питон не импортирует, час ищу почему».

14.3 Server-side hooks: централизованная политика

На сервере (bare-репозитории) есть свои hooks. Они запускаются при приёме push'а:

  • pre-receive - перед принятием любого ref'а. Если возвращает ненулевой код, весь push (все ветки в одном push'е) отменяется.
  • update - для каждого пушимого ref'а отдельно. Можно принять одни и отвергнуть другие.
  • post-receive - после приёма всех refs. Используется для нотификаций, триггеров CI, деплоя.

Server-side hooks нельзя обойти: они выполняются на сервере, до того как изменения становятся видимыми. В отличие от client-side, которые можно проигнорировать через git commit --no-verify.

Когда они нужны

Server-side hooks устанавливают только админы репозитория. Типичные случаи:

  • Запрет force-push на main. Хотя обычно проще через branch protection на форджах (GitHub/GitLab/Bitbucket).
  • Проверка подписей коммитов - все коммиты должны быть подписаны GPG (gpg-signing).
  • Проверка через линтер всего pushed-кода, если local hooks могли быть пропущены.
  • Триггер CI/CD через post-receive (хотя обычно это webhook'ом, не hook'ом).

В мире форджей (GitHub/GitLab/Bitbucket)

Server-side hooks в чистом виде есть только в self-hosted bare-репах. На GitHub их нет - взамен GitHub Apps, branch protection, required status checks, CODEOWNERS. GitLab имеет push rules в Premium-тарифе. Bitbucket Server - да, настоящие hooks.

Если ты на GitHub - не пытайся «делать что-то на pre-receive», этого механизма у тебя нет. Используй branch protection и Actions/Apps.

14.4 Husky и pre-commit framework: команда вместо хаоса

Так как .git/hooks/ не клонируется, на проектах с несколькими разработчиками нужен способ держать hooks одинаковыми у всех. Два самых популярных решения: Husky (JavaScript-проекты) и pre-commit (универсальный, на Python).

Husky (для JS/TS-проектов)

Husky устанавливается через npm и регистрирует git-hooks, которые запускают скрипты из package.json:

json
// package.json
{
  "scripts": {
    "prepare": "husky install"
  },
  "devDependencies": {
    "husky": "^9.0.0"
  }
}
bash
# .husky/pre-commit
npx lint-staged

Когда новый разработчик делает npm install, скрипт prepare устанавливает husky, husky регистрирует hooks из .husky/. Теперь pre-commit одинаковый у всех.

В паре с lint-staged запускает linter только на staged файлах, а не на всём проекте. Это критично для скорости.

pre-commit framework

pre-commit - отдельный инструмент, написан на Python, но работает с любым языком. Конфиг лежит в .pre-commit-config.yaml:

yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff
      - id: ruff-format
  - 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-merge-conflict

Установка:

bash
pip install pre-commit
pre-commit install

pre-commit install создаёт .git/hooks/pre-commit, который запускает hooks из .pre-commit-config.yaml. После этого команда коммитит - hooks запускаются.

Плюсы pre-commit:

  • Огромный каталог готовых hooks (ruff, prettier, black, shellcheck, eslint, dozens of others).
  • rev: пинит версию hook'а. Воспроизводимость.
  • pre-commit autoupdate обновляет все hooks разом.
  • Работает на любом языке, не только Python.

Какой выбрать

ЧтоHuskypre-commit
Язык проектаJS/TSлюбой
Установкачерез npmчерез pip/brew
Конфигpackage.json + .husky/.pre-commit-config.yaml
Каталог готовых hooksчерез lint-staged + плагинысвой большой каталог
Версионирование hooksчерез package.jsonчерез rev: в конфиге

Если проект JS/TS - обычно husky привычнее. Если многоязычный или Python/Go/Rust/etc. - pre-commit.

В любом случае выбери что-то одно и зафиксируй в README: «после clone'а сделай npm install (или pre-commit install)». Без этого hooks не активируются у нового разработчика.

14.5 Что хорошо делать в hooks, а что - нет

Простое правило: hook должен быть быстрым и детерминированным.

Хорошо в pre-commit:

  • Формат кода (ruff format, prettier).
  • Лёгкий статический анализ (linter).
  • Проверка YAML/JSON на синтаксис.
  • Проверка, что нет конфликт-маркеров (<<<<<<<).
  • Запрет коммитить большие бинари.
  • Поиск секретов через secret-scanning.

Плохо в pre-commit:

  • Тесты. Тесты идут в CI, не в hook. Если тесты медленнее нескольких секунд - разработчик отключит hook через --no-verify и не вернётся.
  • Сетевые запросы. Hook должен работать офлайн.
  • Долгий статический анализ. mypy на всё дерево - пять минут, никому не нравится. Если он должен быть - в CI.
  • Сложные миграции. Изменение содержимого других файлов в ответ на изменение этого - обычно усложняет, а не помогает.

Правило 5 секунд

Pre-commit должен укладываться в 5 секунд на типичном изменении. Если дольше - разработчики начнут обходить через --no-verify, и hook потеряет смысл.

Способы оставаться быстрым:

  • Запускать только на staged файлах (git diff --cached --name-only), не на всём дереве. lint-staged (для JS) или опция files: в pre-commit framework делают это автоматически.
  • Кэшировать результаты. ruff, prettier, eslint умеют пропускать неизменившиеся файлы.
  • Не запускать тяжёлое в hook. Сложный typecheck - в CI.

Hooks ≠ заменитель CI

Hook - это «быстро поймать очевидные ошибки до коммита». CI - «полная проверка перед мержем». Они дополняют, не дублируют:

  • Hook отказывает в коммите с непрошедшим formatter'ом → ты исправляешь сразу, не доводишь до push'а.
  • CI прогоняет всё тесты + security scan + build → ты не мерджишь сломанное.

Если ты ставишь в hook то же, что и в CI - это медленно и нудно. Hooks - только лёгкая, быстрая проверка.

--no-verify

Команда коммита поддерживает git commit --no-verify (или -n). Это пропускает все client-side hooks. Полезно в одной ситуации: работа в emergency / hotfix-режиме, когда CI потом всё проверит и нужно зафиксировать состояние сейчас.

Опасно - если становится привычкой. Если ты постоянно --no-verify - значит hook раздражает; разбираться, починить hook или ослабить его, а не привыкать обходить.

14.5.1 Подводный камень: hook на rebase

Tricky случай: на git rebase Git'у нужно пересоздавать коммиты, и pre-commit на каждом из них (по умолчанию rebase запускает hooks) превращает большой rebase в утомительное упражнение. Отдельно от pre-commit есть свой pre-rebase hook, который срабатывает один раз в начале rebase, - его обычно не путают с pre-commit.

Если pre-commit делает что-то «правильное» вроде «удалить trailing whitespace» - он будет делать это снова и снова на каждом rebased коммите. На большом rebase это утомительно, и иногда приводит к конфликтам с самим собой.

Решения:

  • Отключить hooks на время rebase:
    git -c core.hooksPath=/dev/null rebase main
    Это пройдёт rebase, не запуская pre-commit/commit-msg.
  • Сделать hook идемпотентным. Если pre-commit run дважды на одном файле даёт одинаковый результат - повторные запуски безопасны.

Про git stash стоит знать отдельно: native --no-verify у stash нет (в Git 2.x команда такого флага не принимает). Если pre-commit мешает stash'у, отключай hooks через тот же core.hooksPath=/dev/null или временно убирай .git/hooks/pre-commit.

Это редкая боль, но регулярно появляется в больших rebase'ах. Если ты её заметил - знай, что лечение есть, и не списывай на «hooks плохие».

Уроки в sandbox

lab-14.1. Настроить pre-commit с ruff и prettier

Цель - пройти полный цикл «установить pre-commit, настроить два hook'а (ruff для Python и prettier для JSON/YAML), увидеть, как он отказывает в плохом коммите и как форматирует автоматически». В конце - попробовать обойти через --no-verify и решить, в каких сценариях это оправдано.

  1. Создай репо с парой плохо отформатированных файлов: mkdir -p /tmp/hooks-lab && cd /tmp/hooks-lab && git init && cat > app.py <<EOF def hello( name ): print('hi',name ) EOF cat > config.yaml <<EOF port: 8080 name:foo EOF git add . && git commit -m 'initial messy code'.

  2. Установи pre-commit framework: pip install pre-commit (или brew install pre-commit, если на macOS). Проверь: pre-commit --version. Должно показать версию.

  3. Создай конфиг .pre-commit-config.yaml:

    yaml
    repos:
      - repo: https://github.com/astral-sh/ruff-pre-commit
        rev: v0.8.0
        hooks:
          - id: ruff
            args: [--fix]
          - id: ruff-format
      - repo: https://github.com/pre-commit/mirrors-prettier
        rev: v3.1.0
        hooks:
          - id: prettier
            types_or: [yaml, json, markdown]

    Закоммить конфиг: git add .pre-commit-config.yaml && git commit -m 'add pre-commit config'.

  4. Активируй pre-commit: pre-commit install. Это создаст .git/hooks/pre-commit, который при коммите будет запускать наши hooks. Проверь: ls .git/hooks/pre-commit.

  5. Попробуй сделать новый коммит. Сначала измени файл: echo "# extra line" >> app.py && git add app.py && git commit -m 'edit app'. Pre-commit запустит ruff на app.py.

  6. Что увидишь: pre-commit скачает ruff (это разовый шаг), затем запустит. Ruff найдёт проблемы (лишние пробелы, неправильная отступа, неконсистентный кавычки) и исправит автоматически через --fix. Pre-commit пометит файл как «изменён hook'ом», и попросит снова git add.

  7. Снова: git add app.py && git commit -m 'edit app'. Теперь pre-commit пройдёт (файл чистый), коммит создастся.

  8. Аналогично проверь prettier: echo "key:val" >> config.yaml && git add config.yaml && git commit -m 'edit config'. Prettier исправит форматирование, попросит re-add.

  9. Сценарий обхода: хочется в emergency-режиме зафиксировать без проверок. echo 'broken=' >> config.yaml && git add config.yaml && git commit -m 'wip' --no-verify. Этот коммит пройдёт без pre-commit. Делай это редко и осознанно: всё, что обошло hook, рано или поздно поймает CI.

  10. Запусти pre-commit вручную на всех файлах: pre-commit run --all-files. Это полезно, чтобы пройтись по существующему коду после первой настройки и привести его в порядок.

  11. Итог: ты настроил автоматический форматтер + линтер, который ловит проблемы до коммита. Никаких ручных ruff format + prettier --write перед commit'ом.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Hook - это обычный исполняемый файл в .git/hooks/. Если возвращает 0 - операция продолжается, иначе - отменяется (для pre-hooks).
  • Client-side hooks (pre-commit, commit-msg, pre-push) живут локально, не клонируются. Server-side hooks (pre-receive, post-receive) - только на bare-репозитории, на GitHub их в чистом виде нет.
  • Чтобы hook'и были одинаковыми у команды - нужен фреймворк: Husky (для JS-проектов) или pre-commit framework (универсальный).
  • Pre-commit framework хранит конфиг в .pre-commit-config.yaml, пиннит версии каждого hook'а через rev:, имеет каталог готовых hooks для большинства языков.
  • Что хорошо в hook'е: format, лёгкий lint, проверка YAML/JSON/синтаксиса, поиск секретов. Плохо: тесты, сетевые запросы, тяжёлый анализ. Правило 5 секунд - иначе hook будут обходить.
  • Hooks ≠ CI. Hook ловит очевидное до коммита; CI делает полную проверку перед мержем. --no-verify оправдан в emergency, не в привычке.
  • На GitHub server-side hooks недоступны - используй branch protection, required status checks, CODEOWNERS, GitHub Apps.

Контрольные вопросы

  1. Я настроил pre-commit hook у себя, коммитнул, запушил. Коллега pullит - у него pre-commit не активируется. Почему?

    Показать ответ

    Потому что .git/hooks/ не клонируется. Hooks - это локальная политика разработчика, и Git намеренно их не шарит через repo. Чтобы политика была общей, нужен внешний механизм:

    • Husky (для JS): npm install автоматически активирует hooks через скрипт prepare.
    • pre-commit framework: после clone'а коллега делает pre-commit install один раз.

    Обычно в README прописывают: «после clone - выполни команду X для активации hooks». Если этого нет, hooks работают только у тех, кто сам их настроил, и качество кода будет разнородным.

  2. В pre-commit hook'е я хочу запустить полный pytest. Хорошо или плохо?

    Показать ответ

    Плохо. Тесты в pre-commit - антипаттерн, по двум причинам:

    1. Скорость. Полный pytest на нормальном проекте - это минуты, иногда десятки минут. На каждом коммите. Разработчики перестанут терпеть и начнут --no-verify всё подряд. Hook потеряет смысл.
    2. Дублирование с CI. Тесты гоняются в CI на каждый PR. Если pre-commit гоняет то же - это лишняя нагрузка локально на разработчика.

    В pre-commit оставь:

    • format (ruff format, prettier, gofmt),
    • lint только на изменённых файлах (быстро),
    • проверки syntax YAML/JSON,
    • поиск секретов.

    Тесты - в CI. Там у них своё окружение, своё время, и они не блокируют коммит-цикл разработчика.

  3. Можно ли запретить force-push в main через client-side hook?

    Показать ответ

    Можно, но это ненадёжная страховка. Pre-push hook вызывается перед push'ем и может отменить его. Но:

    1. Hook у каждого разработчика свой. Если коллега не настроил - защиты у него нет.
    2. git push --no-verify обходит pre-push hook (как и любой client hook).
    3. Hook не действует на push с другого клона или другой машины.

    Реальная защита - на стороне сервера: branch protection в GitHub/GitLab/Bitbucket. Включи «no force pushes» на main, и сервер физически откажется принимать force-push, независимо от того, что у разработчиков настроено. Pre-push hook полезен как дополнительная проверка, но не вместо protection.

  4. Pre-commit срабатывает при `git rebase`, каждый коммит формирует. На длинном rebase бесит. Что делать?

    Показать ответ

    Это поведение by design: каждый rebased коммит - это новый commit, и hook отрабатывает на нём. Два пути:

    1. Временно отключить hook на время rebase:

      git -c core.hooksPath=/dev/null rebase main

      Это для длинного rebase. После rebase verify-fix-amend цикл всё равно понадобится, чтобы hook прошёл на финальном состоянии.

    2. Сделать hook идемпотентным. Если ruff format запущен повторно на уже отформатированном файле - он ничего не меняет, hook проходит мгновенно. Это норма для нормальных форматтеров. Если ты ловишь «infinite loop hook'а на rebase» - у тебя hook не идемпотентен, и это его починить.

    Вариант 1 быстрее, вариант 2 - правильнее долгосрочно.

  5. Я в команде, мы хотим перейти на pre-commit framework. С чего начать?

    Показать ответ

    Минимальный план:

    1. Создай .pre-commit-config.yaml с самыми очевидными hooks: trailing-whitespace, end-of-file-fixer, check-yaml, check-merge-conflict, форматтер для основного языка (ruff/prettier/gofmt).
    2. Прогони pre-commit run --all-files на всём дереве. Это поправит весь существующий код. Сделай это отдельным большим PR - он не будет «логически содержательным», но обновит форматирование сразу везде.
    3. Закоммить .pre-commit-config.yaml в репо. Так все его видят.
    4. Добавь в README инструкцию: «после clone выполни pre-commit install». Без этого hooks не активируются.
    5. Добавь в CI шаг pre-commit run --all-files. Это защита от тех, кто забыл сделать install или сделал --no-verify.

    Шаги 2 и 5 - самые важные. Без шага 2 ты будешь долго разбирать «зачем мне эти 200 файлов с whitespace-правками» в каждом PR. Без шага 5 не будет гарантии, что hook реально работает у всех.

← Предыдущая13-submodules-worktreesСледующая →15-github-ui
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки