El módulo anterior terminó con un diagnóstico incómodo: el equipo de gestor-tareas tiene ya un proceso impecable —forks, pull requests, revisiones, ramas protegidas, integración continua— y aun así puede acabar con un historial que nadie sea capaz de leer. Empezamos a corregirlo por el punto más cotidiano y más descuidado de todos: el mensaje de confirmación.
Esta lección salda por fin dos deudas del curso. En la lección 02-04 aprendimos a confirmar cambios con git commit -m y dijimos que las convenciones de mensajes llegarían aquí. En la lección 06-01 escribimos un hook commit-msg que exigía un ticket con formato GT-NNN, pero validamos una convención que aún no habíamos explicado. Y en la 07-06 validamos esa misma convención en el CI. Es el momento de explicar de dónde sale todo eso.
Conviene entender la magnitud del asunto antes de empezar. Un desarrollador escribe entre cinco y veinte mensajes de confirmación al día. Son el único texto en prosa que produce el proyecto de forma sistemática, el único que sobrevive a los refactores, y el único que estará ahí cuando el autor original ya no esté en la empresa. El código dice qué hace el programa; el historial es el único sitio donde puede quedar escrito por qué lo hace así.
Contenido
- Para quién se escribe un mensaje de confirmación
- El porqué importa más que el qué
- Anatomía de un buen mensaje
- La línea de asunto: las siete reglas
- El cuerpo: motivación y contexto
- El pie: referencias, tickets y coautoría
- Mensajes malos y su versión buena
- Conventional Commits
BREAKING CHANGEy la versión derivada- Plantillas con
commit.template - Escribir en el editor, no con
-m - Hacer cumplir la convención: hook y CI
- El idioma del mensaje
- Para quién se escribe un mensaje de confirmación
Hay una respuesta intuitiva y equivocada: "para mí, para acordarme de lo que hice". Es equivocada porque tú, dentro de una semana, ya no necesitarás el mensaje: tendrás el contexto fresco. El mensaje existe para tres lectores que no lo tienen.
Lector 1: tu yo de dentro de dos años. No recuerda nada del contexto. No recuerda la reunión donde se decidió aquello, ni el cliente que se quejó, ni la limitación del navegador que obligó a hacer una cosa rara. El commit es lo único que queda.
Lector 2: quien ejecute git blame sobre una línea rara. Es el caso más frecuente y el más valioso. Alguien encuentra en app.js una condición absurda:
// Nunca se filtra por fecha si el navegador es Safari < 15
if (!esSafariAntiguo()) { aplicarFiltroFecha(); }Su primer impulso será borrarla. En la lección 06-03 vimos que git blame responde qué commit introdujo esa línea. Si ese commit dice arreglos, el lector borrará la condición y reintroducirá el fallo. Si dice Evitar el filtro por fecha en Safari 14: su Intl.DateTimeFormat devuelve la zona horaria en un formato que rompe el parseo, no la borrará. La calidad del mensaje decide si el fallo vuelve.
Lector 3: quien revise tu pull request. En la lección 07-02 vimos que revisar bien exige entender la intención antes que el diff. Una PR con commits atómicos y bien descritos se revisa commit a commit y en la mitad de tiempo. Una PR con ocho commits llamados wip, wip2 y ahora sí obliga al revisor a leer un diff gigante sin ninguna guía.
Hay un cuarto lector que además es una máquina: las herramientas que generan changelogs, deducen versiones y filtran el historial. De eso trata el apartado 8.
flowchart LR
C["Commit<br/>+ mensaje"] --> A["Tu yo futuro<br/>(2 años)"]
C --> B["git blame<br/>sobre una línea rara"]
C --> D["Revisor de la PR"]
C --> E["Herramientas:<br/>changelog, SemVer"]
- El porqué importa más que el qué
Esta es la idea central de toda la lección, y la que más cuesta interiorizar.
El diff ya dice el qué. Está ahí, completo, exacto y para siempre. git show te lo enseña con precisión de carácter. Un mensaje que dice Cambia el color del botón a azul cuando el diff dice - color: #c00; / + color: #06c; es redundante: no aporta ni un bit de información nueva.
Lo que el diff no puede decir nunca:
| El diff sabe | El diff no sabe |
|---|---|
| Qué líneas cambiaron | Por qué había que cambiarlas |
| Qué ficheros se tocaron | Qué alternativas se descartaron y por qué |
| El valor nuevo de una constante | De dónde sale ese valor |
| Que se añadió una condición | Qué fallo concreto previene |
| Que se borró una función | Si estaba muerta o si se movió |
| Que la versión de una dependencia subió | Si fue por una vulnerabilidad o por una funcionalidad |
Compara estos dos mensajes sobre exactamente el mismo diff de gestor-tareas:
Sube el tiempo de espera de la sincronización a 30 segundos
El valor anterior de 5 segundos venía del prototipo, cuando la
sincronización solo enviaba las tareas modificadas. Desde GT-118
enviamos también los adjuntos, y en conexiones móviles lentas el
primer envío de una tarea con imagen supera holgadamente ese
límite: el usuario ve un error y reintenta, duplicando la tarea.
30 segundos cubre el percentil 99 de los tiempos medidos en
preproducción durante una semana. No lo subimos más porque a
partir de ahí el usuario asume que la aplicación se ha colgado.
Refs: GT-134El primero es información que ya estaba en el diff. El segundo contiene cuatro hechos que no están en ninguna otra parte del repositorio: de dónde venía el valor viejo, qué fallo concreto provocaba, cómo se eligió el nuevo y por qué no es mayor. Dentro de dos años, cuando alguien proponga bajarlo otra vez "porque 30 segundos es una barbaridad", ese mensaje es la respuesta.
La regla: si tu mensaje se puede deducir leyendo el diff, todavía no has escrito el mensaje.
- Anatomía de un buen mensaje
Git no impone ningún formato, pero sí trata el texto de forma estructurada, y casi todas las herramientas del ecosistema asumen la misma convención. La estructura canónica tiene tres partes:
Resume el cambio en 50 caracteres o menos
Cuerpo explicativo, envuelto a 72 columnas. Explica la motivación
del cambio y el contexto necesario para entenderlo. El qué está en
el diff; aquí va el porqué.
Puede tener varios párrafos, separados por líneas en blanco.
- Y listas, si aclaran
- Por ejemplo, para enumerar los efectos
Refs: GT-134
Co-authored-by: Bruno Salas <[email protected]>flowchart TD
A["Línea de asunto<br/>≤ 50 caracteres, imperativo, sin punto"] --> B["Línea en blanco<br/>(obligatoria)"]
B --> C["Cuerpo<br/>72 columnas · motivación y contexto"]
C --> D["Línea en blanco"]
D --> E["Pie<br/>Refs, Co-authored-by, BREAKING CHANGE"]
La línea en blanco entre el asunto y el cuerpo no es estética: es sintaxis. Git usa la primera línea como asunto en git log --oneline, en %s de --pretty, en el asunto de los correos de git format-patch y en el título por defecto de una pull request. Si te la saltas, el mensaje entero se convierte en asunto:
# MAL: sin línea en blanco
git commit -m "Corrige el filtro
El filtro no distinguía mayúsculas."
git log --oneline -1Todo el texto pegado, y el --oneline inservible.
- La línea de asunto: las siete reglas
Regla 1: 50 caracteres o menos
No es una cifra mágica arbitraria: es el límite práctico para que git log --oneline, la lista de commits de una plataforma y el git shortlog quepan en una línea sin truncarse. Las plataformas de alojamiento cortan visualmente alrededor de los 72 caracteres, y GitHub avisa a partir de 50.
Si no cabe en 50 caracteres, casi siempre es una de estas dos cosas: el commit hace demasiadas cosas (lo veremos en la lección 08-02) o estás metiendo en el asunto lo que va en el cuerpo.
Regla 2: en modo imperativo
Escribe como si dieras una orden al repositorio. La prueba infalible es completar esta frase:
"Si se aplica, este commit _______"
| Mal | Bien |
|---|---|
| ~~Añadido~~ el filtro por etiqueta | Añade el filtro por etiqueta |
| ~~Añadiendo~~ el filtro por etiqueta | Añade el filtro por etiqueta |
| ~~He arreglado~~ el guardado | Arregla el guardado en local |
| ~~Arreglos varios~~ | Corrige el desbordamiento en móvil |
No es capricho: el propio Git escribe así. Merge branch 'x', Revert "...", Initial commit. Si tus mensajes usan otro tiempo verbal, el historial mezcla dos voces.
Regla 3: sin punto final
Es un título, no una frase. Y en 50 caracteres, un carácter cuenta.
Regla 4: mayúscula inicial
Salvo que uses Conventional Commits (apartado 8), donde la convención habitual es minúscula tras los dos puntos. Elige una y sé coherente.
Regla 5: sé específico
Corrige el error no dice nada. Corrige el borrado de tareas con adjunto sí.
Regla 6: no repitas el nombre del fichero
Cambia app.js es inútil: git log --stat ya lo dice. El ámbito, si hace falta, va con la sintaxis de Conventional Commits.
Regla 7: si necesitas "y", probablemente son dos commits
Añade el filtro por etiqueta y corrige el margen del pie describe dos cambios independientes. La lección 08-02 desarrolla esta idea a fondo.
- El cuerpo: motivación y contexto
El cuerpo es opcional para un cambio trivial y obligatorio para cualquier cambio no obvio. Se envuelve manualmente a 72 columnas por una razón muy concreta: git log indenta el cuerpo con cuatro espacios, así que 72 + 4 = 76, y cabe en un terminal de 80 columnas sin que Git reenvuelva el texto (no lo hace: Git nunca reformatea tu mensaje).
Un cuerpo útil responde a estas preguntas, no necesariamente todas:
- ¿Cuál era el problema o la necesidad? El estado anterior y por qué no valía.
- ¿Por qué esta solución? Y sobre todo, qué alternativas se descartaron.
- ¿Qué efectos colaterales tiene? Rendimiento, compatibilidad, migraciones.
- ¿Qué NO hace este cambio? Delimitar es tan útil como describir.
- ¿Qué hay que saber para revisarlo? Un enlace, una medición, un comando de reproducción.
Ejemplo completo, sobre un cambio real de gestor-tareas que hizo Carla:
Guarda las tareas en IndexedDB en lugar de localStorage
localStorage tiene un límite práctico de 5 MB por origen y es
síncrono: cada guardado bloquea el hilo principal. Con listas de
más de 2.000 tareas, Carla midió bloqueos de 180 ms en cada
pulsación de tecla del campo de búsqueda, porque el autoguardado
se dispara en cada cambio.
IndexedDB es asíncrona y no tiene ese límite. La migración es
transparente: al arrancar, si detectamos datos en localStorage y
la base de datos está vacía, los importamos y limpiamos la clave
antigua. Ese código de migración se puede borrar cuando pasen dos
versiones (GT-141).
Se descartó usar la API File System Access porque solo está en
navegadores basados en Chromium y Bruno necesita Safari.
Este cambio NO toca la sincronización con el servidor, que sigue
leyendo del mismo módulo de acceso a datos.
Refs: GT-137Fíjate en lo que aporta y que no está en el diff: la medición concreta, la alternativa descartada con su motivo, la fecha de caducidad del código de migración y el límite explícito del cambio.
- El pie: referencias, tickets y coautoría
El pie (trailer en la terminología de Git) es un bloque de líneas Clave: valor al final del mensaje, separado por una línea en blanco. Git lo entiende de forma nativa a través de git interpret-trailers, y las plataformas lo interpretan también.
| Trailer | Para qué sirve |
|---|---|
Refs: GT-134 |
Relaciona con un ticket sin cerrarlo |
Closes: GT-134 |
Cierra el ticket al fusionar (según la plataforma) |
Fixes: GT-134 |
Igual, para correcciones de fallo |
Co-authored-by: Nombre <correo> |
Atribuye el commit a más de una persona |
Signed-off-by: Nombre <correo> |
Certifica el origen (DCO); lo añade git commit -s |
Reviewed-by: Nombre <correo> |
Deja constancia de la revisión en el propio commit |
BREAKING CHANGE: ... |
Marca una ruptura de compatibilidad (apartado 9) |
La coautoría merece una mención especial, porque resuelve un problema real del equipo. Cuando Ana y Bruno programan en pareja, el commit lo firma solo quien lo escribe, y git blame (lección 06-03) atribuye todo a esa persona. Con el trailer, las plataformas reconocen a los dos:
git commit -m "Refactoriza el módulo de sincronización" -m "$(cat <<'EOF'
Extrae la lógica de reintentos a un módulo propio para poder
probarla sin red.
Co-authored-by: Bruno Salas <[email protected]>
EOF
)"Detalle importante: la línea Co-authored-by debe ir al final, precedida de una línea en blanco, y el correo debe ser el que la persona tiene registrada en la plataforma. Si no, el nombre aparece pero no se enlaza a la cuenta.
Puedes automatizarlo con git interpret-trailers:
# Añade un trailer a un mensaje ya escrito
git interpret-trailers --in-place --trailer "Refs: GT-134" mensaje.txt
# Y consultarlos después
git log -1 --pretty="%(trailers:key=Refs,valueonly)"
- Mensajes malos y su versión buena
Esta tabla recoge mensajes reales del historial temprano de gestor-tareas, cuando el equipo todavía no tenía convención, junto a lo que deberían haber sido.
| Mensaje real | Por qué es malo | Versión buena |
|---|---|---|
cambios |
No dice absolutamente nada | Añade el filtro de tareas por etiqueta |
fix |
¿Qué se arregló? ¿Dónde? | Corrige el borrado de tareas con adjunto |
actualiza app.js |
El nombre del fichero ya está en el diff | Extrae el renderizado de la lista a una función |
wip |
No debería estar en main (ver 08-02) |
Aplastar en el commit definitivo con rebase -i |
asdfasdf |
Prisa | Añade el atajo Ctrl+K para el buscador |
Arreglado el bug que comentó Carla ayer |
Contexto efímero; "ayer" no significa nada dentro de un año | Corrige el orden de las tareas vencidas + cuerpo con el detalle |
Cambios pedidos en la revisión |
Cierto hoy, incomprensible mañana | Valida la longitud del título antes de guardar |
Ahora sí |
Depende del commit anterior para tener sentido | Aplastar con --fixup (lección 05-02) |
Añade el filtro y corrige el CSS del pie y sube la dependencia |
Tres cambios en un commit | Tres commits |
Merge branch 'main' of git.ejemplo.es:... |
Fusión de ruido por hacer pull sin --rebase |
Evitarlo con pull.rebase true (lección 04-04) |
Actualiza dependencias |
¿Cuáles? ¿Por qué? | Sube marked a 12.0.1 por CVE en el saneado de HTML |
. |
El clásico universal | Cualquier cosa |
Fíjate en el patrón: los mensajes malos casi siempre son cortos por prisa o dependientes de un contexto que se evapora ("ayer", "lo que comentó Carla", "ahora sí").
- Conventional Commits
Hasta aquí hemos hablado de prosa para humanos. Conventional Commits es una convención que además hace el asunto legible por máquinas, sin perder legibilidad humana. Es la que usa el hook commit-msg que escribimos en la lección 06-01.
El formato:
Ejemplos sobre gestor-tareas:
feat(filtros): añade el filtro por etiqueta
fix(sync): evita duplicar tareas al reintentar el envío
docs(readme): documenta las variables de entorno necesarias
refactor(app): extrae el renderizado de la lista a una función
perf(lista): virtualiza el listado por encima de 500 tareas
test(sync): cubre el reintento con red intermitente
build(deps): sube marked a 12.0.1
ci(actions): cachea node_modules por hash del bloqueo
style(css): ordena las propiedades de estilos.css
chore(git): añade .env a .gitignoreTabla de tipos
| Tipo | Qué significa | ¿Afecta a la versión SemVer? |
|---|---|---|
feat |
Funcionalidad nueva para el usuario | MINOR |
fix |
Corrección de un fallo | PATCH |
docs |
Solo documentación | No |
style |
Formato, espacios, comas; sin cambio de comportamiento | No |
refactor |
Reestructuración sin cambiar comportamiento | No |
perf |
Mejora de rendimiento | PATCH (a veces MINOR) |
test |
Añade o corrige pruebas | No |
build |
Sistema de compilación o dependencias | No |
ci |
Configuración de integración continua | No |
chore |
Tareas de mantenimiento sin efecto en el código de producción | No |
revert |
Revierte un commit anterior (lección 05-06) | Depende |
El ámbito entre paréntesis es libre y lo define cada proyecto. En gestor-tareas el equipo acordó: filtros, sync, lista, ui, css, readme, deps, ci. Un ámbito estable convierte git log --oneline | grep '(sync)' en una consulta útil.
Qué se gana
- Filtrar el historial es trivial.
git log --oneline --grep '^feat'te da todas las funcionalidades nuevas. - Changelog automático. Herramientas como
git-cliff,standard-versionosemantic-releaseagrupan los commits por tipo y generanCHANGELOG.mdsin intervención humana. - Versión SemVer derivada. Esto enlaza directamente con la lección 05-05: allí acordamos
MAJOR.MINOR.PATCHy etiquetas anotadas, pero decidíamos el número a mano. Con Conventional Commits, el número se deduce del historial. - Disciplina de granularidad. Si no sabes qué tipo poner, casi siempre es porque el commit hace más de una cosa.
Un ejemplo de derivación
Supón que desde v1.4.0 el historial de gestor-tareas contiene:
9f3a1c2 docs(readme): corrige el enlace de instalación 7b2e4d1 fix(sync): evita duplicar tareas al reintentar el envío 4c8a9f0 feat(filtros): añade el filtro por etiqueta 2d1b3e7 test(sync): cubre el reintento con red intermitente
Hay un feat y ningún BREAKING CHANGE, así que la siguiente versión es v1.5.0. Si solo hubiera habido fix, sería v1.4.1. Si no hubiera habido ni feat ni fix, no habría versión que publicar.
BREAKING CHANGE y la versión derivada
BREAKING CHANGE y la versión derivadaUna ruptura de compatibilidad es un cambio que obliga a quien consume tu código a hacer algo. En SemVer sube el número MAJOR, y es el único caso en que la deducción automática no puede fallar sin consecuencias graves.
Se marca de dos formas equivalentes:
Forma 1 — con ! tras el tipo o el ámbito:
Forma 2 — con un trailer en el pie (permite explicar la migración):
feat(api): unifica la ordenación en un único parámetro
BREAKING CHANGE: `listarTareas()` ya no acepta el parámetro
`orden`. Usa `criterio`, que admite los mismos valores más
`vencimiento`. Sustituye `listarTareas({orden: 'alfa'})` por
`listarTareas({criterio: 'alfa'})`.
Refs: GT-152La segunda es netamente mejor: el mensaje incluye la guía de migración, y las herramientas de changelog la copian tal cual a la sección de rupturas. Usa las dos a la vez si quieres que el ! sea visible en --oneline.
| Tipos presentes desde la última etiqueta | Versión siguiente |
|---|---|
Solo docs, test, chore, ci, style |
Ninguna publicación |
Al menos un fix, ningún feat |
PATCH: 1.4.0 → 1.4.1 |
Al menos un feat, sin rupturas |
MINOR: 1.4.0 → 1.5.0 |
Al menos un BREAKING CHANGE o ! |
MAJOR: 1.4.0 → 2.0.0 |
Cuidado con el aplastado. Si integras las pull requests con squash merge (lo veremos a fondo en la lección 08-02), el mensaje que cuenta para la deducción es el del commit aplastado, no los de la rama. Un
BREAKING CHANGEescondido en el tercer commit de una rama desaparece si el título del squash esfeat: mejoras varias. Es un argumento fuerte para revisar el título del squash antes de fusionar.
- Plantillas con
commit.template
commit.templateLa forma más barata de que todo un equipo escriba mensajes mejores es poner el recordatorio delante de sus ojos en el momento exacto. Eso hace commit.template: un fichero cuyo contenido precarga el editor cada vez que confirmas.
Crea el fichero en el repositorio, para que Ana, Bruno y Carla usen el mismo:
Contenido de .gitmensaje:
# <tipo>(<ámbito>): <descripción en imperativo, ≤50 caracteres>
#
# Tipos: feat fix docs style refactor perf test build ci chore revert
# Ámbitos: filtros sync lista ui css readme deps ci
#
# --- Cuerpo (envuelve a 72 columnas) -----------------------------|
# ¿Por qué era necesario este cambio? ¿Qué problema resuelve?
# ¿Qué alternativas se descartaron y por qué?
# ¿Qué efectos colaterales tiene?
#
# --- Pie ---------------------------------------------------------
# Refs: GT-NNN
# Co-authored-by: Nombre <[email protected]>
# BREAKING CHANGE: describe la migración necesariaY se activa:
# Solo para este repositorio (recomendado: la plantilla es del proyecto)
git config --local commit.template .gitmensaje
# O para todos tus repositorios
git config --global commit.template ~/.gitmensajeTres detalles que importan:
- Las líneas que empiezan por
#se descartan al guardar, así que las instrucciones no llegan al historial. El carácter de comentario se puede cambiar concore.commentCharsi necesitas un#literal (por ejemplo, para escribir#123). commit.templateno se versiona solo. El fichero.gitmensajesí está en el repositorio, pero la configuración que lo activa vive en.git/config, que como vimos en la lección 01-05 es local. Documenta elgit config --local commit.template .gitmensajeen elREADME.md, o añádelo al script de arranque del proyecto.- La plantilla no se aplica con
-m. Solo cuando Git abre el editor. Lo cual nos lleva al apartado siguiente.
- Escribir en el editor, no con
-m
-mgit commit -m "..." es cómodo y por eso es el hábito por defecto de todo el mundo. También es la causa estructural de los mensajes malos, por tres motivos:
- La comilla del terminal te presiona a ser breve. Escribir un párrafo entre comillas en la shell es incómodo, así que no lo escribes.
- No ves el contexto. El editor te muestra, comentada, la lista de ficheros modificados. Muchas veces ahí descubres que has preparado un fichero que no querías.
- No hay revisión. Con
-mpulsas Intro y ya está. En el editor lees lo que has escrito antes de guardar.
Compara:
# El hábito rápido
git commit -m "arregla el filtro"
# El hábito bueno: abre el editor con la plantilla y el contexto
git commitAl ejecutar git commit a secas, el editor (core.editor, lección 01-06) se abre con la plantilla y con esto debajo:
# Por favor, escribe el mensaje de confirmación de tus cambios.
# Las líneas que comienzan con '#' serán ignoradas.
#
# En la rama GT-134-timeout-sync
# Tu rama está actualizada con 'origin/GT-134-timeout-sync'.
#
# Cambios a ser confirmados:
# modificado: app.js
# modificado: README.mdY con -v ves además el diff completo dentro del editor, que es la mejor ayuda posible para escribir el porqué:
Actívalo siempre:
Cuándo sí usar -m: para commits verdaderamente triviales (docs: corrige una errata), en scripts, y para los --fixup de la lección 05-02, que van a desaparecer en el rebase.
Si el mensaje es largo y prefieres no depender del editor, hay una alternativa limpia:
# Varios -m se convierten en párrafos separados por línea en blanco
git commit -m "fix(sync): evita duplicar tareas al reintentar" \
-m "El reintento no comprobaba si el envío anterior había llegado. Con red intermitente eso creaba duplicados." \
-m "Refs: GT-134"
- Hacer cumplir la convención: hook y CI
Una convención que no se comprueba se erosiona en tres semanas. En la lección 06-01 escribimos un hook commit-msg; ahora que la convención está explicada, aquí está la versión completa y comentada.
#!/usr/bin/env bash
# .githooks/commit-msg — valida el formato del mensaje
# Git pasa como $1 la ruta del fichero temporal con el mensaje.
mensaje_completo=$(cat "$1")
# Primera línea que no sea comentario ni esté vacía: el asunto.
asunto=$(grep -v '^#' "$1" | grep -v '^[[:space:]]*$' | head -n 1)
# Las fusiones y las reversiones las genera Git: no las validamos.
if echo "$asunto" | grep -qE '^(Merge|Revert) '; then
exit 0
fi
# Formato: tipo(ámbito opcional)!: descripción
patron='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9-]+\))?!?: .{1,}$'
if ! echo "$asunto" | grep -qE "$patron"; then
echo "ERROR: el asunto no sigue Conventional Commits." >&2
echo " Recibido: $asunto" >&2
echo " Formato: tipo(ámbito): descripción" >&2
echo " Tipos: feat fix docs style refactor perf test build ci chore revert" >&2
exit 1
fi
# Longitud del asunto
if [ ${#asunto} -gt 72 ]; then
echo "ERROR: el asunto tiene ${#asunto} caracteres (máximo 72, ideal 50)." >&2
exit 1
fi
# Sin punto final
case "$asunto" in
*.) echo "ERROR: el asunto no debe terminar en punto." >&2; exit 1 ;;
esac
# Referencia al ticket obligatoria en algún sitio del mensaje
if ! echo "$mensaje_completo" | grep -qE 'GT-[0-9]{3,}'; then
echo "ERROR: falta la referencia al ticket (GT-NNN)." >&2
echo " Añade una línea 'Refs: GT-134' al final del mensaje." >&2
exit 1
fi
exit 0Recuerda de la lección 06-01 los tres requisitos para que funcione:
Y recuerda también la advertencia, que la lección 07-06 convirtió en principio: un hook de cliente no es un control. git commit --no-verify lo salta. Por eso la misma comprobación tiene que estar en el CI, donde nadie manda:
# .github/workflows/ci.yml (fragmento)
mensajes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # necesitamos el historial completo
- name: Validar los mensajes de la PR
run: |
# Solo los commits que aporta esta rama, no los de main
BASE="${{ github.event.pull_request.base.sha }}"
fallos=0
while read -r sha; do
asunto=$(git log -1 --pretty=%s "$sha")
if ! echo "$asunto" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9-]+\))?!?: .+'; then
echo "::error::$sha — asunto inválido: $asunto"
fallos=1
fi
done < <(git rev-list "$BASE..HEAD" --no-merges)
exit $fallosUn matiz importante que se olvida siempre: si tu política de integración es squash merge, validar los commits individuales de la rama tiene poco sentido —van a desaparecer— y lo que hay que validar es el título de la pull request, que será el asunto del commit aplastado. Cuál de las dos políticas conviene es exactamente la pregunta que resuelve la lección siguiente.
- El idioma del mensaje
Es una discusión que aparece en todos los equipos y que tiene una única respuesta correcta: elige uno y respétalo.
| Opción | A favor | En contra |
|---|---|---|
| Inglés | Es el idioma del ecosistema; las palabras clave de Conventional Commits ya lo son; facilita contratar y colaborar fuera | Si el equipo no lo domina, los mensajes se vuelven telegráficos y pierden el porqué |
| Español | Mensajes más ricos y precisos si es la lengua de trabajo; menos fricción para escribir cuerpos largos | Choca visualmente con feat/fix; complica abrir el proyecto a colaboradores externos |
Lo verdaderamente malo no es elegir mal: es no elegir. Un historial donde se alternan feat: add tag filter y feat: añade el filtro por etiqueta es más difícil de buscar (--grep deja de funcionar), más difícil de leer y transmite desidia.
En gestor-tareas el equipo acordó, y lo escribió en el README.md: tipo y ámbito en inglés (porque son palabras clave de la convención y las entienden las herramientas), descripción, cuerpo y trailers en español (porque es la lengua de trabajo de los cuatro y permite explicar el porqué sin empobrecerlo). Es un compromiso muy común y funciona bien.
feat(filtros): añade el filtro por etiqueta
Las listas de más de 200 tareas eran inmanejables sin filtrado...
Refs: GT-134Diego, el colaborador externo, se encontró la decisión escrita en el README.md de su fork antes de abrir su primera pull request. Ese es el sitio donde va: en el repositorio, no en la cabeza de nadie.
Errores Comunes y Consejos
Error 1: describir el qué en lugar del porqué. Es el error dominante. Cambia el valor a 30 no aporta nada; El valor de 5 s venía del prototipo y ya no cubre el envío de adjuntos lo aporta todo. Prueba de fuego: si el mensaje se deduce del diff, todavía no has escrito el mensaje.
Error 2: olvidar la línea en blanco tras el asunto. Rompe --oneline, los títulos de PR y el %s de --pretty. Es sintaxis, no estilo.
Error 3: contexto efímero. "Como hablamos ayer", "lo que pidió Carla", "según el correo de esta mañana". Dentro de seis meses no significan nada. Escribe el contenido, no el puntero.
Error 4: enlazar a un sistema externo en lugar de resumir. Ver GT-134 te deja vendido el día que se migre el gestor de tickets o alguien lo consulte sin acceso. Referencia el ticket además de resumir, nunca en lugar de.
Error 5: mensajes de proceso. Cambios pedidos en la revisión, Ahora sí, Otro intento. Describen tu jornada, no el código. Aplástalos con --fixup y rebase -i --autosquash (lección 05-02) antes de publicar.
Error 6: creer que se puede arreglar después. El mensaje de un commit publicado solo se cambia reescribiendo el historial, y eso choca con la regla de oro de la lección 05-01. Un mensaje malo publicado es permanente. (Si aún no lo has enviado, git commit --amend lo arregla; la lección 09-02 desarrolla los casos de deshacer.)
Error 7: aplicar Conventional Commits a medias. Media docena de commits con feat: y el resto con prosa libre produce lo peor de ambos mundos: la disciplina de la convención sin ninguno de sus beneficios automáticos, porque el changelog sale incompleto.
Consejo 1: escribe el mensaje antes que el código. Suena raro y funciona. Si no eres capaz de resumir en una línea lo que vas a hacer, el cambio todavía no está delimitado.
Consejo 2: activa commit.verbose hoy mismo. git config --global commit.verbose true pone el diff dentro del editor. Es la mejora de calidad más barata de todo el módulo.
Consejo 3: lee tu propio historial una vez al mes. git log --oneline -30. Si no entiendes tus commits de hace tres semanas, tienes un problema que crecerá.
Consejo 4: usa git shortlog -sn --no-merges y git log --oneline --grep. Si tus consultas al historial no devuelven nada útil, es señal de que los mensajes no lo son.
Consejo 5: en las revisiones, revisa también los mensajes. Es legítimo pedir en una PR "reescribe el mensaje del segundo commit". Es la única forma de que la convención sobreviva.
Consejo 6: la plantilla en el repositorio, no en tu $HOME. Así Diego la tiene al clonar el fork.
Ejercicios
Ejercicio 1: reescribir cinco mensajes malos
Para cada uno de estos mensajes reales del historial temprano de gestor-tareas, escribe la versión buena: asunto en imperativo de ≤50 caracteres y, cuando el caso lo pida, un cuerpo. Inventa el contexto verosímil que falte.
fix cssactualiza app.jsAñadido el buscador y arreglado el margen del pie de páginaAhora sí funcionaSube la dependencia
Ejercicio 2: montar la infraestructura completa
En un repositorio de pruebas:
- Crea un fichero de plantilla
.gitmensajecon la estructura de Conventional Commits y actívalo concommit.templatea nivel local. - Activa
commit.verbose. - Instala el hook
commit-msgdel apartado 12 en.githooks/y configuracore.hooksPath. - Comprueba que rechaza
arreglos varios, que rechazafeat: añade el filtro(falta el ticket) y que aceptafeat(filtros): añade el filtro por etiquetaconRefs: GT-134en el cuerpo. - Demuestra que
--no-verifylo esquiva y explica qué barrera lo detendría.
Ejercicio 3: deducir la versión
Dado este historial desde la etiqueta v2.3.1 de gestor-tareas:
e5f6a7b chore(deps): sube las dependencias de desarrollo c4d5e6f feat(sync): permite sincronizar en segundo plano b3c4d5e fix(lista): corrige el orden de las tareas vencidas a2b3c4d docs(readme): documenta el nuevo modo sin conexión 9182b3c feat(api)!: elimina el parámetro `orden` de listarTareas()
- ¿Qué versión SemVer corresponde publicar y por qué?
- Escribe el mensaje completo del commit
9182b3ccon su trailerBREAKING CHANGEy su guía de migración. - Escribe el comando de
git logque extraería solo losfeatde ese rango. - Si el equipo integrara esta rama con squash merge y el título del squash fuera
feat: mejoras de sincronización, ¿qué versión deduciría la herramienta? ¿Qué problema revela eso?
Soluciones
Solución 1:
| Original | Versión buena |
|---|---|
fix css |
fix(css): corrige el desbordamiento del pie en móvilCuerpo: "El pie usaba un ancho fijo de 960 px, que en pantallas de menos de 360 px provocaba desplazamiento horizontal en toda la página. Se sustituye por max-width con width: 100%." + Refs: GT-121 |
actualiza app.js |
refactor(app): extrae el renderizado de la lista a una funciónEl nombre del fichero sobra: --stat ya lo dice. Lo que importa es qué se hizo dentro. |
Añadido el buscador y arreglado el margen del pie |
Dos commits. feat(ui): añade el buscador de tareas por título y fix(css): corrige el margen del pie en pantallas estrechas. Además, Añadido no es imperativo. |
Ahora sí funciona |
No tiene versión buena: es un commit de proceso. Lo correcto es git commit --fixup=<sha-del-commit-roto> y aplastarlo con git rebase -i --autosquash antes de publicar (lección 05-02). |
Sube la dependencia |
build(deps): sube marked a 12.0.1 por CVE en el saneado de HTMLEspecifica cuál y por qué. "Por seguridad" y "por una funcionalidad nueva" merecen urgencias distintas. |
Solución 2:
mkdir /tmp/practica-mensajes && cd /tmp/practica-mensajes
git init -b main
git config user.name "Ana Ferrer"
git config user.email "[email protected]"# 1. La plantilla
cat > .gitmensaje <<'EOF'
# <tipo>(<ámbito>): <descripción en imperativo, ≤50 caracteres>
#
# Tipos: feat fix docs style refactor perf test build ci chore revert
#
# --- Cuerpo (72 columnas) ----------------------------------------|
# ¿Por qué era necesario? ¿Qué alternativas se descartaron?
#
# --- Pie ---------------------------------------------------------
# Refs: GT-NNN
EOF
git config --local commit.template .gitmensaje
# 2. El diff dentro del editor
git config --local commit.verbose true# 3. El hook (copia aquí el script del apartado 12)
mkdir -p .githooks
# ... crear .githooks/commit-msg con el contenido de la lección ...
chmod +x .githooks/commit-msg
git config --local core.hooksPath .githooks# 4. Las tres pruebas
echo "hola" > app.js && git add .
git commit -m "arreglos varios"
# ERROR: el asunto no sigue Conventional Commits.
git commit -m "feat: añade el filtro"
# ERROR: falta la referencia al ticket (GT-NNN).
git commit -m "feat(filtros): añade el filtro por etiqueta" -m "Refs: GT-134"
# [main a1b2c3d] feat(filtros): añade el filtro por etiqueta# 5. La vía de escape
echo "más" >> app.js && git add .
git commit --no-verify -m "cualquier cosa"
# [main d4e5f6a] cualquier cosa ← el hook ni se ejecutó--no-verify es una opción del cliente: no viaja por la red y el servidor no se entera de que se usó. La barrera que sí lo detiene es la del CI (apartado 12) declarada como comprobación obligatoria en una rama protegida, tal como vimos en la lección 07-06. El hook local es una ayuda para el desarrollador; la rama protegida es el control.
Solución 3:
1. Corresponde v3.0.0. La presencia de feat(api)! marca una ruptura de compatibilidad, y en SemVer eso sube el número MAJOR y pone MINOR y PATCH a cero, independientemente de que haya además dos feat y un fix. La ruptura manda siempre.
2.
feat(api)!: elimina el parámetro `orden` de listarTareas()
Teníamos dos formas de ordenar la lista: el parámetro `orden`
(heredado del prototipo, con valores 'alfa' y 'fecha') y el
parámetro `criterio` (introducido en la 2.1 para soportar la
ordenación por vencimiento). Mantener los dos obligaba a resolver
la precedencia en cada llamada y era fuente constante de fallos
como GT-149.
Se elimina `orden` y se conserva `criterio`, que es un
superconjunto estricto.
BREAKING CHANGE: `listarTareas()` ya no acepta el parámetro
`orden`. Usa `criterio`, que admite los mismos valores más
`vencimiento`:
listarTareas({orden: 'alfa'}) -> listarTareas({criterio: 'alfa'})
listarTareas({orden: 'fecha'}) -> listarTareas({criterio: 'fecha'})
Si pasas `orden`, se ignora en silencio; revisa tus llamadas.
Refs: GT-1523.
Y para localizar específicamente las rupturas, que pueden estar en el cuerpo:
4. La herramienta deduciría v2.4.0 (un feat, ninguna ruptura visible), porque el BREAKING CHANGE estaba en el cuerpo de un commit que el aplastado hizo desaparecer. El resultado es grave: se publica como MINOR un cambio que rompe a todos los consumidores, y estos actualizan confiando en la garantía de compatibilidad de SemVer.
Lo que revela es que la política de integración no es una decisión estética: cambia qué mensajes sobreviven en la línea principal y, por tanto, qué información queda disponible para las personas y para las herramientas. Si se aplasta, el título del squash debe validarse con el mismo rigor que un commit, y los trailers relevantes deben propagarse a él. Esa política es justo el tema de la lección siguiente.
Conclusión
Lo esencial de esta lección:
- Un mensaje de confirmación se escribe para tres lectores sin contexto: tu yo de dentro de dos años, quien haga
blamesobre una línea rara y quien revise tu PR. Y para un cuarto que es una máquina. - El diff ya dice el qué; el mensaje existe para el porqué. Si tu mensaje se deduce del diff, todavía no lo has escrito. Lo que solo cabe en el mensaje es la motivación, las alternativas descartadas, los efectos colaterales y los límites del cambio.
- La anatomía es sintaxis, no estética: asunto de ≤50 caracteres en imperativo y sin punto, línea en blanco obligatoria, cuerpo a 72 columnas, y pie con trailers (
Refs,Co-authored-by,BREAKING CHANGE). - Conventional Commits hace el asunto legible por máquinas sin dejar de serlo para las personas:
tipo(ámbito): descripción. A cambio de una disciplina mínima, obtienes filtrado del historial, changelog automático y, enlazando con la lección 05-05, la versión SemVer derivada del propio historial. - Una plantilla con
commit.templateen el repositorio pone el recordatorio delante de los ojos en el momento justo. Y escribir en el editor concommit.verboseen lugar de con-mmejora la calidad más que ninguna otra medida individual. - La convención se sostiene con el hook
commit-msgde la lección 06-01 para la comodidad del desarrollador y con la validación en el CI sobre rama protegida de la lección 07-06 para lo innegociable, porque--no-verifyexiste. - El idioma se elige una vez, se escribe en el
README.mdy no se discute más.
Queda un cabo suelto que el ejercicio 3 ha dejado a la vista: un mensaje excelente no sirve de nada si la política de integración lo borra. Y esa política —merge, squash o rebase— es exactamente la decisión que dejamos abierta en la lección 07-04 sobre GitHub Flow.
La resolvemos ahora, junto con todo lo demás que hace que un historial sea legible, bisecable y reversible, en la lección 08-02: Manteniendo un Historial Limpio.
Dominando Git: De Principiante a Avanzado
Módulo 1: Introducción a Git
- ¿Qué es Git?
- Instalando Git
- Terminología Básica de Git
- El Modelo de Datos de Git
- Configurando Git
- Configuración Inicial
Módulo 2: Operaciones Básicas de Git
- Creando un Repositorio
- Clonando un Repositorio
- Flujo de Trabajo Básico de Git
- Preparando y Confirmando Cambios
- Inspeccionando Cambios con git diff
- Visualizando el Historial de Confirmaciones
Módulo 3: Ramas y Fusión
- Entendiendo las Ramas
- Creando y Cambiando Ramas
- Fusionando Ramas
- Estrategias de Fusión
- Resolviendo Conflictos de Fusión
- Gestión de Ramas
Módulo 4: Trabajando con Repositorios Remotos
- Entendiendo los Repositorios Remotos
- Agregando un Repositorio Remoto
- Autenticación con Repositorios Remotos
- Obteniendo y Extrayendo Cambios
- Enviando Cambios
- Rastreando Ramas
Módulo 5: Operaciones Avanzadas de Git
- Rebase
- Rebase Interactivo
- Cherry-Picking de Confirmaciones
- Guardando Cambios Temporales
- Etiquetando Confirmaciones
- Revirtiendo Confirmaciones
Módulo 6: Herramientas y Técnicas de Git
- Usando Git Hooks
- Git Bisect
- Git Blame
- Git Log y Alias
- Submódulos de Git
- Múltiples Copias de Trabajo con git worktree
Módulo 7: Estrategias de Colaboración y Flujo de Trabajo
- Forks y Pull Requests
- Revisiones de Código con Git
- Flujo de Trabajo Git Flow
- GitHub Flow
- Trunk Based Development
- Integración Continua con Git
Módulo 8: Mejores Prácticas y Consejos de Git
- Escribiendo Buenos Mensajes de Confirmación
- Manteniendo un Historial Limpio
- Ignorando Archivos con .gitignore
- Atributos de Fichero con .gitattributes
- Mejores Prácticas de Seguridad
- Consejos de Rendimiento
Módulo 9: Solución de Problemas y Depuración
- Problemas Comunes de Git
- Deshaciendo Cambios
- Resolviendo Divergencias con el Remoto
- Recuperando Confirmaciones Perdidas
- Tratando con Repositorios Corruptos
- Técnicas Avanzadas de Depuración
