El mòdul anterior va acabar amb un diagnòstic incòmode: l'equip de gestor-tasques ja té un procés impecable —forks, pull requests, revisions, branques protegides, integració contínua— i tot i així pot acabar amb un historial que ningú sigui capaç de llegir. Comencem a corregir-ho pel punt més quotidià i més descurat de tots: el missatge de confirmació.

Aquesta lliçó salda per fi dos deutes del curs. A la lliçó 02-04 vam aprendre a confirmar canvis amb git commit -m i vam dir que les convencions de missatges arribarien aquí. A la lliçó 06-01 vam escriure un hook commit-msg que exigia un tiquet amb format GT-NNN, però validàvem una convenció que encara no havíem explicat. I a la 07-06 vam validar aquesta mateixa convenció al CI. És el moment d'explicar d'on surt tot això.

Convé entendre la magnitud de l'assumpte abans de començar. Un desenvolupador escriu entre cinc i vint missatges de confirmació al dia. Són l'únic text en prosa que produeix el projecte de manera sistemàtica, l'únic que sobreviu als refactors i l'únic que hi serà quan l'autor original ja no sigui a l'empresa. El codi diu què fa el programa; l'historial és l'únic lloc on pot quedar escrit per què ho fa així.

Contingut

  1. Per a qui s'escriu un missatge de confirmació
  2. El perquè importa més que el què
  3. Anatomia d'un bon missatge
  4. La línia d'assumpte: les set regles
  5. El cos: motivació i context
  6. El peu: referències, tiquets i coautoria
  7. Missatges dolents i la seva versió bona
  8. Conventional Commits
  9. BREAKING CHANGE i la versió derivada
  10. Plantilles amb commit.template
  11. Escriure a l'editor, no amb -m
  12. Fer complir la convenció: hook i CI
  13. L'idioma del missatge

  1. Per a qui s'escriu un missatge de confirmació

Hi ha una resposta intuïtiva i equivocada: "per a mi, per recordar què vaig fer". És equivocada perquè tu, d'aquí a una setmana, ja no necessitaràs el missatge: tindràs el context fresc. El missatge existeix per a tres lectors que no el tenen.

Lector 1: el teu jo d'aquí a dos anys. No recorda res del context. No recorda la reunió on es va decidir allò, ni el client que es va queixar, ni la limitació del navegador que va obligar a fer una cosa estranya. El commit és l'únic que queda.

Lector 2: qui executi git blame sobre una línia estranya. És el cas més freqüent i el més valuós. Algú troba a app.js una condició absurda:

// Mai no es filtra per data si el navegador és Safari < 15
if (!esSafariAntic()) { aplicaFiltreData(); }

El seu primer impuls serà esborrar-la. A la lliçó 06-03 vam veure que git blame respon quin commit va introduir aquella línia. Si aquell commit diu apanys, el lector esborrarà la condició i reintroduirà la fallada. Si diu Evita el filtre per data a Safari 14: el seu Intl.DateTimeFormat retorna la zona horària en un format que trenca l'anàlisi, no l'esborrarà. La qualitat del missatge decideix si la fallada torna.

Lector 3: qui revisi la teva pull request. A la lliçó 07-02 vam veure que revisar bé exigeix entendre la intenció abans que el diff. Una PR amb commits atòmics i ben descrits es revisa commit a commit i en la meitat de temps. Una PR amb vuit commits anomenats wip, wip2 i ara sí obliga el revisor a llegir un diff gegant sense cap guia.

Hi ha un quart lector que a més és una màquina: les eines que generen changelogs, dedueixen versions i filtren l'historial. D'això tracta l'apartat 8.

flowchart LR
    C["Commit<br/>+ missatge"] --> A["El teu jo futur<br/>(2 anys)"]
    C --> B["git blame<br/>sobre una línia estranya"]
    C --> D["Revisor de la PR"]
    C --> E["Eines:<br/>changelog, SemVer"]

  1. El perquè importa més que el què

Aquesta és la idea central de tota la lliçó, i la que més costa interioritzar.

El diff ja diu el què. Hi és, complet, exacte i per sempre. git show te'l mostra amb precisió de caràcter. Un missatge que diu Canvia el color del botó a blau quan el diff diu - color: #c00; / + color: #06c; és redundant: no aporta ni un bit d'informació nova.

El que el diff no pot dir mai:

El diff sap El diff no sap
Quines línies han canviat Per què calia canviar-les
Quins fitxers s'han tocat Quines alternatives es van descartar i per què
El valor nou d'una constant D'on surt aquest valor
Que s'ha afegit una condició Quina fallada concreta prevé
Que s'ha esborrat una funció Si era codi mort o si s'ha mogut
Que la versió d'una dependència ha pujat Si va ser per una vulnerabilitat o per una funcionalitat

Compara aquests dos missatges sobre exactament el mateix diff de gestor-tasques:

Puja el temps d'espera a 30 segons
Puja el temps d'espera de sincronització a 30 s

El valor anterior de 5 segons venia del prototip, quan la
sincronització només enviava les tasques modificades. Des de
GT-118 enviem també els adjunts, i en connexions mòbils lentes
el primer enviament d'una tasca amb imatge supera de llarg aquest
límit: l'usuari veu un error i reintenta, duplicant la tasca.

