lesson ── git-labs ── ~20 мин ── 9 шагов
Цель - поднять pre-commit framework на маленьком репо, увидеть как hook
блокирует «плохой» коммит, как hook автоматически правит файл и как
--no-verify обходит проверку.
Sandbox air-gapped - до github.com не дойдёшь. В реальном проекте hooks
обычно прилетают из публичных репозиториев (repo: https://github.com/...),
здесь же используем repo: local с shell-командами. Идея и интерфейс
framework те же, что и в проде.
pre-commit framework уже установлен в образе через pip (см.
sandbox-images/git-base/Dockerfile).
интерактивный sandbox
Поднимется контейнер gitlab/git-base с git, bash, pre-commit. В браузере откроется терминал, можно сразу git init. Каждый шаг проверяется автоматически. Сеть air-gapped, github.com недоступен.
stack ── git · bash · 256 MB RAM · air-gapped · самоуничтожается через 30 мин простоя
cd /home/student/work
mkdir -p hooks-lab && cd hooks-lab
git init -b main
# двойные пробелы, нет пробела после ":" - типичные fmt-проблемы
cat > config.yaml <<EOF
port: 8080
name:foo
EOF
# printf вместо echo - чтобы записать \n буквально, без трейлинг newline
printf "name = 'world'\nprint('hi',name )\n" > app.pygit add . && git commit -m "initial messy code"
Файлы плохо отформатированы намеренно. Pre-commit это сейчас поймает.
✓ Репо с messy-файлами создано.
which pre-commit # путь до бинаря в $PATH
pre-commit --version # версия фреймворка
Должно вывести путь и версию 4.0.1. Если не нашло - образ
собран без python+pre-commit, пересобери sandbox-images/git-base/.
`which` показывает путь до бинаря. `pre-commit --version` - его версию.
✓ pre-commit найден.
В air-gapped sandbox используем repo: local. Hook определяется
целиком в конфиге: имя, команда, что делает. На реальном проекте
ты бы написал repo: https://github.com/pre-commit/pre-commit-hooks
и список id: оттуда - результат тот же.
cd /home/student/work/hooks-lab
cat > .pre-commit-config.yaml <<'EOF'
repos:
- repo: local
hooks:
- id: end-of-file-fixer
name: ensure file ends with newline
language: system
entry: bash -c 'for f in "$@"; do [ -s "$f" ] && [ "$(tail -c1 "$f" | wc -l)" -eq 0 ] && echo "" >> "$f"; done' --
types: [text]
- id: trailing-whitespace
name: strip trailing whitespace
language: system
entry: bash -c 'for f in "$@"; do sed -i "s/[[:space:]]*$//" "$f"; done' --
types: [text]
- id: forbid-fixme
name: block FIXME in committed code
language: system
entry: bash -c 'for f in "$@"; do grep -nH "FIXME" "$f" && exit 1; done; exit 0' --
types: [text]
EOF
git add .pre-commit-config.yaml
git commit -m "add pre-commit config"
Три hook'а: первые два чинят файл сами, третий ругается на FIXME
и не пропускает коммит.
✓ Конфиг закоммичен.
pre-commit install создаёт .git/hooks/pre-commit, который при
каждом коммите будет запускать hooks из конфига.
cd /home/student/work/hooks-lab
pre-commit install # создаёт .git/hooks/pre-commit -> запуск фреймворка
ls .git/hooks/pre-commit
Файл должен появиться.
✓ Hook зарегистрирован. Дальше попробуем коммит.
У config.yaml нет финального переноса строки. end-of-file-fixer
это поправит. Сначала измени файл, чтобы pre-commit увидел его
staged:
cd /home/student/work/hooks-lab
printf 'name:foo' > config.yaml # printf без \n - файл без финального newline
git add config.yaml
git commit -m "edit config" # hook добавит newline, файл изменится, коммит отменится
Pre-commit запустится, добавит перенос строки автоматически,
пометит файл изменённым и отменит коммит. Это нормально:
hook поправил, теперь нужен повторный git add.
Если коммит прошёл - hook не сработал, проверь .pre-commit-config.yaml.
✓ pre-commit отработал. Файл поправлен, ждёт re-add.
git add config.yaml # пере-add уже исправленного hook'ом файла
git commit -m "edit config" # повторный hook не находит проблем, коммит проходит
pre-commit запустится снова. Файл уже корректный, hook ничего не меняет, коммит проходит.
Главное правило здесь видно в действии: hook идемпотентен - повторный запуск на исправленном файле ничего не делает.
✓ Коммит создан. Дальше - попробуем явный отказ.
Третий hook (forbid-fixme) - не fixer, а блокирующий. Он не
исправляет, он только не пропускает.
cd /home/student/work/hooks-lab
echo "# FIXME refactor later" >> app.py
git add app.py
git commit -m "wip: refactor app"
Должен упасть с block FIXME in committed code .... Failed.
Коммит не создан. Это второй паттерн pre-commit: некоторые проверки
нельзя автоматически починить, и hook просто отказывает.
Если коммит прошёл - либо в файле нет FIXME, либо hook сломан.
✓ Hook отказал, коммит заблокирован. Это и есть «защитный» режим.
Иногда нужно срочно зафиксировать состояние без проверок. Это
--no-verify:
git commit --no-verify -m "wip emergency" # --no-verify пропускает pre-commit hooks
Коммит прошёл без запуска pre-commit. Это аварийный режим, не привычка. В нормальной работе hooks должны срабатывать.
Правило: --no-verify для реальных emergency (срочный hotfix,
перед которым надо что-то сохранить). Если регулярно ловишь себя
на --no-verify - чини hook, а не привыкай его обходить.
✓ Обход через --no-verify увиден. Теперь финал.
Когда добавляешь pre-commit в существующий репо, важно прогнать hooks по всем файлам разом - не ждать, пока кто-то по одному правит:
pre-commit run --all-files # --all-files = прогнать hooks по всему дереву, не только staged
pre-commit пройдёт по всем файлам, чинимые автоматически - починит, нечинимые - подсветит. Закоммитишь одним PR «cleanup formatting».
✓ pre-commit прогнан по всему дереву. Урок пройден.
pre-commit живёт в .pre-commit-config.yaml, активируется одной
командой pre-commit install, ловит проблемы локально до commit.
Главное правило: hook должен быть быстрым и идемпотентным.
команды
pre-commit installсоздаёт .git/hooks/pre-commit, регистрирует frameworkpre-commit run --all-filesпрогнать все hooks на всём деревеgit commit --no-verify -m '...'пропустить hooks (emergency only)концепции