Commits atómicos: buenas prácticas para un historial limpio

Qué es un commit atómico, por qué acelera la revisión y facilita revert y bisect, y cómo dividir tu trabajo con git add -p paso a paso.

Commits atómicos: buenas prácticas para un historial limpio

Son las 19:00, llevas todo el día picando y sueltas el commit del día: mensaje “cambios varios”, 38 archivos. Todos lo hemos hecho. El problema no aparece hoy. Aparece tres semanas después, cuando algo se rompe en producción, abres ese commit buscando la causa y te encuentras una caja negra con la feature nueva, un refactor a medias, dos typos y una dependencia actualizada, todo revuelto. Aquí te cuento cómo pasar de ese commit gigante a commits atómicos, y por qué ese cambio de hábito es lo que separa a un junior de alguien que parece llevar años en el equipo.

¿Qué es un commit atómico?

Un commit atómico es un único cambio lógico completo y coherente, que compila y pasa sus tests por sí solo. Esa es toda la definición. La palabra “atómico” viene de “indivisible”: si intentaras partir el commit en dos, cada mitad quedaría incompleta o rota.

Fíjate en lo que no es. No es “un commit pequeño”: un cambio lógico puede tocar cinco archivos y añadir cien líneas si eso es lo que la feature necesita. Tampoco es “un commit por archivo”, que es el error contrario y produce un historial igual de inútil, lleno de commits que por separado no significan nada.

La prueba mental es sencilla. Si el mensaje del commit necesita la palabra “y” para describir lo que hace (“añade validación de email y arregla el buscador y actualiza el linter”), no es atómico. Son tres cosas. El mensaje que describe un commit atómico cabe en una frase sin conjunciones, y de eso va precisamente el post sobre mensajes de commit.

¿Por qué los commits atómicos te hacen parecer senior?

Porque un historial atómico convierte operaciones que serían una pesadilla en cosas de un comando. No es una cuestión de estética. Es lo que Git puede hacer por ti cuando cada commit es una unidad limpia.

Empieza por la revisión de código. Quien revisa tu pull request lee cambios, no archivos. Si abre un commit y ve “extraer parseo de fecha a una función”, entiende la intención en dos segundos y valida el diff completo. Si abre “cambios varios” con 38 archivos, o lo aprueba sin leerlo (peligro) o tarda media hora en reconstruir mentalmente qué pretendías. Este detalle importa todavía más cuando trabajas con otros: el flujo de trabajo en equipo con Git y GitHub se apoya entero en que los demás puedan leer tu historial.

Sigue con git revert. Imagina que la feature de exportar a CSV resulta estar rota y hay que quitarla ya. Si vive en su propio commit, git revert <hash> genera un commit inverso que la deshace de forma quirúrgica y deja el resto intacto. Si esa feature comparte commit con el refactor del módulo de usuarios, revertir la exportación se lleva por delante el refactor. Ahora tienes que resolver a mano lo que Git habría hecho solo.

Y luego está git bisect, que para mí es el argumento definitivo. git bisect hace una búsqueda binaria por tu historial para encontrar el commit exacto que introdujo un bug: marcas un commit bueno y uno malo, y Git te va dando commits intermedios para que digas “aquí funciona” o “aquí ya está roto”, partiendo el rango a la mitad cada vez. En un historial de mil commits llega al culpable en unos diez pasos. Pero solo sirve bien si cada commit compila. Cuando uno de los commits intermedios no arranca, no puedes decir si es bueno o malo: te toca saltarlo con git bisect skip, que lo excluye a costa de perder precisión, y si hay muchos así la búsqueda deja de merecer la pena.

El resultado de todo esto es un historial que se lee como una narración: cada línea de git log te cuenta una decisión. Compara.

# Historial "de las 19:00": no puedes revertir ni bisectar nada
a3f9c21 cambios varios
7b1e004 wip
c8d2f6a más cambios y fix del test
6b0af13 ya casi
# Historial atómico: cada línea es un cambio que puedes revertir solo
d4e8a1c feat: validar email en el registro
9f2b7c0 refactor: extraer parseo de fecha a util
6a1c3e5 test: cubrir email duplicado en el registro
b0d5f89 fix: normalizar acentos en el buscador

El segundo historial te deja revertir un commit suelto, bisectar un fallo o simplemente leer qué pasó en cada línea.

Comparación entre un commit gigante que mezcla varios cambios y un historial de commits atómicos separados por tipo de cambio
Un commit gigante es una caja negra. Un historial atómico es una lista de decisiones que puedes revertir o localizar una a una.

¿Cómo divido el trabajo mientras programo?