30 segons cobreixen el percentil 99 dels temps mesurats en
preproducció durant una setmana. No el pugem més perquè a partir
d'aquí l'usuari assumeix que l'aplicació s'ha penjat.

Refs: GT-134

El primer és informació que ja era al diff. El segon conté quatre fets que no són en cap altra banda del repositori: d'on venia el valor vell, quina fallada concreta provocava, com es va triar el nou i per què no és més gran. D'aquí a dos anys, quan algú proposi tornar-lo a abaixar "perquè 30 segons és una barbaritat", aquell missatge és la resposta.

La regla: si el teu missatge es pot deduir llegint el diff, encara no has escrit el missatge.

  1. Anatomia d'un bon missatge

Git no imposa cap format, però sí que tracta el text de manera estructurada, i gairebé totes les eines de l'ecosistema assumeixen la mateixa convenció. L'estructura canònica té tres parts:

Resumeix el canvi en 50 caràcters o menys

Cos explicatiu, ajustat a 72 columnes. Explica la motivació del
canvi i el context necessari per entendre'l. El què és al diff;
aquí hi va el perquè.

Pot tenir diversos paràgrafs, separats per línies en blanc.

- I llistes, si aclareixen
- Per exemple, per enumerar-ne els efectes

Refs: GT-134
Co-authored-by: Bruno Salas <[email protected]>
flowchart TD
    A["Línia d'assumpte<br/>≤ 50 caràcters, imperatiu, sense punt"] --> B["Línia en blanc<br/>(obligatòria)"]
    B --> C["Cos<br/>72 columnes · motivació i context"]
    C --> D["Línia en blanc"]
    D --> E["Peu<br/>Refs, Co-authored-by, BREAKING CHANGE"]

La línia en blanc entre l'assumpte i el cos no és estètica: és sintaxi. Git fa servir la primera línia com a assumpte a git log --oneline, al %s de --pretty, a l'assumpte dels correus de git format-patch i al títol per defecte d'una pull request. Si te la saltes, el missatge sencer es converteix en assumpte:

# MALAMENT: sense línia en blanc
git commit -m "Corregeix el filtre
El filtre no distingia majúscules."

git log --oneline -1
a1b2c3d Corregeix el filtre El filtre no distingia majúscules.

Tot el text enganxat, i el --oneline inservible.

  1. La línia d'assumpte: les set regles

Regla 1: 50 caràcters o menys

No és una xifra màgica arbitrària: és el límit pràctic perquè git log --oneline, la llista de commits d'una plataforma i el git shortlog càpiguen en una línia sense truncar-se. Les plataformes d'allotjament tallen visualment al voltant dels 72 caràcters, i GitHub avisa a partir de 50.

Si no cap en 50 caràcters, gairebé sempre és una d'aquestes dues coses: el commit fa massa coses (ho veurem a la lliçó 08-02) o estàs posant a l'assumpte allò que va al cos.

Regla 2: en mode imperatiu

Escriu com si donessis una ordre al repositori. La prova infal·lible és completar aquesta frase:

"Si s'aplica, aquest commit _______"

Malament
~~Afegit~~ el filtre per etiqueta Afegeix el filtre per etiqueta
~~Afegint~~ el filtre per etiqueta Afegeix el filtre per etiqueta
~~He arreglat~~ el desament Arregla el desament en local
~~Apanys diversos~~ Corregeix el desbordament en mòbil

No és caprici: el mateix Git escriu així. Merge branch 'x', Revert "...", Initial commit. Si els teus missatges fan servir un altre temps verbal, l'historial barreja dues veus.

Regla 3: sense punt final

És un títol, no una frase. I en 50 caràcters, un caràcter compta.

Regla 4: majúscula inicial

Llevat que facis servir Conventional Commits (apartat 8), on la convenció habitual és minúscula després dels dos punts. Tria'n una i sigues coherent.

Regla 5: sigues específic

Corregeix l'error no diu res. Corregeix l'esborrat de tasques amb adjunt sí.

Regla 6: no repeteixis el nom del fitxer

Canvia app.js és inútil: git log --stat ja ho diu. L'àmbit, si cal, va amb la sintaxi de Conventional Commits.

Regla 7: si necessites "i", probablement són dos commits

Afegeix el filtre per etiqueta i corregeix el marge del peu descriu dos canvis independents. La lliçó 08-02 desenvolupa aquesta idea a fons.

  1. El cos: motivació i context

El cos és opcional per a un canvi trivial i obligatori per a qualsevol canvi no obvi. S'ajusta manualment a 72 columnes per una raó molt concreta: git log sagna el cos amb quatre espais, així que 72 + 4 = 76, i cap en un terminal de 80 columnes sense que Git reajusti el text (no ho fa: Git mai no reformata el teu missatge).

Un cos útil respon aquestes preguntes, no necessàriament totes:

  1. Quin era el problema o la necessitat? L'estat anterior i per què no servia.
  2. Per què aquesta solució? I sobretot, quines alternatives es van descartar.
  3. Quins efectes col·laterals té? Rendiment, compatibilitat, migracions.
  4. Què NO fa aquest canvi? Delimitar és tan útil com descriure.
  5. Què cal saber per revisar-ho? Un enllaç, un mesurament, una ordre de reproducció.

