Escribir buenos mensajes de commit en Git
1. General
El mensaje de commit es lo que más relees cuando debuggeas, revisas código o escribes changelog — mucho más que el código que acabas de escribir. Este artículo te ayuda a dominar la estructura estándar de mensajes de commit, aplicar Conventional Commits, y validar automáticamente mensajes con commitlint + husky. Al terminar, escribirás commits claros, consistentes, y tu equipo podrá generar changelog automáticamente desde el historial de git.
2. Por qué el mensaje de commit es importante
Un buen mensaje de commit responde tres preguntas: qué cambió, por qué cambió, y qué impacto tiene. Sin él, tienes que hacer git show de cada commit para adivinar la intención del autor — gastarás mucho más tiempo que si escribieras un mensaje decente desde el principio.
Compara dos mensajes para el mismo cambio:
# Malo
fix bug
# Bueno
fix(payment): manejar null pointer cuando PaymentService.charge() recibe Order sin Customer
El mensaje malo no dice qué bug ni dónde está. El bueno declara el módulo (payment), la acción (fix), y la causa específica — lees 5 segundos y ya entiendes.
3. Estructura de un mensaje de commit estándar
Git recomienda una estructura de 3 partes: subject (asunto), body (cuerpo), footer (pie de página).
<type>(<scope>): <subject>
<body>
<footer>
subject: resumen corto, ≤ 50 caracteres, en tiempo presente (“add” no “added”).body: explica por qué cambió, no qué cambió (el diff ya lo dice).footer: metadata — breaking change, issue relacionado, co-author.
Ejemplo completo:
fix(order): evitar double-charge al reintentar pago fallido
PaymentService.charge() no tiene idempotency key, entonces si
hay network timeout y el cliente reintenta, el sistema llama
al API gateway dos veces para la misma Order.
Añadimos idempotencyKey = order.getId() + "-" + attemptCount
en el request header para que gateway haga deduplicación automática.
Closes #482
3.1. La línea subject
El subject es la línea más importante — aparece en git log --oneline, en el título de PR, en changelog. Reglas:
# Incorrecto - primera letra mayúscula, punto final, pasado
Fixed the bug in order service.
# Correcto - minúscula, sin punto, verbo en presente
fix: order no deduce inventario al pagar con billetera digital
Nota: El límite de 50 caracteres no es una ley inquebrantable, pero pasar 72 se corta en muchas terminales e interfaces de GitHub. Intenta ser conciso.
3.2. Sección body
El body se usa para explicar el contexto — lo que el diff no puede decir: por qué elegiste esta solución, por qué se descartaron otras, si hay efectos secundarios.
refactor(cart): convertir CartService de síncrono a async
Antes CartService.checkout() llamaba InventoryService y
ShippingService secuencialmente, latencia total ~800ms. Cambiamos
a CompletableFuture para correr ambas llamadas en paralelo,
reduciendo latencia a ~450ms.
No cambió el API público, solo la implementación interna.
Separa subject y body con una línea vacía — git y todas las herramientas de parsing de commits se basan en esta convención.
4. Conventional Commits
Conventional Commits es el estándar más popular hoy, define type fijo para que las herramientas puedan clasificar automáticamente y generar changelog.
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Tipos (type) más comunes:
| Type | Significado |
|---|---|
feat |
agregar nueva funcionalidad |
fix |
corregir bug |
docs |
solo cambios en documentación |
style |
formato de código, sin cambiar lógica |
refactor |
cambiar estructura de código, sin cambiar comportamiento |
test |
agregar/corregir tests |
chore |
tareas menores (build, dependencia, config) |
perf |
mejorar rendimiento |
Ejemplo con scope como nombre del módulo:
feat(auth): soportar login con OAuth2 Google
Agregamos GoogleOAuth2Provider que implementa OAuth2LoginProvider.
Añadimos client-id y client-secret en
application.properties.
4.1. Breaking change
Cuando un cambio rompe la compatibilidad hacia atrás, marca con ! después de type/scope o en el footer BREAKING CHANGE:.
feat(api)!: cambiar respuesta de GET /orders/{id} a OrderDto
BREAKING CHANGE: el campo "customerName" cambió a objeto
"customer" que contiene id, name, email. Los clientes deben
actualizar su parser.
Semantic-release y herramientas CI/CD se basan en la línea BREAKING CHANGE: para aumentar automáticamente la versión mayor.
5. Validar automáticamente el mensaje de commit
Las buenas convenciones solo ayudan si el equipo las sigue consistentemente. Usa commitlint para bloquear commits con formato incorrecto localmente, antes de hacer push.
<!-- No aplica para proyecto Node, solo ejemplo de dependencia si usas Maven wrapper para husky-git-hooks -->
En la práctica commitlint corre en Node, se instala vía npm:
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
Crea archivo de configuración:
// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };
Adjunta el hook commit-msg con husky:
npx husky init
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg
Ahora intenta hacer commit con formato incorrecto:
git commit -m "fixed bug"
⧗ input: fixed bug
✖ subject may not be empty [subject-empty]
✖ type must be one of [feat, fix, docs, style, refactor, test, chore, perf]
✖ found 2 problems, 0 warnings
husky - commit-msg script failed (code 1)
El commit se bloquea inmediatamente, obligándote a corregir el formato antes de crear el commit.
5.1. Usar con proyectos Java/Spring Boot
Si tu proyecto usa Maven, aún puedes usar husky + commitlint en paralelo (solo necesitan Node runtime, no tocan el build Java). O si prefieres algo puramente Java, escribe un script de validación simple y llámalo vía core.hooksPath:
#!/bin/bash
# .githooks/commit-msg
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf)(\([a-z-]+\))?: .{1,50}"
MSG=$(cat "$1")
if ! [[ "$MSG" =~ $PATTERN ]]; then
echo "Mensaje de commit no cumple formato Conventional Commits" >&2
exit 1
fi
git config core.hooksPath .githooks
chmod +x .githooks/commit-msg
5.2. Escribir tests para el script de validación de formato
Si mantienes un script de validación propio (sin commitlint), debes tener tests para esa regex. Supongamos que encapsulas la lógica de parsing en una clase Java para reutilizar en CI:
public class CommitMessageValidator {
private static final Pattern PATTERN =
Pattern.compile("^(feat|fix|docs|style|refactor|test|chore|perf)" +
"(\\([a-z0-9-]+\\))?: .{1,50}");
public boolean isValid(String message) {
String subject = message.lines().findFirst().orElse("");
return PATTERN.matcher(subject).find();
}
}
class CommitMessageValidatorTest {
private final CommitMessageValidator validator = new CommitMessageValidator();
@Test
void shouldAcceptConventionalFormat() {
boolean result = validator.isValid("fix(order): evitar double-charge al reintentar");
assertThat(result).isTrue();
}
@Test
void shouldRejectMessageWithoutType() {
boolean result = validator.isValid("fixed the bug");
assertThat(result).isFalse();
}
@Test
void shouldRejectUnknownType() {
boolean result = validator.isValid("update: agregar algo");
assertThat(result).isFalse();
}
}
Estos tests aseguran que la lógica de validación no sufra regresión cuando edites la regex después — por ejemplo, añadir un type nuevo olvidando actualizar los tests.
6. Errores comunes
Commits demasiado grandes con mensajes genéricos. “update code” para un commit que modifica 15 archivos diferentes es señal de que debes dividirlo en muchos commits pequeños, cada uno con un propósito claro (atomic commit).
Mezclar múltiples tipos de cambios en un commit. Si haces fix de un bug y refactor sin relación en el mismo commit, hace difícil usar git revert y git bisect — revertir un fix podría arrastrar la pérdida de todo el refactor.
Body que repite el subject. El body debe complementar con contexto, no re-explicar la primera línea con frases más largas.
# Repetición, sin valor
fix(order): corregir error de cálculo de total
Corregimos el error de cálculo de total en OrderService.
Usar emoji/caracteres decorativos en lugar de contenido. Emojis no están mal, pero si los usas para reemplazar la descripción real, el changelog automático quedará vacío de información.
Nota: Conventional Commits es adecuado para equipos con CI/CD que generan changelog automáticamente o usan semantic versioning. Si tu equipo es pequeño, hace pocos commits, aplicar reglas demasiado estrictas puede causar fricción innecesaria — prioriza claridad sobre conformidad mecánica con el formato.
7. Conclusión
Los buenos mensajes de commit no necesitan ser largos, solo deben seguir la estructura correcta: subject conciso en presente, body que explique la razón, footer con metadatos necesarios. Aplicar Conventional Commits ayuda al equipo a leer el historial de git más rápido y permite que las herramientas automaticen changelog y versionado. Configura commitlint o git hooks personalizados para convertir la convención en una restricción automática, evitando depender de la conciencia individual de cada miembro del equipo.