El mòdul anterior va acabar amb l'Ana revertint en producció un commit que trencava l'emmagatzematge de tasques. A la retrospectiva de l'equip va sortir una frase que es repeteix a tots els equips del món: "això ho hauríem vist si algú ho hagués mirat". I al cap de dos dies, el Bruno va pujar un console.log('AQUI!!') oblidat a app.js que va arribar fins a main.
Cap dels dos problemes és de Git. Són problemes de disciplina, i la disciplina humana falla a les set de la tarda d'un divendres. El que sí que pot fer Git és executar comprovacions automàticament en moments concrets: just abans de crear un commit, just després de fusionar, just abans d'enviar al servidor.
Aquests punts d'enganxament es diuen hooks, i són la primera de les eines d'aquest mòdul. Amb ells, gestor-tasques deixarà d'acceptar commits amb console.log, amb marcadors de conflicte sense resoldre o amb missatges d'una sola paraula. I de passada entendràs per què un hook de client mai és una mesura de seguretat, per més que a molta gent li agradaria que ho fos.
Contingut
- Què és un hook i on viu
- Els
.samplede fàbrica - Anatomia d'un hook: entrada, sortida i codi de retorn
- Taula dels hooks de client
- Un
pre-commitreal per agestor-tasques - Un
commit-msgque valida la forma del missatge - Més hooks útils:
post-checkout,post-merge,pre-push --no-verify: per què els hooks de client no són un control de seguretat- El problema de
.git/hooks: no es versiona core.hooksPathi els gestors de hooks- Els hooks de servidor, en una pàgina
- Què és un hook i on viu
Un hook és simplement un fitxer executable amb un nom concret dins de .git/hooks/. No hi ha registre, ni configuració, ni connector: si el fitxer existeix, té el nom exacte que Git espera i té permís d'execució, Git el llança en el moment corresponent.
applypatch-msg.sample pre-merge-commit.sample commit-msg.sample pre-push.sample fsmonitor-watchman.sample pre-rebase.sample post-update.sample pre-receive.sample prepare-commit-msg.sample push-to-checkout.sample pre-applypatch.sample update.sample pre-commit.sample sendemail-validate.sample
Recorda de la lliçó 01-04 que .git/ és el repositori de veritat: els objectes, les referències i la configuració. hooks/ és un subdirectori més d'aquest espai administratiu, i això té una conseqüència important que veurem a l'apartat 9: no forma part del contingut versionat.
Tres regles que governen tota la resta:
- El nom és exacte i sense extensió.
pre-commit, nopre-commit.shniprecommit. - Ha de ser executable.
chmod +x .git/hooks/pre-commit. És l'errada número u. - Pot estar escrit en qualsevol llenguatge. Git només l'executa; la línia
#!(shebang) decideix l'intèrpret. Farem servirbash, però un#!/usr/bin/env nodefunciona igual de bé.
- Els
.sample de fàbrica
.sample de fàbricaQuan fas git init (o git clone), Git copia un joc d'exemples a .git/hooks/. Tots acaben en .sample precisament perquè no s'executin: com que el nom no coincideix amb el que Git busca, es queden allà com a documentació viva.
Activar-ne un és treure-li el sufix:
cd ~/projectes/gestor-tasques
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commitVal la pena llegir el pre-commit.sample que porta Git, perquè fa alguna cosa útil de debò: detecta noms de fitxer no ASCII i espais en blanc al final de línia, mitjançant git diff-index --check. És un bon punt de partida.
D'on surten els
.sample. Es copien d'una plantilla, que per defecte és a/usr/share/git-core/templates/(Linux) o dins de la instal·lació de Git a macOS/Windows. Es pot canviar ambinit.templateDir, i aquesta és una manera —tosca, però real— que tots els teus repositoris nous neixin amb els mateixos hooks.
- Anatomia d'un hook: entrada, sortida i codi de retorn
Un hook rep informació de tres maneres possibles, segons quin sigui:
| Via | Exemple |
|---|---|
| Arguments de línia d'ordres | commit-msg rep la ruta del fitxer amb el missatge |
Entrada estàndard (stdin) |
pre-push rep una línia per cada referència que s'enviarà |
| El mateix repositori | Qualsevol hook pot llançar git diff, git log, etc. |
I retorna informació d'una sola manera: el seu codi de sortida.
exit 0→ tot correcte, Git continua.exitdiferent de 0 → en els hooks previs (pre-*), Git avorta l'operació. En els posteriors (post-*), Git ja ha fet la feina i el codi de sortida s'ignora a la pràctica.
Aquesta distinció és la que cal interioritzar:
flowchart LR
A["git commit"] --> B{"pre-commit"}
B -- "exit 0" --> C["prepare-commit-msg"]
B -- "exit != 0" --> X["Commit AVORTAT"]
C --> D["Editor del missatge"]
D --> E{"commit-msg"}
E -- "exit 0" --> F["Es crea el commit"]
E -- "exit != 0" --> X
F --> G["post-commit (informatiu)"]
Tot el que el hook escrigui a la sortida estàndard o d'error ho veu l'usuari al terminal. Per això un hook ben escrit no només falla: explica què ha fallat i com arreglar-ho.
- Taula dels hooks de client
Aquests són els que s'executen a la màquina de l'Ana, el Bruno o la Carla. Els de servidor són una altra família i els veiem a l'apartat 11.
| Hook | Quan s'executa | Què rep | Si retorna != 0 |
|---|---|---|---|
pre-commit |
Abans de demanar el missatge, amb l'índex ja preparat | Res | Avorta el commit |
prepare-commit-msg |
Després de generar el missatge per defecte, abans d'obrir l'editor | 1) ruta del fitxer de missatge 2) origen (message, template, merge, squash, commit) 3) SHA si escau |
Avorta el commit |
commit-msg |
Amb el missatge ja escrit, abans de crear l'objecte | 1) ruta del fitxer amb el missatge final | Avorta el commit |
post-commit |
Just després de crear el commit | Res | S'ignora (ja està fet) |
pre-rebase |
Abans d'iniciar un rebase | 1) branca base 2) branca que es rebasa (buit si és l'actual) | Impedeix el rebase |
post-checkout |
Després d'un git checkout/switch/clone que canviï l'arbre |
1) SHA anterior 2) SHA nou 3) indicador: 1 canvi de branca, 0 de fitxers |
S'ignora (llevat que impedeixi el clone) |
post-merge |
Després de completar una fusió amb èxit | 1) indicador: 1 si era una fusió squash |
S'ignora |
pre-push |
Abans de transferir objectes al remot | Args: 1) nom del remot 2) URL. Per stdin: <ref-local> <sha-local> <ref-remota> <sha-remot> per cada ref |
Avorta el push |
Alguns matisos que estalvien sorpreses:
pre-commitno veu el missatge, perquè encara no existeix. Si necessites el missatge, el teu hook éscommit-msg.pre-commitveu l'índex, no l'arbre de treball. És la distinció de les tres zones de la lliçó 01-03, i a l'apartat 5 veurem per què és crítica.prepare-commit-msges fa servir poc per validar i molt per emplenar: inserir el número d'incidència a partir del nom de la branca, per exemple.post-checkoutes dispara també en clonar, cosa que és útil per avisar de dependències que cal instal·lar.pre-rebaseés el mecanisme estàndard per protegir branques: "amainno s'hi rebasa mai" (la regla d'or de la lliçó 05-01, feta codi).
Existeixen més hooks de client que no cobrim aquí (applypatch-msg, pre-applypatch, post-rewrite, pre-auto-gc, post-index-change...). git help hooks els llista tots amb precisió; els vuit de la taula cobreixen el 95 % dels usos reals.
- Un
pre-commit real per a gestor-tasques
pre-commit real per a gestor-tasquesL'Ana escriu el hook que l'equip necessita. Requisits, sortits directament dels dos incidents:
- Cap
console.logal codi JavaScript que es confirma. - Cap marcador de conflicte sense resoldre (
<<<<<<<,=======,>>>>>>>), que és el que passa quan algú confirma enmig d'una fusió mal acabada (lliçó 03-05).
#!/usr/bin/env bash
#
# .git/hooks/pre-commit — gestor-tasques
# Rebutja console.log i marcadors de conflicte sense resoldre.
set -euo pipefail
# Colors només si la sortida és un terminal
if [ -t 1 ]; then
VERMELL=$'\033[31m'; GROC=$'\033[33m'; NEUTRE=$'\033[0m'
else
VERMELL=''; GROC=''; NEUTRE=''
fi
errors=0
# Fitxers AFEGITS, COPIATS o MODIFICATS que són a l'índex.
# --cached: mira l'índex, no l'arbre de treball.
# --diff-filter=ACM: ignora els esborrats (no té sentit analitzar-los).
# -z + read -d '': admet noms amb espais.
mapfile -d '' fitxers < <(git diff --cached --name-only --diff-filter=ACM -z)
if [ ${#fitxers[@]} -eq 0 ]; then
exit 0
fi
for f in "${fitxers[@]}"; do
# S'analitza el CONTINGUT DE L'ÍNDEX, no el del disc.
contingut=$(git show ":$f" 2>/dev/null) || continue
# 1. Marcadors de conflicte, en qualsevol fitxer de text
if printf '%s\n' "$contingut" | grep -qE '^(<{7}|={7}|>{7})( |$)'; then
echo "${VERMELL}✗ $f conté marcadors de conflicte sense resoldre.${NEUTRE}"
errors=1
fi
# 2. console.log, només en JavaScript
case "$f" in
*.js)
coincidencies=$(printf '%s\n' "$contingut" | grep -nE 'console\.(log|debug)\(' || true)
if [ -n "$coincidencies" ]; then
echo "${VERMELL}✗ $f conté crides a console.log:${NEUTRE}"
printf '%s\n' "$coincidencies" | sed 's/^/ /'
errors=1
fi
;;
esac
done
if [ "$errors" -ne 0 ]; then
echo
echo "${GROC}Commit avortat pel hook pre-commit.${NEUTRE}"
echo "Corregeix el que hi ha a dalt, torna a fer 'git add' i repeteix el commit."
echo "Si n'estàs absolutament segur: git commit --no-verify"
exit 1
fi
exit 0Instal·lació:
El que importa d'aquest script, línia a línia del que no és obvi:
-
set -euo pipefail.-eavorta si una ordre falla,-usi es fa servir una variable no definida,-o pipefailpropaga la fallada de qualsevol canonada. Sense això, una errada tipogràfica dins del hook fa que acabi amb èxit i no validi res — un hook trencat que diu "tot bé" és pitjor que no tenir-ne cap. -
git diff --cached --name-only --diff-filter=ACM. És la clau del hook.--cachedcompara l'índex ambHEAD, és a dir, exactament el que es confirmarà. Si fessis servir l'arbre de treball, ungit add -p(lliçó 02-04) que hagués preparat només mitja línia donaria falsos positius pel que va quedar fora.--diff-filter=ACMdeixa fora els fitxers esborrats. -
git show ":$f". La sintaxi:<ruta>significa "aquesta ruta a l'índex". És la mateixa notació de les etapes de conflicte de la lliçó 03-05 (:1:,:2:,:3:), amb l'etapa 0 implícita. Torna a ser el mateix principi: es valida el que es confirma, no el que hi ha al disc. -
grep -qE '^(<{7}|={7}|>{7})( |$)'. Els marcadors de conflicte són set caràcters al principi de línia, seguits d'un espai o del final de línia. Aquesta precisió evita que una línia de guions decorativa dispari una falsa alarma. -
|| truedesprés delgrepdeconsole.log.grepretorna 1 quan no troba res, i ambset -eaixò mataria l'script. El|| trueneutralitza aquest codi de sortida. -
El missatge d'error esmenta
--no-verify. És deliberat: un hook que bloqueja sense oferir sortida acaba desinstal·lat per algú que ja n'està fart. Val més que la sortida d'emergència sigui explícita i conscient.
Provant el hook:
echo "console.log('AQUI!!');" >> app.js
git add app.js
git commit -m "Afegeix el comptador de tasques pendents"✗ app.js conté crides a console.log:
142: console.log('AQUI!!');
Commit avortat pel hook pre-commit.
Corregeix el que hi ha a dalt, torna a fer 'git add' i repeteix el commit.
Si n'estàs absolutament segur: git commit --no-verifyEl commit no existeix. git log no ha canviat, l'índex continua preparat i n'hi ha prou de corregir i repetir.
Un hook lent és un hook que es desinstal·la. El
pre-commits'executa a cada commit. Si triga quaranta segons perquè llança tota la bateria de proves, l'equip començarà a fer servir--no-verifyper costum i hauràs perdut la partida. Regla pràctica: enpre-commit, comprovacions de menys de dos segons sobre els fitxers modificats. El que és pesat va apre-push(apartat 7) o a integració contínua (lliçó 07-06).
- Un
commit-msg que valida la forma del missatge
commit-msg que valida la forma del missatgeEl segon hook de l'equip comprova el missatge. Rep la ruta d'un fitxer temporal amb el missatge ja escrit, i el pot llegir, modificar o rebutjar.
#!/usr/bin/env bash
#
# .git/hooks/commit-msg — gestor-tasques
# Comprova la FORMA del missatge, no el seu contingut.
set -euo pipefail
fitxer_missatge="$1"
# Primera línia que no sigui comentari ni estigui buida
assumpte=$(grep -v '^#' "$fitxer_missatge" | sed '/^[[:space:]]*$/d' | head -n 1)
# Les fusions i reversions automàtiques es deixen passar
case "$assumpte" in
"Merge "*|"Revert "*|"fixup!"*|"squash!"*) exit 0 ;;
esac
if [ -z "$assumpte" ]; then
echo "✗ El missatge de commit és buit."
exit 1
fi
if [ ${#assumpte} -lt 15 ]; then
echo "✗ L'assumpte és massa curt (${#assumpte} caràcters, mínim 15)."
echo " Assumpte: '$assumpte'"
echo " Descriu QUÈ canvia, no 'apanys' ni 'canvis'."
exit 1
fi
if [ ${#assumpte} -gt 72 ]; then
echo "✗ L'assumpte és massa llarg (${#assumpte} caràcters, màxim 72)."
echo " Resumeix a la primera línia i amplia al cos, deixant"
echo " una línia en blanc entre tots dos."
exit 1
fi
if printf '%s' "$assumpte" | grep -qE '\.$'; then
echo "✗ L'assumpte no ha d'acabar en punt."
exit 1
fi
exit 0Amb ell instal·lat:
✗ L'assumpte és massa curt (6 caràcters, mínim 15). Assumpte: 'apanys' Descriu QUÈ canvia, no 'apanys' ni 'canvis'.
Dues observacions sobre el disseny d'aquest hook:
- Valida forma, no fons. Un script pot comprovar longitud, puntuació o que l'assumpte comenci per verb si segueixes una plantilla. No pot comprovar que el missatge sigui útil. Això ho fa la revisió de codi (lliçó 07-02).
- Deixa passar
Merge,Revert,fixup!isquash!. Són missatges generats per Git (lliçons 03-03, 05-06 i 05-02). Un hook que els rebutgi converteix cada fusió i cada--autosquashen una baralla.
Què ha de dir un bon missatge de commit —l'assumpte, el cos, l'imperatiu, els formats tipus Conventional Commits— és el tema complet de la lliçó 08-01: Escrivint Bons Missatges de Confirmació. Aquí només ens interessa el mecanisme:
commit-msgés el punt on aquestes convencions, siguin les que siguin, es poden fer complir automàticament. Quan arribem a 08-01 tindràs les regles; el hook per aplicar-les ja el saps escriure.
Un ús alternatiu, i molt pràctic, és modificar el missatge en lloc de rebutjar-lo. Amb prepare-commit-msg es pot afegir l'identificador de la incidència deduït del nom de la branca:
#!/usr/bin/env bash
# .git/hooks/prepare-commit-msg
# Si la branca és 'funcionalitat/GT-123-el-que-sigui', afegeix [GT-123] al final.
fitxer_missatge="$1"
origen="${2:-}"
# No tocar missatges de fusió, squash ni d'un commit reutilitzat (-C)
case "$origen" in
merge|squash|commit) exit 0 ;;
esac
branca=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
incidencia=$(printf '%s' "$branca" | grep -oE 'GT-[0-9]+' || true)
if [ -n "$incidencia" ] && ! grep -q "$incidencia" "$fitxer_missatge"; then
printf '\nRefs: %s\n' "$incidencia" >> "$fitxer_missatge"
fiLa Carla treballa a funcionalitat/GT-214-esborrat-multiple i els seus commits surten amb Refs: GT-214 sense que ella se n'hagi de recordar. Fixa't en git symbolic-ref --short HEAD: és la mateixa ordre de la lliçó 03-01 que retorna el nom de la branca actual, i aquí resol el problema d'una revolada.
- Més hooks útils:
post-checkout, post-merge, pre-push
post-checkout, post-merge, pre-pushpost-merge i post-checkout: avisar que cal reinstal·lar dependències.
#!/usr/bin/env bash
# .git/hooks/post-merge
if git diff-tree -r --name-only HEAD@{1} HEAD | grep -q '^package-lock.json$'; then
echo "⚠ package-lock.json ha canviat. Executa 'npm install'."
fiHEAD@{1} és el reflog: on era HEAD abans de la fusió. git diff-tree -r --name-only entre aquests dos punts llista els fitxers que han canviat. Si el fitxer de blocatge hi és, hi ha dependències noves. És un avís, no un blocatge: els hooks post-* no poden avortar res.
pre-push: el que és pesat, aquí.
pre-push és on han d'anar les comprovacions que triguen, perquè s'executa un cop cada molts commits. Rep per stdin una línia per referència:
<ref-local> <sha-local> <ref-remota> <sha-remot> refs/heads/main a1b2c3d... refs/heads/main e5f6a7b...
#!/usr/bin/env bash
#
# .git/hooks/pre-push — gestor-tasques
# Impedeix enviar a main commits marcats com a WIP o fixup!.
set -euo pipefail
remot="$1"
url="$2"
sha_buit="0000000000000000000000000000000000000000"
while read -r ref_local sha_local ref_remota sha_remot; do
# Esborrat de branca: res a comprovar
[ "$sha_local" = "$sha_buit" ] && continue
# Només ens importa main
[ "$ref_remota" = "refs/heads/main" ] || continue
if [ "$sha_remot" = "$sha_buit" ]; then
rang="$sha_local" # branca nova al remot
else
rang="$sha_remot..$sha_local"
fi
sospitosos=$(git rev-list --grep='^\(WIP\|fixup!\|squash!\)' "$rang")
if [ -n "$sospitosos" ]; then
echo "✗ Hi ha commits WIP/fixup! sense consolidar en el que enviaràs a main:"
git log --oneline --grep='^\(WIP\|fixup!\|squash!\)' "$rang" | sed 's/^/ /'
echo
echo " Consolida'ls amb: git rebase -i --autosquash origin/main"
exit 1
fi
done
exit 0Aquest hook fa servir tot el del mòdul 5: els rangs A..B (lliçó 02-06), els commits fixup! (lliçó 05-02) i el --autosquash com a solució suggerida. I respecta el cas d'una branca que encara no existeix al remot, en què sha_remot ve a zeros.
--no-verify: per què els hooks de client no són un control de seguretat
--no-verify: per què els hooks de client no són un control de seguretatTot l'anterior es salta amb una opció:
--no-verify (o -n a git commit) desactiva pre-commit, commit-msg i prepare-commit-msg en el commit, i pre-push en l'enviament. I ni tan sols cal: com que el hook és un fitxer del disc de l'usuari, qualsevol el pot esborrar, editar o treure-li el permís d'execució.
D'aquí la conclusió que cal gravar a foc:
Els hooks de client són una ajuda, no una barrera. Protegeixen del despistament, no de la voluntat. Qualsevol regla que s'hagi de complir s'aplica al servidor, no al portàtil de ningú.
| Hook de client | Comprovació al servidor / CI | |
|---|---|---|
| On s'executa | Màquina del desenvolupador | Servidor Git o plataforma de CI |
| Es pot saltar? | Sí, amb --no-verify o esborrant el fitxer |
No |
| Es distribueix en clonar? | No (apartat 9) | Sí, és únic i central |
| Velocitat percebuda | Ha de ser immediata | Pot trigar minuts |
| Moment de l'avís | Abans de crear el commit: barat de corregir | Després d'enviar: més car |
| Paper correcte | Detectar el despistament aviat | Garantir que la regla es compleix |
Tots dos són complementaris, i aquesta és la manera sana de plantejar-ho: el hook local t'estalvia el viatge d'anada i tornada al servidor; el servidor és el que decideix de veritat. Un --no-verify puntual i conscient —per exemple, per desar feina a mitges en una branca personal— és perfectament legítim. El que no és legítim és que sigui el costum.
- El problema de
.git/hooks: no es versiona
.git/hooks: no es versionaL'Ana té un pre-commit estupend. El Bruno fa git clone i... no el té. La Carla tampoc.
.git/hooks/ és dins de .git/, i .git/ no es clona com a contingut. En clonar es transfereixen objectes i referències (lliçó 04-01), no el directori administratiu de l'altre. Cada repositori neix amb els seus .sample de fàbrica i res més.
flowchart TD
subgraph Ana
A1["Arbre de treball<br/>app.js, index.html..."]
A2[".git/hooks/pre-commit ✅"]
end
subgraph Servidor["git.exemple.cat"]
S1["Objectes i referències"]
end
subgraph Bruno
B1["Arbre de treball<br/>app.js, index.html..."]
B2[".git/hooks/ només .sample ❌"]
end
A1 -->|push| S1
S1 -->|clone| B1
A2 -.->|"NO viatja"| B2
Hi ha tres maneres de resoldre-ho, de pitjor a millor.
A. A mà. Desar els hooks en un directori versionat (eines/hooks/) i que cadascú els copiï:
Funciona, es documenta al README.md i s'oblida el primer dia. Serveix per a equips de dues persones i poca cosa més.
B. core.hooksPath. És la solució nativa de Git des de la versió 2.9, i la millor si no vols dependències externes.
C. Un gestor de hooks. És l'estàndard en projectes amb ecosistema (npm, Python...). Totes dues es veuen a l'apartat següent.
core.hooksPath i els gestors de hooks
core.hooksPath i els gestors de hookscore.hooksPath
Aquesta opció li diu a Git que busqui els hooks en un altre directori, que sí que pot estar versionat:
A partir d'aquí, .git/hooks/ s'ignora del tot i Git executa eines/hooks/pre-commit, eines/hooks/commit-msg, etc.
L'estructura de gestor-tasques queda així:
gestor-tasques/
├── app.js
├── estils.css
├── index.html
├── README.md
└── eines/
└── hooks/
├── commit-msg
├── pre-commit
├── prepare-commit-msg
└── pre-pushEls hooks s'editen, es revisen en una pull request i evolucionen amb el projecte, com qualsevol altre fitxer. Amb dos matisos:
- Els permisos d'execució sí que es versionen. Git desa el bit executable en el mode de l'entrada de l'arbre (lliçó 01-04:
100755davant de100644). Si afegeixes un hook sensechmod +x, fes-ho després ambgit update-index --chmod=+x eines/hooks/pre-commit. core.hooksPathés configuració local, i la configuració no es clona (lliçó 01-05). Continua calent que cada persona executi una vegada elgit config. La diferència és que ara és una sola ordre en lloc de mantenir còpies sincronitzades: es posa alREADME.mdo en un scripteines/installa.sh.
#!/usr/bin/env bash
# eines/installa.sh — executar una vegada després de clonar
git config core.hooksPath eines/hooks
chmod +x eines/hooks/*
echo "Hooks de gestor-tasques instal·lats."Compte amb l'àmbit. Si el poses amb
--globalafectarà tots els teus repositoris i trencarà els que depenguin dels seus propis hooks.core.hooksPathgairebé sempre ha de ser local al repositori.
Gestors de hooks
En projectes amb un ecosistema de paquets al darrere, l'habitual és delegar en una eina:
| Eina | Ecosistema | Com funciona | Quan encaixa |
|---|---|---|---|
| Husky | Node.js / npm | Fixa core.hooksPath a .husky/ en un script d'instal·lació; cada hook és un fitxer curt en aquest directori |
Projectes JS/TS, com gestor-tasques |
| pre-commit (marc de treball en Python) | Multillenguatge | Un .pre-commit-config.yaml declara quines comprovacions cal executar; l'eina instal·la el hook i gestiona els entorns |
Equips amb molts linters o diversos llenguatges |
| Lefthook | Multillenguatge (binari Go) | Configuració YAML, executa tasques en paral·lel | Repositoris grans on importa la velocitat |
core.hooksPath a pèl |
Cap | Scripts propis versionats | Sense dependències, control total, més feina manual |
Amb Husky, el pre-commit de gestor-tasques quedaria a .husky/pre-commit i només hauria d'invocar la comprovació:
lint-staged aplica el linter només als fitxers preparats, que és la mateixa idea del git diff --cached de l'apartat 5, resolta per un altre. Aquest és l'argument de fons per fer servir un gestor: no tornar a inventar la lògica de "quins fitxers són a l'índex", "com restaurar si falla" o "com desar els entorns a la memòria cau".
I l'argument en contra, igual de real: afegeixes una dependència i una capa d'indirecció per a una cosa que al capdavall és un script de vint línies. Per a gestor-tasques, core.hooksPath és suficient i l'equip entén exactament què s'executa.
- Els hooks de servidor, en una pàgina
Com va quedar clar a l'apartat 8, el que s'ha de complir sempre es comprova al costat del servidor. Allà hi ha una altra família de hooks, que viuen al hooks/ del repositori bare (lliçó 04-01):
| Hook | Quan | Què rep | Si retorna != 0 |
|---|---|---|---|
pre-receive |
Un cop, abans d'acceptar res del push | Per stdin: <sha-vell> <sha-nou> <ref> per cada ref |
Rebutja el push sencer |
update |
Un cop per cada referència | Args: <ref> <sha-vell> <sha-nou> |
Rebutja aquella referència; les altres poden passar |
post-receive |
Després d'acceptar, amb tot ja actualitzat | Igual que pre-receive, per stdin |
S'ignora |
Usos típics: pre-receive per rebutjar force pushes sobre main o commits de més de X megues; update per permetre que només certes persones creïn etiquetes; post-receive per notificar un xat, disparar un desplegament o avisar un sistema de tiquets.
Dues precisions importants:
- Requereixen accés al servidor. En un servidor propi s'editen directament; en una plataforma allotjada (GitHub, GitLab, Bitbucket) no tens accés a
hooks/, i l'equivalent són els seus mecanismes propis: branch protection rules, push rules, required status checks i webhooks. - Aquest és justament el terreny de la integració contínua. Com es configura una canonada que executi les proves a cada enviament i a cada pull request, què és un status check obligatori i com es connecta tot això amb el flux de treball de l'equip és el contingut de la lliçó 07-06: Integració Contínua amb Git. Aquí n'hi ha prou de saber que existeixen, on viuen i per què són el lloc correcte per a les regles innegociables.
Errors Habituals i Consells
Error 1: oblidar el chmod +x. Git no avisa: simplement no executa el hook, i tu et penses que la teva validació funciona. Comprova amb ls -l .git/hooks/ que hi apareix la x. És, de llarg, l'errada més freqüent.
Error 2: deixar-li l'extensió. pre-commit.sh no s'executa mai. El nom ha de ser exactament el que Git espera.
Error 3: validar l'arbre de treball en lloc de l'índex. Si el teu pre-commit llegeix els fitxers del disc, un git add -p parcial donarà falsos positius o —pitjor— falsos negatius. Fes servir git diff --cached i git show :<ruta>.
Error 4: donar per fet que els hooks es clonen. No es clonen. Sense core.hooksPath o un gestor, tu tens la validació i els teus companys no.
Error 5: hooks lents a pre-commit. La bateria completa de proves a cada commit garanteix que l'equip acabi fent servir --no-verify per sistema. El que és ràpid a pre-commit, el que és lent a pre-push, el que és definitiu a CI.
Error 6: confiar en els hooks de client com a control de seguretat. Se salten amb una opció. Les regles obligatòries van al servidor.
Error 7: un hook que falla sense explicar per què. Un exit 1 mut és la pitjor experiència possible. Digues quin fitxer, quina línia i com s'arregla.
Error 8: bloquejar els missatges automàtics. Un commit-msg que rebutgi Merge branch ... o fixup! fa impossible fusionar i fer servir --autosquash. Dona-li una excepció explícita.
Consell 1: prova el hook abans de confiar-hi. El pots executar a mà amb l'índex preparat: .git/hooks/pre-commit; echo "sortida: $?".
Consell 2: escriu hooks defensius. set -euo pipefail, tractament de noms amb espais (-z + mapfile -d '') i || true allà on una ordre pugui retornar un codi no nul legítimament.
Consell 3: registra les excepcions. Si algú fa servir --no-verify, que sigui una decisió, no un hàbit. Un post-commit pot deixar constància local de quan es va saltar una comprovació.
Consell 4: comença per un de sol. Un pre-commit que evita console.log i marcadors de conflicte ja justifica l'eina. Afegir sis hooks el primer dia és la via ràpida al rebuig.
Consell 5: git help hooks és la referència definitiva. Està instal·lada a la teva màquina, és exacta per a la teva versió de Git i documenta cada argument de cada hook.
Exercicis
Exercici 1: un pre-commit que impedeix confirmar TODO:
En un repositori de proves, escriu un hook pre-commit que:
- Rebutgi el commit si algun fitxer
.jspreparat conté la cadenaTODO:. - Mostri el fitxer i el número de línia de cada coincidència.
- Analitzi el contingut de l'índex, no el de l'arbre de treball.
- Demostra que funciona: crea un fitxer amb un
TODO:, prepara'l i intenta confirmar-lo. Després demostra que--no-verifyse'l salta.
Exercici 2: commit-msg amb prefix obligatori
Escriu un hook commit-msg que exigeixi que l'assumpte comenci per un d'aquests prefixos: feat:, fix:, docs: o refactor:. Ha de:
- Rebutjar
Actualitza el READMEi acceptardocs: actualitza el README. - Deixar passar els missatges que comencin per
Merge,Revert,fixup!osquash!. - Mostrar la llista de prefixos vàlids quan rebutgi.
Exercici 3: hooks versionats amb core.hooksPath
Partint dels dos hooks anteriors:
- Mou-los a
eines/hooks/dins del repositori i confirma'ls. - Configura
core.hooksPathperquè Git els faci servir. - Verifica que continuen funcionant i que
.git/hooks/pre-commitja no s'executa (deixa'l amb unechodiferent per comprovar-ho). - Clona el repositori en un altre directori i comprova què passa: s'executen els hooks al clon? Què hi falta?
Solucions
Solució 1:
cat > .git/hooks/pre-commit <<'FI'
#!/usr/bin/env bash
set -euo pipefail
errors=0
mapfile -d '' fitxers < <(git diff --cached --name-only --diff-filter=ACM -z)
for f in "${fitxers[@]:-}"; do
case "$f" in
*.js)
trobat=$(git show ":$f" | grep -nE 'TODO:' || true)
if [ -n "$trobat" ]; then
echo "✗ $f conté TODO: sense resoldre"
printf '%s\n' "$trobat" | sed 's/^/ línia /'
errors=1
fi
;;
esac
done
[ "$errors" -eq 0 ] || { echo "Commit avortat."; exit 1; }
exit 0
FI
chmod +x .git/hooks/pre-commitprintf 'function desa() {\n // TODO: validar les dades\n}\n' > app.js
git add app.js
git commit -m "Afegeix la funció de desament"El hook no ha intervingut: --no-verify se l'ha saltat del tot. Aquesta és exactament la demostració de l'apartat 8.
Solució 2:
cat > .git/hooks/commit-msg <<'FI'
#!/usr/bin/env bash
set -euo pipefail
assumpte=$(grep -v '^#' "$1" | sed '/^[[:space:]]*$/d' | head -n 1)
case "$assumpte" in
"Merge "*|"Revert "*|"fixup!"*|"squash!"*) exit 0 ;;
esac
if ! printf '%s' "$assumpte" | grep -qE '^(feat|fix|docs|refactor): .+'; then
echo "✗ L'assumpte ha de començar per un prefix vàlid."
echo " Prefixos: feat: fix: docs: refactor:"
echo " Rebut: '$assumpte'"
exit 1
fi
exit 0
FI
chmod +x .git/hooks/commit-msg✗ L'assumpte ha de començar per un prefix vàlid. Prefixos: feat: fix: docs: refactor: Rebut: 'Actualitza el README'
I l'excepció de les fusions:
git switch -c branca-prova
echo "x" > x.txt && git add . && git commit -m "feat: afegeix el fitxer x"
git switch main
git merge --no-ff branca-prova -m "Merge branch 'branca-prova'"El missatge Merge branch ... no compleix el patró de prefixos, però l'excepció el deixa passar.
Solució 3:
mkdir -p eines/hooks
git mv .git/hooks/pre-commit eines/hooks/pre-commit 2>/dev/null \
|| cp .git/hooks/pre-commit eines/hooks/pre-commit
cp .git/hooks/commit-msg eines/hooks/commit-msg
chmod +x eines/hooks/*
# Marquem el hook vell per distingir-lo
printf '#!/usr/bin/env bash\necho "AQUEST ÉS EL HOOK VELL DE .git/hooks"\nexit 1\n' \
> .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
git add eines
git commit --no-verify -m "feat: afegeix els hooks versionats del projecte"git config core.hooksPath eines/hooks
printf 'const a = 1; // TODO: revisar\n' > altre.js
git add altre.js
git commit -m "feat: afegeix el modul altre"El missatge és el del hook versionat, no el "AQUEST ÉS EL HOOK VELL": core.hooksPath ha desviat la cerca per complet.
# El clon
cd /tmp
git clone /tmp/practica-hooks practica-hooks-clon
cd practica-hooks-clon
ls eines/hooks/
git config --get core.hooksPathEls fitxers dels hooks sí que han viatjat, perquè estan versionats. El que no ha viatjat és la configuració core.hooksPath, que és local (lliçó 01-05). Per això cal el pas d'instal·lació:
I aquesta és la raó de ser d'eines/installa.sh: convertir el "copia aquests quatre fitxers i dona'ls permisos" en una sola ordre que s'executa un cop després de clonar.
Conclusió
Els hooks converteixen Git en alguna cosa més que un magatzem d'historial: en un lloc on enganxar automatismes. L'essencial:
- Un hook és un fitxer executable amb un nom exacte a
.git/hooks/. Sense registre, sense connectors: si hi és i és executable, s'executa. - Els
.samplede fàbrica estan desactivats pel sufix i serveixen de documentació; treure'ls el.sampleels activa. - La comunicació és simple: arguments o
stdind'entrada, codi de sortida de sortida. En els hookspre-*, un codi diferent de zero avorta l'operació; en elspost-*només informa. - Els vuit hooks de client de la taula cobreixen gairebé tot:
pre-commiticommit-msgper validar el que entra,prepare-commit-msgper emplenar,pre-pushper al que és car, ipost-checkout/post-merge/post-commitper avisar. - Un
pre-commitha d'analitzar l'índex (git diff --cached,git show :<ruta>), no l'arbre de treball, i ha de ser ràpid. --no-verifys'ho salta tot, i el fitxer és al disc de l'usuari: els hooks de client ajuden contra el despistament, no són un control de seguretat. El que és obligatori es comprova al servidor..git/hooks/no es versiona. La solució nativa éscore.hooksPathapuntant a un directori del repositori; la de l'ecosistema, un gestor tipus Husky, pre-commit o Lefthook.- Els hooks de servidor (
pre-receive,update,post-receive) són els que sí que obliguen; el seu desenvolupament pràctic, juntament amb les canonades de comprovació automàtica, és a la lliçó 07-06. Les convencions concretes dels missatges que validacommit-msg, a la 08-01.
Amb els hooks, l'equip de gestor-tasques ja impedeix que certes fallades entrin a l'historial. Però queden les que hi van entrar abans d'instal·lar-los, i aquest és un problema diferent: algú reporta que l'esborrat de tasques ha deixat de funcionar, funcionava segur a la versió 1.0, i entre aquella etiqueta i avui hi ha més de dos-cents commits. Ningú sap quin el va trencar.
Revisar-los un a un són dues-centes proves. Però si l'historial és una seqüència ordenada en què alguna cosa va passar de funcionar a no funcionar, hi ha una tècnica que resol això en vuit proves en lloc de dues-centes. La veiem a la lliçó 06-02: Git Bisect.
Dominant Git: De Principiant a Avançat
Mòdul 1: Introducció a Git
- Què és Git?
- Instal·lant Git
- Terminologia Bàsica de Git
- El Model de Dades de Git
- Configurant Git
- Configuració Inicial
Mòdul 2: Operacions Bàsiques de Git
- Creant un Repositori
- Clonant un Repositori
- Flux de Treball Bàsic de Git
- Preparant i Confirmant Canvis
- Inspeccionant Canvis amb git diff
- Visualitzant l'Historial de Confirmacions
Mòdul 3: Branques i Fusió
- Entenent les Branques
- Creant i Canviant Branques
- Fusionant Branques
- Estratègies de Fusió
- Resolent Conflictes de Fusió
- Gestió de Branques
Mòdul 4: Treballant amb Repositoris Remots
- Entenent els Repositoris Remots
- Afegint un Repositori Remot
- Autenticació amb Repositoris Remots
- Obtenint i Baixant Canvis
- Enviant Canvis
- Rastrejant Branques
Mòdul 5: Operacions Avançades de Git
- Rebase
- Rebase Interactiu
- Cherry-Picking de Confirmacions
- Desant Canvis Temporals
- Etiquetant Confirmacions
- Revertint Confirmacions
Mòdul 6: Eines i Tècniques de Git
- Usant Git Hooks
- Git Bisect
- Git Blame
- Git Log i Àlies
- Submòduls de Git
- Múltiples Còpies de Treball amb git worktree
Mòdul 7: Estratègies de Col·laboració i Flux de Treball
- Forks i Pull Requests
- Revisions de Codi amb Git
- Flux de Treball Git Flow
- GitHub Flow
- Trunk Based Development
- Integració Contínua amb Git
Mòdul 8: Bones Pràctiques i Consells de Git
- Escrivint Bons Missatges de Confirmació
- Mantenint un Historial Net
- Ignorant Fitxers amb .gitignore
- Atributs de Fitxer amb .gitattributes
- Bones Pràctiques de Seguretat
- Consells de Rendiment
Mòdul 9: Resolució de Problemes i Depuració
- Problemes Habituals de Git
- Desfent Canvis
- Resolent Divergències amb el Remot
- Recuperant Confirmacions Perdudes
- Tractant amb Repositoris Corruptes
- Tècniques Avançades de Depuració