Exemple complet, sobre un canvi real de gestor-tasques que va fer la Carla:

Desa les tasques a IndexedDB i no a localStorage

localStorage té un límit pràctic de 5 MB per origen i és síncron:
cada desament bloqueja el fil principal. Amb llistes de més de
2.000 tasques, la Carla va mesurar bloquejos de 180 ms a cada
pulsació de tecla del camp de cerca, perquè el desament automàtic
es dispara a cada canvi.

IndexedDB és asíncrona i no té aquest límit. La migració és
transparent: en arrencar, si detectem dades a localStorage i la
base de dades és buida, les importem i netegem la clau antiga.
Aquest codi de migració es pot esborrar quan passin dues
versions (GT-141).

Es va descartar fer servir l'API File System Access perquè només
és a navegadors basats en Chromium i el Bruno necessita Safari.

Aquest canvi NO toca la sincronització amb el servidor, que
continua llegint del mateix mòdul d'accés a dades.

Refs: GT-137

Fixa't en el que aporta i que no és al diff: el mesurament concret, l'alternativa descartada amb el seu motiu, la data de caducitat del codi de migració i el límit explícit del canvi.

  1. El peu: referències, tiquets i coautoria

El peu (trailer en la terminologia de Git) és un bloc de línies Clau: valor al final del missatge, separat per una línia en blanc. Git l'entén de manera nativa a través de git interpret-trailers, i les plataformes també l'interpreten.

Trailer Per a què serveix
Refs: GT-134 Relaciona amb un tiquet sense tancar-lo
Closes: GT-134 Tanca el tiquet en fusionar (segons la plataforma)
Fixes: GT-134 Igual, per a correccions de fallades
Co-authored-by: Nom <correu> Atribueix el commit a més d'una persona
Signed-off-by: Nom <correu> Certifica l'origen (DCO); l'afegeix git commit -s
Reviewed-by: Nom <correu> Deixa constància de la revisió al commit mateix
BREAKING CHANGE: ... Marca un trencament de compatibilitat (apartat 9)

La coautoria mereix una menció especial, perquè resol un problema real de l'equip. Quan l'Ana i el Bruno programen en parella, el commit el signa només qui l'escriu, i git blame (lliçó 06-03) ho atribueix tot a aquella persona. Amb el trailer, les plataformes reconeixen els dos:

git commit -m "Refactoritza el mòdul de sincronització" -m "$(cat <<'EOF'
Extreu la lògica de reintents a un mòdul propi per poder
provar-la sense xarxa.

Co-authored-by: Bruno Salas <[email protected]>
EOF
)"

Detall important: la línia Co-authored-by ha d'anar al final, precedida d'una línia en blanc, i el correu ha de ser el que la persona té registrat a la plataforma. Si no, el nom apareix però no s'enllaça amb el compte.

Ho pots automatitzar amb git interpret-trailers:

# Afegeix un trailer a un missatge ja escrit
git interpret-trailers --in-place --trailer "Refs: GT-134" missatge.txt

# I consultar-los després
git log -1 --pretty="%(trailers:key=Refs,valueonly)"

  1. Missatges dolents i la seva versió bona

Aquesta taula recull missatges reals de l'historial primerenc de gestor-tasques, quan l'equip encara no tenia convenció, al costat del que haurien hagut de ser.

Missatge real Per què és dolent Versió bona
canvis No diu absolutament res Afegeix el filtre de tasques per etiqueta
fix Què s'ha arreglat? On? Corregeix l'esborrat de tasques amb adjunt
actualitza app.js El nom del fitxer ja és al diff Extreu el renderitzat de la llista a una funció
wip No hauria de ser a main (veure 08-02) Condensar-lo al commit definitiu amb rebase -i
asdfasdf Pressa Afegeix la drecera Ctrl+K per al cercador
Arreglat el bug que va comentar la Carla ahir Context efímer; "ahir" no significa res d'aquí a un any Corregeix l'ordre de les tasques vençudes + cos amb el detall
Canvis demanats a la revisió Cert avui, incomprensible demà Valida la longitud del títol abans de desar
Ara sí Depèn del commit anterior per tenir sentit Condensar amb --fixup (lliçó 05-02)
Afegeix el filtre i corregeix el CSS del peu i puja la dependència Tres canvis en un commit Tres commits
Merge branch 'main' of git.exemple.cat:... Fusió de soroll per fer pull sense --rebase Evitar-ho amb pull.rebase true (lliçó 04-04)
Actualitza dependències Quines? Per què? Puja marked a 12.0.1 per CVE en el sanejat d'HTML
. El clàssic universal Qualsevol cosa

Fixa't en el patró: els missatges dolents gairebé sempre són curts per pressa o dependents d'un context que s'evapora ("ahir", "el que va comentar la Carla", "ara sí").

  1. Conventional Commits

Fins aquí hem parlat de prosa per a humans. Conventional Commits és una convenció que a més fa l'assumpte llegible per màquines, sense perdre llegibilitat humana. És la que fa servir el hook commit-msg que vam escriure a la lliçó 06-01.

