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

  1. Què és un hook i on viu
  2. Els .sample de fàbrica
  3. Anatomia d'un hook: entrada, sortida i codi de retorn
  4. Taula dels hooks de client
  5. Un pre-commit real per a gestor-tasques
  6. Un commit-msg que valida la forma del missatge
  7. Més hooks útils: post-checkout, post-merge, pre-push
  8. --no-verify: per què els hooks de client no són un control de seguretat
  9. El problema de .git/hooks: no es versiona
  10. core.hooksPath i els gestors de hooks
  11. Els hooks de servidor, en una pàgina

  1. 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.

cd ~/projectes/gestor-tasques
ls .git/hooks/
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, no pre-commit.sh ni precommit.
  • 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 servir bash, però un #!/usr/bin/env node funciona igual de bé.

  1. Els .sample de fàbrica

Quan 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-commit

Val 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 amb init.templateDir, i aquesta és una manera —tosca, però real— que tots els teus repositoris nous neixin amb els mateixos hooks.

  1. 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.
  • exit diferent 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.

  1. 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-commit no veu el missatge, perquè encara no existeix. Si necessites el missatge, el teu hook és commit-msg.
  • pre-commit veu 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-msg es 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-checkout es 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: "a main no 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.

  1. Un pre-commit real per a gestor-tasques

L'Ana escriu el hook que l'equip necessita. Requisits, sortits directament dels dos incidents:

  1. Cap console.log al codi JavaScript que es confirma.
  2. 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 0

Instal·lació:

chmod +x .git/hooks/pre-commit

El que importa d'aquest script, línia a línia del que no és obvi:

  • set -euo pipefail. -e avorta si una ordre falla, -u si es fa servir una variable no definida, -o pipefail propaga 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. --cached compara l'índex amb HEAD, és a dir, exactament el que es confirmarà. Si fessis servir l'arbre de treball, un git add -p (lliçó 02-04) que hagués preparat només mitja línia donaria falsos positius pel que va quedar fora. --diff-filter=ACM deixa 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.

  • || true després del grep de console.log. grep retorna 1 quan no troba res, i amb set -e això mataria l'script. El || true neutralitza 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-verify

El 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-commit s'executa a cada commit. Si triga quaranta segons perquè llança tota la bateria de proves, l'equip començarà a fer servir --no-verify per costum i hauràs perdut la partida. Regla pràctica: en pre-commit, comprovacions de menys de dos segons sobre els fitxers modificats. El que és pesat va a pre-push (apartat 7) o a integració contínua (lliçó 07-06).

  1. Un commit-msg que valida la forma del missatge

El 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 0

Amb ell instal·lat:

git commit -m "apanys"
✗ 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! i squash!. 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 --autosquash en 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"
fi

La 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.

  1. Més hooks útils: post-checkout, post-merge, pre-push

post-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'."
fi

