lesson ── git-labs ── ~20 мин ── 9 шагов
The goal is to bring up the pre-commit framework on a small repo, see how a hook
blocks a "bad" commit, how a hook fixes a file automatically, and how
--no-verify skips the check.
The sandbox is air-gapped, so you cannot reach github.com. On a real project, hooks
usually come from public repositories (repo: https://github.com/...),
but here you use repo: local with shell commands. The idea and the
framework interface are the same as in production.
The pre-commit framework is already installed in the image via pip (see
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
# double spaces, no space after ":" = typical fmt problems
cat > config.yaml <<EOF
port: 8080
name:foo
EOF
# printf instead of echo = write \n literally, no trailing newline
printf "name = 'world'\nprint('hi',name )\n" > app.pygit add . && git commit -m "initial messy code"
The files are badly formatted on purpose. Pre-commit will catch this in a moment.
✓ A repo with messy files is created.
which pre-commit # path to the binary in $PATH
pre-commit --version # framework version
It should print a path and version 4.0.1. If nothing is found, the image
was built without python+pre-commit, so rebuild sandbox-images/git-base/.
`which` shows the path to the binary. `pre-commit --version` shows its version.
✓ pre-commit found.
In the air-gapped sandbox you use repo: local. The hook is defined
entirely in the config: name, command, what it does. On a real project
you would write repo: https://github.com/pre-commit/pre-commit-hooks
and a list of id: from there, and the result is the same.
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"
Three hooks: the first two fix the file themselves, the third complains about FIXME
and does not let the commit through.
✓ The config is committed.
pre-commit install creates .git/hooks/pre-commit, which on
every commit will run the hooks from the config.
cd /home/student/work/hooks-lab
pre-commit install # creates .git/hooks/pre-commit -> runs the framework
ls .git/hooks/pre-commit
The file should appear.
✓ The hook is registered. Next, try a commit.
config.yaml has no final newline. end-of-file-fixer
will fix that. First change the file so pre-commit sees it
staged:
cd /home/student/work/hooks-lab
printf 'name:foo' > config.yaml # printf without \n = file with no final newline
git add config.yaml
git commit -m "edit config" # the hook adds a newline, the file changes, the commit is canceled
Pre-commit runs, adds the newline automatically,
marks the file as changed, and cancels the commit. That is normal:
the hook made a fix, so now you need a second git add.
If the commit went through, the hook did not fire; check .pre-commit-config.yaml.
✓ pre-commit did its job. The file is fixed and waiting for a re-add.
git add config.yaml # re-add the file the hook already fixed
git commit -m "edit config" # the repeat hook finds no problems, the commit goes through
pre-commit runs again. The file is already correct, the hook changes nothing, and the commit goes through.
The main rule is visible in action here: a hook is idempotent, so a second run on a fixed file does nothing.
✓ The commit is created. Next, try an explicit rejection.
The third hook (forbid-fixme) is not a fixer, it is a blocker. It does not
fix, it only refuses to let the commit through.
cd /home/student/work/hooks-lab
echo "# FIXME refactor later" >> app.py
git add app.py
git commit -m "wip: refactor app"
It should fail with block FIXME in committed code .... Failed.
The commit is not created. This is the second pre-commit pattern: some checks
cannot be fixed automatically, and the hook simply refuses.
If the commit went through, either the file has no FIXME or the hook is broken.
✓ The hook refused and the commit is blocked. That is the "guard" mode.
Sometimes you need to record a state urgently without checks. That is
--no-verify:
git commit --no-verify -m "wip emergency" # --no-verify skips pre-commit hooks
The commit went through without running pre-commit. This is an emergency mode, not a habit. In normal work, hooks should fire.
The rule: --no-verify is for real emergencies (an urgent hotfix
where you need to save something first). If you keep catching yourself
using --no-verify, fix the hook instead of getting used to bypassing it.
✓ You saw the bypass via --no-verify. Now the finale.
When you add pre-commit to an existing repo, it matters to run the hooks over all files at once, not wait for someone to fix them one by one:
pre-commit run --all-files # --all-files = run hooks over the whole tree, not only staged
pre-commit walks all files, fixes the ones it can fix automatically, and highlights the ones it cannot. You commit it in one PR, "cleanup formatting".
✓ pre-commit ran across the whole tree. Lesson complete.
pre-commit lives in .pre-commit-config.yaml, is activated by a single
command pre-commit install, and catches problems locally before a commit.
The main rule: a hook must be fast and idempotent.
команды
pre-commit installcreates .git/hooks/pre-commit, registers the frameworkpre-commit run --all-filesrun all hooks across the whole treegit commit --no-verify -m '...'skip hooks (emergency only)концепции