El format:

<tipus>(<àmbit opcional>): <descripció>

[cos opcional]

[peu(s) opcional(s)]

Exemples sobre gestor-tasques:

feat(filtres): afegeix el filtre per etiqueta
fix(sync): evita duplicar tasques en reintentar l'enviament
docs(readme): documenta les variables d'entorn necessàries
refactor(app): extreu el renderitzat de la llista a una funció
perf(llista): virtualitza el llistat per sobre de 500 tasques
test(sync): cobreix el reintent amb xarxa intermitent
build(deps): puja marked a 12.0.1
ci(actions): desa node_modules a la memòria cau per hash
style(css): ordena les propietats d'estils.css
chore(git): afegeix .env al .gitignore

Taula de tipus

Tipus Què significa Afecta la versió SemVer?
feat Funcionalitat nova per a l'usuari MINOR
fix Correcció d'una fallada PATCH
docs Només documentació No
style Format, espais, comes; sense canvi de comportament No
refactor Reestructuració sense canviar el comportament No
perf Millora de rendiment PATCH (de vegades MINOR)
test Afegeix o corregeix proves No
build Sistema de compilació o dependències No
ci Configuració d'integració contínua No
chore Tasques de manteniment sense efecte al codi de producció No
revert Reverteix un commit anterior (lliçó 05-06) Depèn

L'àmbit entre parèntesis és lliure i el defineix cada projecte. A gestor-tasques l'equip va acordar: filtres, sync, llista, ui, css, readme, deps, ci. Un àmbit estable converteix git log --oneline | grep '(sync)' en una consulta útil.

Què s'hi guanya

  1. Filtrar l'historial és trivial. git log --oneline --grep '^feat' et dona totes les funcionalitats noves.
  2. Changelog automàtic. Eines com git-cliff, standard-version o semantic-release agrupen els commits per tipus i generen CHANGELOG.md sense intervenció humana.
  3. Versió SemVer derivada. Això enllaça directament amb la lliçó 05-05: allà vam acordar MAJOR.MINOR.PATCH i etiquetes anotades, però decidíem el número a mà. Amb Conventional Commits, el número es dedueix de l'historial.
  4. Disciplina de granularitat. Si no saps quin tipus posar-hi, gairebé sempre és perquè el commit fa més d'una cosa.

Un exemple de derivació

Suposa que des de v1.4.0 l'historial de gestor-tasques conté:

git log --oneline v1.4.0..HEAD
9f3a1c2 docs(readme): corregeix l'enllaç d'instal·lació
7b2e4d1 fix(sync): evita duplicar tasques en reintentar l'enviament
4c8a9f0 feat(filtres): afegeix el filtre per etiqueta
2d1b3e7 test(sync): cobreix el reintent amb xarxa intermitent

Hi ha un feat i cap BREAKING CHANGE, així que la versió següent és v1.5.0. Si només hi hagués hagut fix, seria v1.4.1. Si no hi hagués hagut ni feat ni fix, no hi hauria versió a publicar.

  1. BREAKING CHANGE i la versió derivada

Un trencament de compatibilitat és un canvi que obliga qui consumeix el teu codi a fer alguna cosa. En SemVer puja el número MAJOR, i és l'únic cas en què la deducció automàtica no pot fallar sense conseqüències greus.

Es marca de dues maneres equivalents:

Forma 1 — amb ! després del tipus o de l'àmbit:

feat(api)!: elimina el paràmetre `ordre` de llistaTasques()

Forma 2 — amb un trailer al peu (permet explicar la migració):

feat(api): unifica l'ordenació en un únic paràmetre

BREAKING CHANGE: `llistaTasques()` ja no accepta el paràmetre
`ordre`. Fes servir `criteri`, que admet els mateixos valors més
`venciment`. Substitueix `llistaTasques({ordre: 'alfa'})` per
`llistaTasques({criteri: 'alfa'})`.

Refs: GT-152

La segona és netament millor: el missatge inclou la guia de migració, i les eines de changelog la copien tal qual a la secció de trencaments. Fes servir les dues alhora si vols que el ! sigui visible a --oneline.

Tipus presents des de l'última etiqueta Versió següent
Només docs, test, chore, ci, style Cap publicació
Com a mínim un fix, cap feat PATCH: 1.4.01.4.1
Com a mínim un feat, sense trencaments MINOR: 1.4.01.5.0
Com a mínim un BREAKING CHANGE o ! MAJOR: 1.4.02.0.0

Compte amb el condensat. Si integres les pull requests amb squash merge (ho veurem a fons a la lliçó 08-02), el missatge que compta per a la deducció és el del commit condensat, no els de la branca. Un BREAKING CHANGE amagat al tercer commit d'una branca desapareix si el títol del squash és feat: millores diverses. És un argument fort per revisar el títol del squash abans de fusionar.

  1. Plantilles amb commit.template

La manera més barata que tot un equip escrigui missatges millors és posar-los el recordatori davant dels ulls en el moment exacte. Això fa commit.template: un fitxer el contingut del qual precarrega l'editor cada vegada que confirmes.