HEAD@{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 0

Aquest 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.

  1. --no-verify: per què els hooks de client no són un control de seguretat

Tot l'anterior es salta amb una opció:

git commit --no-verify -m "això no passa el hook i m'és igual"
git push --no-verify

--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? , 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.

  1. El problema de .git/hooks: no es versiona

L'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ï:

cp eines/hooks/* .git/hooks/ && chmod +x .git/hooks/*

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.

  1. core.hooksPath i els gestors de hooks

core.hooksPath

Aquesta opció li diu a Git que busqui els hooks en un altre directori, que sí que pot estar versionat:

# Al repositori, amb els hooks ja a eines/hooks/
git config core.hooksPath eines/hooks

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-push

Els 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: 100755 davant de 100644). Si afegeixes un hook sense chmod +x, fes-ho després amb git 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 el git config. La diferència és que ara és una sola ordre en lloc de mantenir còpies sincronitzades: es posa al README.md o en un script eines/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 --global afectarà tots els teus repositoris i trencarà els que depenguin dels seus propis hooks. core.hooksPath gairebé 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ó:

#!/usr/bin/env sh
npx lint-staged

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.

  1. 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:

  1. Rebutgi el commit si algun fitxer .js preparat conté la cadena TODO:.
  2. Mostri el fitxer i el número de línia de cada coincidència.
  3. Analitzi el contingut de l'índex, no el de l'arbre de treball.
  4. Demostra que funciona: crea un fitxer amb un TODO:, prepara'l i intenta confirmar-lo. Després demostra que --no-verify se'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:

  1. Rebutjar Actualitza el README i acceptar docs: actualitza el README.
  2. Deixar passar els missatges que comencin per Merge , Revert , fixup! o squash!.
  3. Mostrar la llista de prefixos vàlids quan rebutgi.

Exercici 3: hooks versionats amb core.hooksPath

Partint dels dos hooks anteriors:

  1. Mou-los a eines/hooks/ dins del repositori i confirma'ls.
  2. Configura core.hooksPath perquè Git els faci servir.
  3. Verifica que continuen funcionant i que .git/hooks/pre-commit ja no s'executa (deixa'l amb un echo diferent per comprovar-ho).
  4. Clona el repositori en un altre directori i comprova què passa: s'executen els hooks al clon? Què hi falta?

Solucions

Solució 1:

mkdir /tmp/practica-hooks && cd /tmp/practica-hooks
git init -b main
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-commit
printf 'function desa() {\n  // TODO: validar les dades\n}\n' > app.js
git add app.js
git commit -m "Afegeix la funció de desament"
✗ app.js conté TODO: sense resoldre
    línia 2:  // TODO: validar les dades
Commit avortat.
git commit --no-verify -m "Afegeix la funció de desament"
git log --oneline
[main (root-commit) 7c2a9f1] Afegeix la funció de desament
 1 file changed, 3 insertions(+)
7c2a9f1 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
echo "# gestor-tasques" > README.md && git add README.md
git commit -m "Actualitza el README"
✗ L'assumpte ha de començar per un prefix vàlid.
  Prefixos: feat:  fix:  docs:  refactor:
  Rebut: 'Actualitza el README'
git commit -m "docs: actualitza el README amb les instruccions d'arrencada"
[main 3f8b1c4] docs: actualitza el README amb les instruccions d'arrencada

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'"
Merge made by the 'ort' strategy.

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"
✗ altre.js conté TODO: sense resoldre
    línia 1:const a = 1; // TODO: revisar
Commit avortat.

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.hooksPath
commit-msg  pre-commit
(sense sortida)

Els 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ó:

git config core.hooksPath eines/hooks

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 .sample de fàbrica estan desactivats pel sufix i serveixen de documentació; treure'ls el .sample els activa.
  • La comunicació és simple: arguments o stdin d'entrada, codi de sortida de sortida. En els hooks pre-*, un codi diferent de zero avorta l'operació; en els post-* només informa.
  • Els vuit hooks de client de la taula cobreixen gairebé tot: pre-commit i commit-msg per validar el que entra, prepare-commit-msg per emplenar, pre-push per al que és car, i post-checkout/post-merge/post-commit per avisar.
  • Un pre-commit ha d'analitzar l'índex (git diff --cached, git show :<ruta>), no l'arbre de treball, i ha de ser ràpid.
  • --no-verify s'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 és core.hooksPath apuntant 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 valida commit-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

Mòdul 2: Operacions Bàsiques de Git

Mòdul 3: Branques i Fusió

Mòdul 4: Treballant amb Repositoris Remots

Mòdul 5: Operacions Avançades de Git

Mòdul 6: Eines i Tècniques de Git

Mòdul 7: Estratègies de Col·laboració i Flux de Treball

Mòdul 8: Bones Pràctiques i Consells de Git

Mòdul 9: Resolució de Problemes i Depuració

Mòdul 10: Git al Món Real

© Copyright 2026. Tots els drets reservats