Хуки и подписи коммитов

Как автоматически прогонять проверки перед коммитом и пушем, криптографически подписывать коммиты и теги и раскапывать причины багов через blame и log.

Git hook — исполняемый скрипт, который Git сам запускает в определённый момент (перед коммитом, перед пушем и т.д.); если он завершается ненулевым кодом, операция отменяется. Подпись коммита — криптографическая гарантия (GPG или SSH-ключом), что автором изменения действительно были вы.

Зачем это нужно на практике

Линтер, который вы «забыли запустить», секрет, случайно попавший в коммит, или сломанные тесты, уехавшие в общую ветку, — всё это ловится до того, как мусор окажется в истории. Хуки автоматизируют дисциплину: правила выполняются машиной, а не силой воли. Подписи решают другую задачу — доверие: в открытых и корпоративных репозиториях важно доказать, что коммит от «Alice» сделала именно Alice, а не кто-то, подставивший её имя в user.name.

Хуки: pre-commit и pre-push

Хуки лежат в .git/hooks/. Там есть примеры с суффиксом .sample; чтобы хук заработал, создайте файл без суффикса и сделайте его исполняемым. pre-commit запускается до создания коммита — идеальное место для линтера и быстрых проверок:

#!/usr/bin/env bash
# .git/hooks/pre-commit — блокирует коммит при ошибках линтера
files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
[ -z "$files" ] && exit 0
if ! ruff check $files; then
  echo "✗ Линтер нашёл ошибки — коммит отменён" >&2
  exit 1
fi
exit 0
chmod +x .git/hooks/pre-commit   # без этого Git хук не запустит

pre-push срабатывает перед отправкой и подходит для более тяжёлых проверок — например, полного прогона тестов, чтобы не пушить красное:

#!/usr/bin/env bash
# .git/hooks/pre-push — не пускать пуш, если тесты падают
if ! pytest -q; then
  echo "✗ Тесты не прошли — push заблокирован" >&2
  exit 1
fi

Важный нюанс: папка .git/hooks/ не коммитится, поэтому хуки не разъезжаются с репозиторием автоматически. Чтобы делиться ими в команде, хранят скрипты в отдельной папке под версионным контролем (например, .githooks/) и переключают путь:

git config core.hooksPath .githooks   # искать хуки в версионируемой папке

На практике для общих хуков часто берут менеджер вроде pre-commit или husky, но под капотом они делают ровно то же — кладут исполняемые скрипты в точки pre-commit/pre-push.

Подпись коммитов и тегов

Git умеет подписывать коммиты двумя видами ключей: классический GPG и современный SSH-ключ (тот же, которым вы пушите). Настройка SSH-подписи проще:

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true   # подписывать ВСЕ коммиты автоматически

Для GPG настройка похожа, только формат другой:

git config --global user.signingkey ABCDEF1234567890   # id GPG-ключа
git config --global commit.gpgsign true

Разово подписать конкретный коммит или тег можно флагом -S:

git commit -S -m "release: v2.0.0"   # подписанный коммит
git tag -s v2.0.0 -m "signed release"  # подписанный тег

Проверить подписи в истории и у тега:

git log --show-signature -1   # показать статус подписи последнего коммита
git verify-tag v2.0.0         # проверить подпись тега

На GitHub/GitLab подписанные коммиты помечаются плашкой Verified, если публичная часть ключа добавлена в профиль. Это и есть практический смысл подписи: визуальная и проверяемая гарантия авторства.

blame и log-археология для расследований

Когда баг найден, нужно понять, кто, когда и зачем написал подозрительную строку. git blame показывает для каждой строки файла коммит, автора и дату:

git blame -L 40,60 src/auth.py   # авторство строк 40-60
git blame -w -M src/auth.py      # -w игнорирует пробелы, -M видит перемещения

Флаги важны: -w не даёт «форматировщику» перетянуть на себя авторство (игнорирует изменения пробелов), а -M отслеживает строки, перемещённые внутри файла. Дальше — раскопки логом. Поиск коммитов по тексту изменения (когда появилась или исчезла строка) делает «кирка» -S:

git log -S "calculate_tax" -- src/billing.py   # где трогали этот идентификатор
git log -p -- src/auth.py                        # история файла с дифами
git log --oneline --follow -- src/old_name.py    # история сквозь переименования

Связка железная: blame даёт хеш виновной строки, git show <хеш> — полный контекст того коммита (сообщение, все изменённые файлы, diff), а git log -S — когда конкретная логика вообще появилась в проекте. Вместе с git bisect из первого урока это полный набор для расследования регрессий: bisect находит какой коммит сломал, blame/show — почему.

Как это работает под капотом

Хук — это не магия, а обычный файл, который Git ищет по фиксированному имени в core.hooksPath (по умолчанию .git/hooks/) и просто запускает в нужный момент жизненного цикла. Решение «продолжать или нет» принимается по коду возврата процесса: 0 — продолжить, иначе — прервать. Поэтому хуки клиентские (живут на вашей машине) и не являются защитой сервера: их можно обойти флагом --no-verify. Серверную гарантию дают защищённые ветки и серверные хуки на стороне GitHub/GitLab.

Подпись же встраивается в сам объект коммита: при создании Git считает хеш содержимого и шифрует его вашим приватным ключом, добавляя блок подписи в коммит. Любой, у кого есть ваш публичный ключ, пересчитывает хеш и сверяет — совпало значит коммит не подменён и сделан владельцем ключа. Именно поэтому подделать имя в user.name легко, а подделать подпись без приватного ключа — нет.

Частые ошибки

  • Хук не исполняемый. Забыли chmod +x — Git молча его не запускает. Проверьте права файла.
  • Надежда на клиентские хуки как на защиту. Их обходят git commit --no-verify. Критичные правила дублируйте серверными проверками/CI.
  • Хуки не доезжают до команды. .git/hooks/ не версионируется; используйте core.hooksPath и папку в репозитории.
  • Подпись настроена, но не Verified. Публичный ключ не добавлен в профиль GitHub/GitLab, либо email коммита не совпадает с email ключа.
  • blame обвиняет форматировщик. Без -w и -M авторство уезжает к тому, кто переформатировал или переместил код, а не к автору логики.

Итоги

  • Хуки pre-commit/pre-push автоматизируют проверки; решение принимается по коду возврата, файл должен быть исполняемым.
  • .git/hooks/ не коммитится — для команды используйте core.hooksPath и версионируемую папку.
  • Клиентские хуки обходятся --no-verify; настоящая защита — серверные проверки и защищённые ветки.
  • Подпись (commit.gpgsign true, ключ GPG или SSH) криптографически доказывает авторство; на хостингах это плашка Verified.
  • git blame -w -M, git log -S и git show — связка для археологии истории и поиска причины бага.
Проверьте себя
1. По какому признаку Git решает, прерывать ли операцию из-за хука pre-commit?
AПо имени файла хука
BПо коду возврата скрипта: 0 — продолжить, ненулевой — отменить
CПо размеру вывода скрипта
DПо дате изменения хука
2. Почему клиентский pre-commit хук нельзя считать надёжной защитой репозитория?
AОн работает только на Windows
BЕго легко обойти флагом git commit --no-verify; гарантию даёт сервер (защищённые ветки/CI)
CХуки замедляют push в 10 раз
DGit удаляет хуки при каждом clone специально
3. Что криптографически гарантирует подпись коммита (GPG или SSH-ключом)?
AЧто код в коммите не содержит багов
BЧто коммит сделан владельцем приватного ключа и содержимое не подменено
CЧто коммит нельзя откатить
DЧто коммит зашифрован и его не прочитать без ключа
4. Зачем для расследования полезны флаги -w и -M в git blame?
A-w ускоряет blame, -M включает многопоточность
B-w игнорирует изменения пробелов, -M отслеживает перемещённые строки — авторство не уезжает к форматировщику
CОни показывают только ваши коммиты
DОни нужны, чтобы blame работал на бинарных файлах