Crea el fitxer al repositori, perquè l'Ana, el Bruno i la Carla facin servir el mateix:

# A l'arrel de gestor-tasques

Contingut de .gitmissatge:

# <tipus>(<àmbit>): <descripció en imperatiu, ≤50 caràcters>
#
# Tipus: feat fix docs style refactor perf test build ci chore revert
# Àmbits: filtres sync llista ui css readme deps ci
#
# --- Cos (ajusta a 72 columnes) ----------------------------------|
# Per què era necessari aquest canvi? Quin problema resol?
# Quines alternatives es van descartar i per què?
# Quins efectes col·laterals té?
#
# --- Peu ---------------------------------------------------------
# Refs: GT-NNN
# Co-authored-by: Nom <[email protected]>
# BREAKING CHANGE: descriu la migració necessària

I s'activa:

# Només per a aquest repositori (recomanat: la plantilla és del projecte)
git config --local commit.template .gitmissatge

# O per a tots els teus repositoris
git config --global commit.template ~/.gitmissatge

Tres detalls que importen:

  • Les línies que comencen per # es descarten en desar, així que les instruccions no arriben a l'historial. El caràcter de comentari es pot canviar amb core.commentChar si necessites un # literal (per exemple, per escriure #123).
  • commit.template no es versiona sol. El fitxer .gitmissatge sí que és al repositori, però la configuració que l'activa viu a .git/config, que com vam veure a la lliçó 01-05 és local. Documenta el git config --local commit.template .gitmissatge al README.md, o afegeix-lo a l'script d'arrencada del projecte.
  • La plantilla no s'aplica amb -m. Només quan Git obre l'editor. Cosa que ens porta a l'apartat següent.

  1. Escriure a l'editor, no amb -m

git commit -m "..." és còmode i per això és l'hàbit per defecte de tothom. També és la causa estructural dels missatges dolents, per tres motius:

  1. La cometa del terminal et pressiona a ser breu. Escriure un paràgraf entre cometes a la shell és incòmode, així que no l'escrius.
  2. No veus el context. L'editor et mostra, comentada, la llista de fitxers modificats. Moltes vegades allà descobreixes que has preparat un fitxer que no volies.
  3. No hi ha revisió. Amb -m prems Retorn i ja està. A l'editor llegeixes el que has escrit abans de desar.

Compara:

# L'hàbit ràpid
git commit -m "arregla el filtre"

# L'hàbit bo: obre l'editor amb la plantilla i el context
git commit

En executar git commit a seques, l'editor (core.editor, lliçó 01-06) s'obre amb la plantilla i amb això a sota:

# Si us plau, escriu el missatge de confirmació dels teus canvis.
# Les línies que comencen amb '#' s'ignoraran.
#
# A la branca GT-134-timeout-sync
# La teva branca està actualitzada amb 'origin/GT-134-timeout-sync'.
#
# Canvis que es confirmaran:
#	modificat:      app.js
#	modificat:      README.md

I amb -v hi veus a més el diff complet dins de l'editor, que és la millor ajuda possible per escriure el perquè:

git commit -v

Activa-ho sempre:

git config --global commit.verbose true

Quan sí que cal fer servir -m: per a commits veritablement trivials (docs: corregeix una errada), en scripts, i per als --fixup de la lliçó 05-02, que desapareixeran al rebase.

Si el missatge és llarg i prefereixes no dependre de l'editor, hi ha una alternativa neta:

# Diversos -m es converteixen en paràgrafs separats per línia en blanc
git commit -m "fix(sync): evita duplicar tasques en reintentar" \
           -m "El reintent no comprovava si l'enviament anterior havia arribat. Amb xarxa intermitent això creava duplicats." \
           -m "Refs: GT-134"

  1. Fer complir la convenció: hook i CI

Una convenció que no es comprova s'erosiona en tres setmanes. A la lliçó 06-01 vam escriure un hook commit-msg; ara que la convenció està explicada, aquí tens la versió completa i comentada.

#!/usr/bin/env bash
# .githooks/commit-msg — valida el format del missatge
# Git passa com a $1 el camí del fitxer temporal amb el missatge.

missatge_complet=$(cat "$1")
# Primera línia que no sigui comentari ni estigui buida: l'assumpte.
assumpte=$(grep -v '^#' "$1" | grep -v '^[[:space:]]*$' | head -n 1)

# Les fusions i les reversions les genera Git: no les validem.
if echo "$assumpte" | grep -qE '^(Merge|Revert) '; then
  exit 0
fi

# Format: tipus(àmbit opcional)!: descripció
patro='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9-]+\))?!?: .{1,}$'

if ! echo "$assumpte" | grep -qE "$patro"; then
  echo "ERROR: l'assumpte no segueix Conventional Commits." >&2
  echo "  Rebut:    $assumpte" >&2
  echo "  Format:   tipus(àmbit): descripció" >&2
  echo "  Tipus:    feat fix docs style refactor perf test build ci chore revert" >&2
  exit 1
fi

