Modul 3 — Verzovací systémy: Git
15. Git hooks a automatizace na úrovni repozitáře
Co jsou Git hooky, jaký je rozdíl mezi lokálními a server-side hooky a jak jimi automatizovat kontroly kvality kódu.
Úvod a kontext
V předchozích lekcích jsme viděli, jak Git zaznamenává historii a jak
týmy organizují spolupráci. Git ale umí i automaticky reagovat na
události v repozitáři — spustit vlastní skript těsně před commitem,
po commitu, před pushem apod. Tento mechanismus se nazývá Git
hooks a je základním stavebním kamenem automatizace kvality kódu
přímo na úrovni repozitáře, ještě předtím, než se změna vůbec dostane
do CI pipeline na serveru.
Teorie
Co je Git hook
Git hook je spustitelný skript (shell, Python, cokoliv s "shebang"
řádkem), uložený ve složce .git/hooks/ uvnitř repozitáře, který Git
automaticky spustí v přesně definovaném okamžiku životního cyklu
commitu nebo pushe. Git při inicializaci repozitáře vytvoří v této
složce sadu vzorových souborů s příponou .sample — aby hook fungoval,
je potřeba soubor přejmenovat (odstranit .sample) a nastavit mu
právo ke spuštění (chmod +x).
Lokální (client-side) hooky
Spouští se na stroji vývojáře, jde tedy o kontrolu ještě předtím, než
se cokoliv dostane na sdílený server:
pre-commit— spustí se před vytvořením commitu; pokud skript
skončí s nenulovým návratovým kódem, commit se nevytvoří. Typicky
se zde spouští linter, formátovač kódu nebo rychlé jednotkové
testy.commit-msg— spustí se po napsání zprávy commitu, dostane
cestu k souboru se zprávou jako argument; typicky ověřuje formát
zprávy (např. vyžaduje prefixfeat:/fix:podle Conventional
Commits).pre-push— spustí se před odesláním commitů na vzdálený
repozitář; vhodné místo pro spuštění celé testovací sady, protože
push je poslední lokální kontrolní bod před sdílením změny.
Server-side hooky
Spouští se na serveru hostujícím repozitář (nebo v ekvivalentní
podobě jako "server-side" kontroly na GitHubu/GitLabu — tam se jim
říká spíš branch protection rules a webhooks, ne přímo Git hooky, ale
princip je stejný):
pre-receive— spustí se na serveru, než přijme nové commity;
umí push zcela odmítnout (např. pokud commit obsahuje zapomenuté
heslo nebo nesplňuje formát zprávy). Typické využití: vynutit
pravidla, která nejde spolehnout jen na lokální hooky vývojářů (ty
totiž lze při commitu obejít přepínačem--no-verify).post-receive— spustí se po úspěšném přijetí pushe; typicky
spouští navazující automatizaci (např. notifikaci, spuštění
deploye, aktualizaci zrcadla repozitáře).
Proč lokální hooky nestačí jako jediná ochrana
Lokální hooky jsou pohodlné pro rychlou zpětnou vazbu (chyba se odhalí
hned, ne až po odeslání na server), ale nejsou vynutitelné — vývojář
je má jen ve své lokální kopii .git/hooks/ (ta se git clone
automaticky nekopíruje) a navíc je může kdykoliv obejít přepínačem
git commit --no-verify. Z toho plyne důležitý princip: lokální
hooky jsou pomocník pro vývojáře, ne bezpečnostní hranice — skutečné
vynucení pravidel (povinné testy, povinná revize) patří na server
(pre-receive hooky, nebo v praxi spíš CI pipeline a branch protection
pravidla popsaná v modulu o CI/CD).
Sdílení hooků v týmu
Protože se .git/hooks/ nekopíruje s klonováním repozitáře, běžný
způsob, jak hooky sdílet v týmu, je buď je nakonfigurovat mimo
.git a nasměrovat na ně Git příkazem git config core.hooksPath,
nebo použít nástroj jako pre-commit (framework, ne jen hook se
stejným názvem) či Husky (populární v JavaScriptovém ekosystému),
které hooky instalují automaticky jako součást standardního nastavení
projektu (npm install, pip install pre-commit && pre-commit install).
Praktický příklad
Ruční pre-commit hook, který odmítne commit, pokud kód obsahuje
zapomenutý print() ladicí výpis nebo neprojde lintem:
$ cat .git/hooks/pre-commit
#!/bin/sh
# Odmítne commit, pokud staged Python soubory obsahují "print(" nebo neprojdou flake8.
if git diff --cached --name-only --diff-filter=ACM | grep -q '\.py$'; then
if git diff --cached | grep -q '^+.*print('; then
echo "Commit odmítnut: nalezen zapomenutý print() v Python souboru."
exit 1
fi
flake8 $(git diff --cached --name-only --diff-filter=ACM -- '*.py') || exit 1
fi
exit 0
$ chmod +x .git/hooks/pre-commit
Vyzkoušení, že hook skutečně blokuje problémový commit:
$ echo 'print("debug")' >> app.py
$ git add app.py
$ git commit -m "Přidat funkci"
Commit odmítnut: nalezen zapomenutý print() v Python souboru.
Konfigurace sdíleného hooksPath, aby hooky ze složky ve verzovaném
projektu (ne v .git/) fungovaly pro všechny po naklonování:
$ mkdir -p .githooks
$ mv .git/hooks/pre-commit .githooks/pre-commit
$ git config core.hooksPath .githooks
$ git add .githooks
$ git commit -m "Sdílet pre-commit hook v repozitáři"
Nový člen týmu po naklonování repozitáře už jen spustí
git config core.hooksPath .githooks (typicky zdokumentováno v
README nebo automatizováno instalačním skriptem), a hook se mu
aktivuje bez ručního kopírování souborů.
Ukázka konfigurace populárního frameworku pre-commit (Python),
který stejnou myšlenku standardizuje napříč projekty:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
$ pip install pre-commit
$ pre-commit install # zaregistruje hooky do .git/hooks/pre-commit
Shrnutí
Git hooky jsou skripty spouštěné automaticky v definovaných bodech
životního cyklu commitu a pushe. Lokální hooky (pre-commit,
commit-msg, pre-push) dávají vývojáři rychlou zpětnou vazbu, ale
lze je obejít a nesdílejí se automaticky s klonováním — proto se pro
opravdové vynucení pravidel používají server-side mechanismy
(pre-receive hooky nebo ekvivalentní CI/branch-protection pravidla
na GitHubu/GitLabu). Sdílení hooků v týmu se řeší buď přes
core.hooksPath, nebo přes dedikované nástroje jako pre-commit či
Husky.
Kontrolní otázky
- Jaký je rozdíl mezi client-side hooky (
pre-commit,pre-push) a
server-side hooky (pre-receive,post-receive)? - Proč nelze spoléhat jen na lokální
pre-commithook jako jedinou
ochranu proti nekvalitnímu kódu v hlavní větvi? - Jak zajistíte, aby stejný
pre-commithook fungoval automaticky u
všech členů týmu po naklonování repozitáře? - Napište jednoduchý
commit-msghook (pseudokód nebo shell), který
odmítne commit, pokud zpráva commitu nezačíná jedním z prefixů
feat:,fix:,docs:nebochore:.
Lekce na sebe nejsou zamčené — libovolnou lekci můžete otevřít i označit jako hotovou v jakémkoliv pořadí.