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.

Odhadovaná délka studia: 45 minut · Stav: nehotovo

Technologie: Git

Ú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 prefix feat:/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

  1. Jaký je rozdíl mezi client-side hooky (pre-commit, pre-push) a
    server-side hooky (pre-receive, post-receive)?
  2. Proč nelze spoléhat jen na lokální pre-commit hook jako jedinou
    ochranu proti nekvalitnímu kódu v hlavní větvi?
  3. Jak zajistíte, aby stejný pre-commit hook fungoval automaticky u
    všech členů týmu po naklonování repozitáře?
  4. Napište jednoduchý commit-msg hook (pseudokód nebo shell), který
    odmítne commit, pokud zpráva commitu nezačíná jedním z prefixů
    feat:, fix:, docs: nebo chore:.

Lekce na sebe nejsou zamčené — libovolnou lekci můžete otevřít i označit jako hotovou v jakémkoliv pořadí.