# Longitud de l'assumpte
if [ ${#assumpte} -gt 72 ]; then
  echo "ERROR: l'assumpte té ${#assumpte} caràcters (màxim 72, ideal 50)." >&2
  exit 1
fi

# Sense punt final
case "$assumpte" in
  *.) echo "ERROR: l'assumpte no ha d'acabar en punt." >&2; exit 1 ;;
esac

# Referència al tiquet obligatòria en algun lloc del missatge
if ! echo "$missatge_complet" | grep -qE 'GT-[0-9]{3,}'; then
  echo "ERROR: falta la referència al tiquet (GT-NNN)." >&2
  echo "  Afegeix una línia 'Refs: GT-134' al final del missatge." >&2
  exit 1
fi

exit 0

Recorda de la lliçó 06-01 els tres requisits perquè funcioni:

chmod +x .githooks/commit-msg
git config --local core.hooksPath .githooks

I recorda també l'advertiment, que la lliçó 07-06 va convertir en principi: un hook de client no és un control. git commit --no-verify se'l salta. Per això la mateixa comprovació ha de ser al CI, on no mana ningú:

# .github/workflows/ci.yml (fragment)
  missatges:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0          # necessitem l'historial complet
      - name: Validar els missatges de la PR
        run: |
          # Només els commits que aporta aquesta branca, no els de main
          BASE="${{ github.event.pull_request.base.sha }}"
          fallades=0
          while read -r sha; do
            assumpte=$(git log -1 --pretty=%s "$sha")
            if ! echo "$assumpte" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9-]+\))?!?: .+'; then
              echo "::error::$sha — assumpte invàlid: $assumpte"
              fallades=1
            fi
          done < <(git rev-list "$BASE..HEAD" --no-merges)
          exit $fallades

Un matís important que sempre s'oblida: si la teva política d'integració és squash merge, validar els commits individuals de la branca té poc sentit —desapareixeran— i el que cal validar és el títol de la pull request, que serà l'assumpte del commit condensat. Quina de les dues polítiques convé és exactament la pregunta que resol la lliçó següent.

  1. L'idioma del missatge

És una discussió que apareix a tots els equips i que té una única resposta correcta: tria'n un i respecta'l.

Opció A favor En contra
Anglès És l'idioma de l'ecosistema; les paraules clau de Conventional Commits ja ho són; facilita contractar i col·laborar a fora Si l'equip no el domina, els missatges es tornen telegràfics i perden el perquè
Català Missatges més rics i precisos si és la llengua de treball; menys fricció per escriure cossos llargs Xoca visualment amb feat/fix; complica obrir el projecte a col·laboradors externs

El que és veritablement dolent no és triar malament: és no triar. Un historial on s'alternen feat: add tag filter i feat: afegeix el filtre per etiqueta és més difícil de cercar (--grep deixa de funcionar), més difícil de llegir i transmet deixadesa.

A gestor-tasques l'equip va acordar, i ho va escriure al README.md: tipus i àmbit en anglès (perquè són paraules clau de la convenció i les entenen les eines), descripció, cos i trailers en català (perquè és la llengua de treball dels quatre i permet explicar el perquè sense empobrir-lo). És un compromís molt habitual i funciona bé.

feat(filtres): afegeix el filtre per etiqueta

Les llistes de més de 200 tasques eren immanejables sense filtrat...

Refs: GT-134

El Diego, el col·laborador extern, es va trobar la decisió escrita al README.md del seu fork abans d'obrir la seva primera pull request. Aquest és el lloc on va: al repositori, no al cap de ningú.

Errors Habituals i Consells

Error 1: descriure el què en lloc del perquè. És l'error dominant. Canvia el valor a 30 no aporta res; El valor de 5 s venia del prototip i ja no cobreix l'enviament d'adjunts ho aporta tot. Prova de foc: si el missatge es dedueix del diff, encara no has escrit el missatge.

Error 2: oblidar la línia en blanc després de l'assumpte. Trenca --oneline, els títols de PR i el %s de --pretty. És sintaxi, no estil.

Error 3: context efímer. "Com vam parlar ahir", "el que va demanar la Carla", "segons el correu d'aquest matí". D'aquí a sis mesos no signifiquen res. Escriu el contingut, no el punter.

Error 4: enllaçar a un sistema extern en lloc de resumir. Veure GT-134 et deixa venut el dia que es migri el gestor de tiquets o algú el consulti sense accés. Referencia el tiquet a més de resumir, mai en lloc de.

Error 5: missatges de procés. Canvis demanats a la revisió, Ara sí, Un altre intent. Descriuen la teva jornada, no el codi. Condensa'ls amb --fixup i rebase -i --autosquash (lliçó 05-02) abans de publicar.

Error 6: creure que es pot arreglar després. El missatge d'un commit publicat només es canvia reescrivint l'historial, i això xoca amb la regla d'or de la lliçó 05-01. Un missatge dolent publicat és permanent. (Si encara no l'has enviat, git commit --amend ho arregla; la lliçó 09-02 desenvolupa els casos de desfer.)

Error 7: aplicar Conventional Commits a mitges. Mitja dotzena de commits amb feat: i la resta amb prosa lliure produeix el pitjor dels dos mons: la disciplina de la convenció sense cap dels seus beneficis automàtics, perquè el changelog surt incomplet.