La forma más barata de tener commits atómicos es pensarlos antes de escribir el código, no después. Cuando vas a atacar una tarea, dedica diez segundos a partirla mentalmente: “primero extraigo esta función, luego construyo la feature encima, luego añado el test”. Eso ya son tres commits antes de tocar el teclado.

La regla que más rendimiento me da: el refactor va en un commit separado de la feature. Si para añadir tu funcionalidad necesitas reordenar código que ya existía, haz primero el refactor puro (mismo comportamiento, código movido) y commitéalo. Luego, encima y ya limpio, construye la feature. Así, si algo peta, sabes al instante si fue el movimiento de código o la lógica nueva. Mezclarlos es garantía de un diff que nadie sabe leer.

Lo segundo: commitea a menudo. En cuanto una pieza está completa y verde, ciérrala. No esperes al final del día a que se te acumulen cuatro cambios sin relación. Un commit marca un punto en el que el proyecto está entero: ciérralo cuando una pieza esté lista, no cuando termine tu jornada.

Pero seamos realistas: por mucho que planifiques, hay días en los que arrancas a arreglar un bug, ves de paso un typo, tocas una función cercana y de repente tienes el working tree hecho un revoltijo. Para eso Git tiene una herramienta que casi nadie usa.

¿Y cuando ya lo tengo todo mezclado en el working tree?

Cuando ya tienes varios cambios sin relación mezclados, git add -p te deja stagear por fragmentos en vez de por archivos. Recorre tus cambios hunk a hunk (cada bloque contiguo de líneas modificadas) y para cada uno te pregunta si lo quieres meter en el commit o no. Así construyes un primer commit solo con los trozos que van juntos, y dejas el resto para el siguiente.

Al lanzarlo, Git te muestra el primer hunk. Fíjate en que dos cambios sin relación (añadir la validación de email y registrar una métrica de alta) han quedado pegados en el mismo bloque porque están a pocas líneas uno del otro:

$ git add -p
diff --git a/src/registro.ts b/src/registro.ts
@@ -4,8 +4,12 @@ export function registrar(datos) {
   const origen = datos.origen ?? "web"
   const ahora = Date.now()
   const email = datos.email.trim()
+  if (!validarEmail(email)) {
+    throw new Error("Email no válido")
+  }
   const nombre = normalizar(datos.nombre)
   const usuario = crearUsuario(email, nombre)
   guardar(usuario)
+  registrarMetrica("signup")
   return usuario
 }
(1/1) Stage this hunk [y,n,q,a,d,s,e,?]?

Las opciones que más vas a usar son y (mete este hunk en staging), n (sáltalo) y q (salir). Pulsa ? en cualquier momento y Git te lista todas. La que hace la magia de verdad es s: parte el hunk actual en hunks más pequeños. Cuando Git te presenta un bloque que contiene dos cambios sin relación pegados por proximidad, s intenta trocearlo por las líneas de contexto sin cambios que haya en medio, y te vuelve a preguntar por cada trozo:

(1/1) Stage this hunk [y,n,q,a,d,s,e,?]? s
Split into 2 hunks.
@@ -4,6 +4,9 @@ export function registrar(datos) {
   const origen = datos.origen ?? "web"
   const ahora = Date.now()
   const email = datos.email.trim()
+  if (!validarEmail(email)) {
+    throw new Error("Email no válido")
+  }
   const nombre = normalizar(datos.nombre)
   const usuario = crearUsuario(email, nombre)
   guardar(usuario)
(1/2) Stage this hunk [y,n,q,a,d,s,e,?]? y

Si ni siquiera así queda limpio, porque los dos cambios comparten línea, e abre el hunk en tu editor para que decidas línea a línea qué se stagea. Es el bisturí fino. No lo necesitas a diario, pero el día que lo necesitas no hay otra cosa que lo haga.

Cuando tengas stageado lo que forma el primer cambio lógico, un git commit normal lo cierra. Repites git add -p con lo que queda y sacas el segundo commit. Un working tree caótico sale convertido en tres commits atómicos sin haber tocado el código.

¿Cómo separo un commit que ya hice pero mezcla dos cosas?

Si el commit todavía es local y no lo has compartido, git reset --soft HEAD~1 lo deshace y conserva todos los cambios en el área de staging, tal cual estaban. El commit desaparece del historial, pero ni una línea de tu código se pierde: la copia de trabajo y el índice quedan intactos. Desde ahí ya puedes re-trocearlo.

# Deshace el último commit; los cambios quedan en staging
git reset --soft HEAD~1

# Sácalos del staging para elegir qué va en cada commit
git restore --staged .

# Primer cambio lógico
git add src/registro.ts
git commit -m "feat: validar email en el registro"

# Segundo cambio lógico, por partes si hace falta
git add -p
git commit -m "fix: normalizar acentos en el buscador"

El aviso importante: esto reescribe historia. Hazlo solo con commits que aún viven únicamente en tu máquina. Si ya hiciste push a una rama que comparten otros, no toques ese commit con reset ni con rebase: alguien puede haber construido encima, y reescribir lo que ya tiene te obliga a un push --force que le machaca el trabajo. Para commits ya compartidos, la vía limpia es un commit nuevo encima que corrija lo que haga falta.

Errores comunes y cómo evitarlos

La teoría se entiende rápido. Los tropiezos son siempre los mismos, así que vale la pena nombrarlos.

Confundir atómico con “un commit por archivo”. Es el malentendido número uno. Un cambio lógico puede tocar el controlador, el servicio y su test a la vez, y los tres van en el mismo commit porque son la misma idea. Partirlos por archivo rompe la atomicidad en la otra dirección: te deja commits que por separado ni compilan.

Mezclar refactor y feature. Ya lo vimos arriba, pero es tan frecuente que repito el remedio: refactor primero y solo, feature después. Si te encuentras escribiendo un mensaje con “y”, ese “y” es la costura por donde separar dos commits.

Creer que git commit -am incluye los archivos nuevos. No lo hace, y aquí se cuela el bug silencioso. El flag -a stagea automáticamente las modificaciones y los borrados de archivos que Git ya rastrea, pero ignora por completo los archivos nuevos. Haces git commit -am "añade servicio de pagos", el commit sale verde, y el archivo pagos.ts que acabas de crear se ha quedado fuera. El equipo se descarga la rama y no compila. Antes de un -am, un git status de dos segundos te ahorra el sofoco.

Dejar commits que no compilan “para arreglar en el siguiente”. Suena inofensivo y es el que más caro sale. En el momento no molesta, pero cualquier git bisect futuro se estrella contra ese commit, y git revert sobre commits vecinos arrastra el fallo. Un commit atómico compila y pasa sus tests. Sin excepciones.

Checklist antes de cada commit

  • El mensaje describe el cambio en una frase, sin necesitar “y”.
  • El código compila y sus tests pasan en este commit, no en el siguiente.
  • El refactor, si lo hay, va en su propio commit antes de la feature.
  • Has revisado el diff con git diff --staged antes de confirmar.
  • Si creaste archivos nuevos, los has añadido con git add (un -am no basta).
  • Solo reescribes con reset o rebase commits que aún no has compartido.

Cambiar este hábito no se lee, se practica: la primera vez que partes un working tree revuelto con git add -p es cuando hace clic. En el curso Git de cero a profesional tienes ejercicios interactivos donde trozeas commits reales paso a paso, con feedback inmediato, en vez de leer sobre ello. Si prefieres seguir por tu cuenta, el recopilatorio de prácticas de Git para 2026 recoge el resto de piezas.

Preguntas Frecuentes

¿Un commit atómico tiene que ser pequeño?

No. Atómico se refiere a que el commit contiene un único cambio lógico completo, no a su tamaño: una feature bien acotada puede tocar varios archivos y sumar bastantes líneas sin dejar de ser atómica.

¿Cada cuánto debo commitear?

Cada vez que una pieza esté completa y en verde, sin esperar al final del día. Si has terminado de extraer una función y compila, ciérrala en un commit antes de empezar lo siguiente. Commitear a menudo es lo que evita que se te acumulen cuatro cambios sin relación que luego tienes que separar con git add -p.

¿git add -p sirve para archivos nuevos?

No directamente. Un archivo sin rastrear no tiene versión anterior con la que comparar, así que Git no genera hunks para él y git add -p lo ignora. El truco es marcarlo primero con git add -N (intent-to-add): eso registra en el índice que el archivo existe pero sin contenido, y a partir de ahí git add -p ya te ofrece sus líneas como hunks normales.

git add -N pagos.ts   # intent-to-add: registra el archivo, aún sin contenido
git add -p pagos.ts   # ahora sí te ofrece sus líneas como hunks

¿Puedo arreglar un commit que ya empujé?

Con cuidado, y solo si nadie más ha construido sobre él. Si estás seguro de que ese commit únicamente vive en tu rama y nadie lo ha descargado, puedes reescribirlo con git reset o git rebase y hacer un push --force-with-lease, que es más seguro que --force porque aborta si el remoto cambió desde tu última sincronización. Si la rama es compartida y hay dudas, no lo reescribas: haz un commit nuevo encima que corrija lo que necesites y déjalo en el historial.

Un concepto nuevo cada semana