Consell 1: escriu el missatge abans que el codi. Sona estrany i funciona. Si no ets capaç de resumir en una línia el que vas a fer, el canvi encara no està delimitat.

Consell 2: activa commit.verbose avui mateix. git config --global commit.verbose true posa el diff dins de l'editor. És la millora de qualitat més barata de tot el mòdul.

Consell 3: llegeix el teu propi historial un cop al mes. git log --oneline -30. Si no entens els teus commits de fa tres setmanes, tens un problema que creixerà.

Consell 4: fes servir git shortlog -sn --no-merges i git log --oneline --grep. Si les teves consultes a l'historial no retornen res útil, és senyal que els missatges no ho són.

Consell 5: a les revisions, revisa també els missatges. És legítim demanar en una PR "reescriu el missatge del segon commit". És l'única manera que la convenció sobrevisqui.

Consell 6: la plantilla al repositori, no al teu $HOME. Així el Diego la té en clonar el fork.

Exercicis

Exercici 1: reescriure cinc missatges dolents

Per a cadascun d'aquests missatges reals de l'historial primerenc de gestor-tasques, escriu la versió bona: assumpte en imperatiu de ≤50 caràcters i, quan el cas ho demani, un cos. Inventa el context versemblant que falti.

  1. fix css
  2. actualitza app.js
  3. Afegit el cercador i arreglat el marge del peu de pàgina
  4. Ara sí que funciona
  5. Puja la dependència

Exercici 2: muntar la infraestructura completa

En un repositori de proves:

  1. Crea un fitxer de plantilla .gitmissatge amb l'estructura de Conventional Commits i activa'l amb commit.template a nivell local.
  2. Activa commit.verbose.
  3. Instal·la el hook commit-msg de l'apartat 12 a .githooks/ i configura core.hooksPath.
  4. Comprova que rebutja apanys diversos, que rebutja feat: afegeix el filtre (falta el tiquet) i que accepta feat(filtres): afegeix el filtre per etiqueta amb Refs: GT-134 al cos.
  5. Demostra que --no-verify l'esquiva i explica quina barrera el detindria.

Exercici 3: deduir la versió

Donat aquest historial des de l'etiqueta v2.3.1 de gestor-tasques:

e5f6a7b chore(deps): puja les dependències de desenvolupament
c4d5e6f feat(sync): permet sincronitzar en segon pla
b3c4d5e fix(llista): corregeix l'ordre de les tasques vençudes
a2b3c4d docs(readme): documenta el nou mode sense connexió
9182b3c feat(api)!: elimina el paràmetre `ordre` de llistaTasques()
  1. Quina versió SemVer correspon publicar i per què?
  2. Escriu el missatge complet del commit 9182b3c amb el seu trailer BREAKING CHANGE i la seva guia de migració.
  3. Escriu l'ordre de git log que extrauria només els feat d'aquest rang.
  4. Si l'equip integrés aquesta branca amb squash merge i el títol del squash fos feat: millores de sincronització, quina versió deduiria l'eina? Quin problema revela això?

Solucions

Solució 1:

Original Versió bona
fix css fix(css): corregeix el desbordament del peu en mòbil

Cos: "El peu feia servir una amplada fixa de 960 px, que en pantalles de menys de 360 px provocava desplaçament horitzontal a tota la pàgina. Se substitueix per max-width amb width: 100%." + Refs: GT-121
actualitza app.js refactor(app): extreu el renderitzat de la llista a una funció

El nom del fitxer sobra: --stat ja ho diu. El que importa és què s'ha fet a dins.
Afegit el cercador i arreglat el marge del peu Dos commits. feat(ui): afegeix el cercador de tasques per títol i fix(css): corregeix el marge del peu en pantalles estretes. A més, Afegit no és imperatiu.
Ara sí que funciona No té versió bona: és un commit de procés. El correcte és git commit --fixup=<sha-del-commit-trencat> i condensar-lo amb git rebase -i --autosquash abans de publicar (lliçó 05-02).
Puja la dependència build(deps): puja marked a 12.0.1 per CVE en el sanejat d'HTML

Especifica quina i per què. "Per seguretat" i "per una funcionalitat nova" mereixen urgències diferents.

Solució 2:

mkdir /tmp/practica-missatges && cd /tmp/practica-missatges
git init -b main
git config user.name "Ana Ferrer"
git config user.email "[email protected]"
# 1. La plantilla
cat > .gitmissatge <<'FI'
# <tipus>(<àmbit>): <descripció en imperatiu, ≤50 caràcters>
#
# Tipus: feat fix docs style refactor perf test build ci chore revert
#
# --- Cos (72 columnes) -------------------------------------------|
# Per què era necessari? Quines alternatives es van descartar?
#
# --- Peu ---------------------------------------------------------
# Refs: GT-NNN
FI

git config --local commit.template .gitmissatge

# 2. El diff dins de l'editor
git config --local commit.verbose true
# 3. El hook (copia-hi l'script de l'apartat 12)
mkdir -p .githooks
# ... crear .githooks/commit-msg amb el contingut de la lliçó ...
chmod +x .githooks/commit-msg
git config --local core.hooksPath .githooks
# 4. Les tres proves
echo "hola" > app.js && git add .

git commit -m "apanys diversos"
# ERROR: l'assumpte no segueix Conventional Commits.

git commit -m "feat: afegeix el filtre"
# ERROR: falta la referència al tiquet (GT-NNN).

git commit -m "feat(filtres): afegeix el filtre per etiqueta" -m "Refs: GT-134"
# [main a1b2c3d] feat(filtres): afegeix el filtre per etiqueta
# 5. La via d'escapament
echo "més" >> app.js && git add .
git commit --no-verify -m "qualsevol cosa"
# [main d4e5f6a] qualsevol cosa   ← el hook ni s'ha executat

--no-verify és una opció del client: no viatja per la xarxa i el servidor no s'assabenta que s'ha fet servir. La barrera que sí que el detura és la del CI (apartat 12) declarada com a comprovació obligatòria en una branca protegida, tal com vam veure a la lliçó 07-06. El hook local és una ajuda per al desenvolupador; la branca protegida és el control.

Solució 3:

1. Correspon v3.0.0. La presència de feat(api)! marca un trencament de compatibilitat, i en SemVer això puja el número MAJOR i posa MINOR i PATCH a zero, independentment que a més hi hagi dos feat i un fix. El trencament mana sempre.

2.

feat(api)!: elimina el paràmetre `ordre` de llistaTasques()

Teníem dues maneres d'ordenar la llista: el paràmetre `ordre`
(heretat del prototip, amb valors 'alfa' i 'data') i el
paràmetre `criteri` (introduït a la 2.1 per donar suport a
l'ordenació per venciment). Mantenir tots dos obligava a resoldre
la precedència a cada crida i era font constant de fallades
com GT-149.

S'elimina `ordre` i es conserva `criteri`, que és un
superconjunt estricte.

BREAKING CHANGE: `llistaTasques()` ja no accepta el paràmetre
`ordre`. Fes servir `criteri`, que admet els mateixos valors més
`venciment`:

  llistaTasques({ordre: 'alfa'})  ->  llistaTasques({criteri: 'alfa'})
  llistaTasques({ordre: 'data'})  ->  llistaTasques({criteri: 'data'})

Si hi passes `ordre`, s'ignora en silenci; revisa les teves crides.

Refs: GT-152

3.

git log --oneline --grep '^feat' v2.3.1..HEAD

I per localitzar específicament els trencaments, que poden ser al cos:

git log v2.3.1..HEAD --grep 'BREAKING CHANGE' --pretty='%h %s'

4. L'eina deduiria v2.4.0 (un feat, cap trencament visible), perquè el BREAKING CHANGE era al cos d'un commit que el condensat va fer desaparèixer. El resultat és greu: es publica com a MINOR un canvi que trenca tots els consumidors, i aquests actualitzen confiant en la garantia de compatibilitat de SemVer.

El que revela és que la política d'integració no és una decisió estètica: canvia quins missatges sobreviuen a la línia principal i, per tant, quina informació queda disponible per a les persones i per a les eines. Si es condensa, el títol del squash s'ha de validar amb el mateix rigor que un commit, i els trailers rellevants s'hi han de propagar. Aquesta política és justament el tema de la lliçó següent.

Conclusió

L'essencial d'aquesta lliçó:

  • Un missatge de confirmació s'escriu per a tres lectors sense context: el teu jo d'aquí a dos anys, qui faci blame sobre una línia estranya i qui revisi la teva PR. I per a un quart que és una màquina.
  • El diff ja diu el què; el missatge existeix per al perquè. Si el teu missatge es dedueix del diff, encara no l'has escrit. El que només cap al missatge és la motivació, les alternatives descartades, els efectes col·laterals i els límits del canvi.
  • L'anatomia és sintaxi, no estètica: assumpte de ≤50 caràcters en imperatiu i sense punt, línia en blanc obligatòria, cos a 72 columnes, i peu amb trailers (Refs, Co-authored-by, BREAKING CHANGE).
  • Conventional Commits fa l'assumpte llegible per màquines sense deixar de ser-ho per a les persones: tipus(àmbit): descripció. A canvi d'una disciplina mínima, obtens filtrat de l'historial, changelog automàtic i, enllaçant amb la lliçó 05-05, la versió SemVer derivada del mateix historial.
  • Una plantilla amb commit.template al repositori posa el recordatori davant dels ulls en el moment just. I escriure a l'editor amb commit.verbose en lloc de fer-ho amb -m millora la qualitat més que cap altra mesura individual.
  • La convenció se sosté amb el hook commit-msg de la lliçó 06-01 per a la comoditat del desenvolupador i amb la validació al CI sobre branca protegida de la lliçó 07-06 per a allò innegociable, perquè --no-verify existeix.
  • L'idioma es tria una vegada, s'escriu al README.md i no es discuteix més.

Queda un cap solt que l'exercici 3 ha deixat a la vista: un missatge excel·lent no serveix de res si la política d'integració l'esborra. I aquesta política —merge, squash o rebase— és exactament la decisió que vam deixar oberta a la lliçó 07-04 sobre GitHub Flow.

La resolem ara, juntament amb tota la resta que fa que un historial sigui llegible, bisecable i reversible, a la lliçó 08-02: Mantenint un Historial Net.

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