En la lección anterior recorrimos el ciclo completo —editar, preparar, confirmar— y vimos cómo git status nos guía en cada paso. Pero usamos git add y git commit en su forma más elemental: preparar ficheros enteros y confirmar con un mensaje corto.

En la práctica eso se queda corto enseguida. Una tarde de trabajo real no produce cambios ordenados: produce un fichero con dos arreglos que no tienen nada que ver entre sí, un fichero de configuración que nunca debió versionarse, otro que hay que renombrar y un mensaje de confirmación con una errata. Git tiene una respuesta precisa para cada una de esas situaciones, y dominarlas es la diferencia entre un historial que se puede leer y uno que solo se puede sufrir.

Esta lección es la más densa del módulo. Vamos a ver git add a fondo —incluida la diferencia entre -A, -u y ., que confunde a casi todo el mundo— el modo interactivo por fragmentos, cómo deshacer una preparación, cómo mover y borrar ficheros dentro de Git, y todas las variantes útiles de git commit, incluida --amend. Cerraremos con el concepto que da sentido a todo: el commit atómico.

Contenido

  1. git add: las formas de indicar qué preparar
  2. -A frente a -u frente a .: la tabla que resuelve la duda
  3. Preparación por fragmentos con git add -p
  4. Quitar del área de preparación: git restore --staged
  5. Descartar cambios del directorio de trabajo: git restore
  6. Mover y renombrar con git mv
  7. Borrar con git rm (y el caso especial --cached)
  8. git commit: todas las formas de confirmar
  9. --amend: corregir la última confirmación
  10. El commit atómico

  1. git add: las formas de indicar qué preparar

git add copia el contenido actual de uno o varios ficheros al área de preparación. Acepta muchas formas de decirle sobre qué actuar:

# Un fichero concreto
git add app.js

# Varios ficheros
git add app.js estilos.css index.html

# Un directorio entero, recursivamente
git add src/

# Todo lo que hay bajo el directorio actual
git add .

# Todo el repositorio, estés donde estés dentro de él
git add -A

# Un patrón (comillas obligatorias, ver más abajo)
git add "*.css"

Un detalle importante sobre los patrones

Cuando escribes git add *.css sin comillas, quien expande el asterisco es tu intérprete de órdenes, no Git. El resultado es que solo se expanden los ficheros del directorio actual, y si no hay ninguno el comando falla.

Con comillas, el patrón llega intacto a Git, que lo aplica de forma recursiva sobre todo el repositorio:

git add "*.css"        # todos los .css del repositorio, a cualquier profundidad
git add *.css          # solo los .css de la carpeta actual (los expande el shell)

Es una diferencia sutil con consecuencias reales. La recomendación: usa comillas cuando quieras el comportamiento de Git y sé consciente de cuál estás pidiendo.

git add también prepara borrados y ficheros nuevos

Un malentendido frecuente es pensar que git add sirve solo para "añadir". En realidad su función es sincronizar el área de preparación con el directorio de trabajo para las rutas indicadas. Eso incluye:

  • Ficheros nuevos → se empiezan a rastrear.
  • Ficheros modificados → se actualiza su versión preparada.
  • Ficheros borrados del disco → se prepara el borrado.

Este último caso sorprende:

rm antiguo.js
git add antiguo.js       # prepara el BORRADO de antiguo.js
git status -s
# → D  antiguo.js

Sí: git add sobre un fichero que ya no existe registra su desaparición. Tiene sentido si piensas en add como "toma nota del estado actual de esta ruta".

Comprobar sin ejecutar

Dos opciones muy útiles antes de un add amplio:

# Muestra qué haría, sin hacerlo
git add --dry-run .
git add -n .
add 'app.js'
add 'estilos.css'
add 'src/utilidades.js'
# Muestra el detalle de lo que va añadiendo
git add --verbose .

En un repositorio grande o desconocido, git add -n . cuesta un segundo y evita disgustos.

  1. -A frente a -u frente a .: la tabla que resuelve la duda

Esta es, con diferencia, la confusión más extendida sobre git add. Las tres formas se parecen y hacen cosas distintas. La diferencia se explica con dos ejes: qué tipos de cambio incluyen y sobre qué parte del repositorio actúan.

Forma Ficheros nuevos Ficheros modificados Ficheros borrados Alcance
git add . Solo desde el directorio actual hacia abajo
git add -A (--all) Todo el repositorio, estés donde estés
git add -u (--update) No Todo el repositorio
git add <ruta> Solo esa ruta

Dos conclusiones se leen directamente de la tabla:

  • -u es el único que no incluye ficheros nuevos. Actúa exclusivamente sobre ficheros que Git ya rastrea. Es la opción correcta cuando quieres registrar todo tu trabajo sobre ficheros conocidos sin arriesgarte a añadir un fichero nuevo por accidente.
  • . y -A hacen lo mismo salvo por el alcance. La diferencia solo se nota si no estás en la raíz del repositorio.

Demostración práctica

Preparemos el escenario en el repositorio de Ana:

cd ~/proyectos/gestor-tareas
mkdir -p src
echo "// utilidades" > src/utilidades.js     # nuevo, dentro de src/
echo "/* nuevo */" >> estilos.css            # modificado, en la raíz
rm README.md                                 # borrado, en la raíz
echo "# temporal" > BORRADOR.md              # nuevo, en la raíz
git status -s
 M estilos.css
 D README.md
?? BORRADOR.md
?? src/

Caso A — git add . desde la raíz:

git add .
git status -s
A  BORRADOR.md
D  README.md
M  estilos.css
A  src/utilidades.js

Todo entra: nuevos, modificados y borrados.

Caso B — git add . desde un subdirectorio:

git reset            # deshacemos la preparación anterior
cd src
git add .
git status -s
 M estilos.css
 D README.md
?? BORRADOR.md
A  src/utilidades.js

Aquí está la trampa. Solo se ha preparado src/utilidades.js, porque . significa "el directorio actual". Los cambios de la raíz siguen sin preparar. Alguien que confirmara ahora, convencido de haber añadido todo, dejaría fuera tres cambios.

Caso C — git add -A desde el mismo subdirectorio:

git reset
git add -A            # seguimos dentro de src/
git status -s
A  BORRADOR.md
D  README.md
M  estilos.css
A  src/utilidades.js

-A ignora dónde estás y actúa sobre todo el repositorio. Es el comportamiento que la mayoría de la gente cree estar pidiendo cuando escribe git add ..

Caso D — git add -u:

git reset
git add -u
git status -s
D  README.md
M  estilos.css
?? BORRADOR.md
?? src/

Solo se han preparado los cambios de ficheros ya rastreados: la modificación de estilos.css y el borrado de README.md. Los dos ficheros nuevos siguen sin seguimiento. Este es el comportamiento que quieres cuando estás trabajando en medio de una carpeta llena de ficheros generados y no te fías de un add masivo.

Nota histórica. Antes de Git 2.0, git add . no preparaba los borrados, y esa asimetría causaba muchísimos problemas. Desde Git 2.0 el comportamiento es el descrito en la tabla. Si lees documentación antigua que dice lo contrario, está desactualizada.

Cuál usar

  • git add <ruta> cuando sabes exactamente qué quieres. Es la opción por defecto de quien cuida su historial.
  • git add -u para registrar todo tu trabajo sobre ficheros conocidos, sin sorpresas.
  • git add -A cuando de verdad quieres todo y tienes un .gitignore en el que confías.
  • git add . solo si estás en la raíz y sabes que estás en la raíz.

Y en cualquier caso: git status antes de confirmar.

  1. Preparación por fragmentos con git add -p

Llega el martes siguiente. Ana quiere implementar el marcado de tareas como completadas, pero mientras está en ello ve un color feo en estilos.css y lo corrige de paso. Al terminar, app.js contiene dos cambios sin relación: la funcionalidad nueva y una corrección menor de un mensaje de consola que también arregló al vuelo.

Preparar app.js entero mezclaría las dos cosas en la misma confirmación. La solución es el modo por fragmentos:

git add -p app.js
git add --patch app.js     # forma larga

Git recorre el fichero mostrando los cambios en trozos (hunks) y pregunta qué hacer con cada uno:

diff --git a/app.js b/app.js
index 7b2e8f1..3c9d4a2 100644
--- a/app.js
+++ b/app.js
@@ -12,4 +12,5 @@ function anadirTarea(texto) {
   tareas.push({ id: Date.now(), texto: texto, hecha: false });
   pintarLista();
+  actualizarContador();
 }

(1/3) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

Las respuestas posibles son muchas, pero en la práctica se usan cinco:

Tecla Acción
y , prepara este fragmento
n No, déjalo sin preparar
s Divide (split) el fragmento en otros más pequeños
e Edita el fragmento a mano, línea a línea
q Sal del modo interactivo
a Prepara este fragmento y todos los que quedan del fichero
d No prepares este ni ninguno de los que quedan del fichero
? Muestra la ayuda con todas las opciones

Veamos la sesión de Ana completa. Primer fragmento: la llamada al contador, que forma parte de la funcionalidad nueva.

(1/3) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y

Segundo fragmento: la función de marcado.

@@ -25,4 +26,12 @@ function pintarLista() {
     const li = document.createElement('li');
     li.textContent = tarea.texto;
+    li.addEventListener('click', function () {
+      tarea.hecha = !tarea.hecha;
+      pintarLista();
+      actualizarContador();
+    });
+    if (tarea.hecha) {
+      li.classList.add('hecha');
+    }
     lista.appendChild(li);

(2/3) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y

Tercer fragmento: la corrección del mensaje de consola, que no pertenece a esta confirmación.

@@ -48,4 +57,4 @@ document.querySelector('#form-tarea').addEventListener('submit', function (event
   if (campo.value.trim() !== '') {
     anadirTarea(campo.value.trim());
-    console.log('tarea aniadida');
+    console.log('Tarea añadida correctamente');
     campo.value = '';

(3/3) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? n

Resultado:

git status -s
MM app.js
 M estilos.css

app.js sale como MM: una parte preparada (la funcionalidad) y otra pendiente (el mensaje). Ahora Ana puede confirmar solo lo primero:

git commit -m "Marcar tareas como completadas al hacer clic"
[main 9d1e4b7] Marcar tareas como completadas al hacer clic
 1 file changed, 9 insertions(+)

Y después, en una confirmación aparte, lo demás:

git add app.js estilos.css
git commit -m "Corregir el mensaje de consola y el color del texto secundario"

Dos confirmaciones limpias donde había un revoltijo.

s: dividir un fragmento

Si dos cambios independientes caen dentro del mismo fragmento porque están cerca, s lo parte:

(1/2) Stage this hunk [y,n,q,a,d,s,e,?]? s
Split into 2 hunks.

Git solo puede dividir cuando hay al menos una línea de contexto sin cambios entre los dos bloques. Si los cambios son literalmente contiguos, s no estará disponible y habrá que usar e.

e: editar el fragmento a mano

Es la opción más potente y la que más asusta. Git abre el fragmento en tu editor (el que configuraste en Configuración Inicial) con instrucciones al final:

# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.

Las dos reglas que necesitas:

  • Para no preparar una línea añadida (+), bórrala del fragmento.
  • Para no preparar una línea eliminada (-), cámbiale el - por un espacio.

Nunca borres una línea que empiece por - ni añadas líneas nuevas: el parche dejaría de aplicar y Git protestará (aunque te dará la oportunidad de reintentarlo).

Comandos relacionados. -p funciona también con otros comandos: git restore -p, git stash -p, git checkout -p. Y git add -i abre un menú interactivo completo, del que -p es solo una opción. En la práctica, -p cubre el 95 % de los casos.

  1. Quitar del área de preparación: git restore --staged

Preparaste algo que no querías. La operación inversa es:

git restore --staged <fichero>

Ejemplo. Ana prepara todo por costumbre y se da cuenta de que BORRADOR.md no debía entrar:

git add -A
git status -s
A  BORRADOR.md
M  app.js
M  estilos.css
git restore --staged BORRADOR.md
git status -s
M  app.js
M  estilos.css
?? BORRADOR.md

BORRADOR.md ha vuelto a ser un fichero sin seguimiento. Su contenido en el disco no se ha tocado: --staged actúa solo sobre el área de preparación.

La forma antigua: git reset HEAD

Verás muchísima documentación —y muchos compañeros— usando esto:

git reset HEAD <fichero>
git reset <fichero>          # equivalente, HEAD es el valor por defecto
git reset                    # quita TODO del área de preparación

Hace exactamente lo mismo. La diferencia es de diseño: git reset es un comando con tres modos y varios significados, y git checkout también estaba sobrecargado. Para resolverlo, Git 2.23 (2019) introdujo dos comandos con un propósito claro cada uno:

Tarea Forma moderna Forma antigua
Quitar del área de preparación git restore --staged <f> git reset HEAD <f>
Descartar cambios del disco git restore <f> git checkout -- <f>
Cambiar de rama git switch <rama> git checkout <rama>

Las formas antiguas siguen funcionando y las verás por todas partes. Usa las modernas en tu trabajo y reconoce las antiguas cuando las leas. El uso de git reset para mover el HEAD y reescribir el historial es otro asunto, y se trata en Deshaciendo Cambios.

  1. Descartar cambios del directorio de trabajo: git restore

Sin --staged, git restore hace algo bastante más serio: sobrescribe el fichero del disco con la versión de referencia, descartando tus cambios.

git restore <fichero>

Ana experimenta con un diseño en estilos.css, no le gusta nada y quiere volver atrás:

git status -s
# →  M estilos.css

git restore estilos.css

git status -s
# → (vacío)

El fichero ha vuelto a como estaba. Los cambios se han perdido de forma irrecuperable: no estaban en ninguna confirmación ni en el área de preparación, así que Git no tiene copia de ellos. Este es uno de los pocos comandos de Git que destruye trabajo sin red de seguridad.

Aviso serio. Antes de un git restore, pregúntate si de verdad quieres tirar ese trabajo. Si dudas, hay alternativas que lo guardan: git stash (lección 05-04) o simplemente confirmarlo y decidir después.

La combinación importa

Cuando un fichero está en estado MM —preparado y modificado después— hay que tener claro qué descarta cada variante:

Comando Área de preparación Directorio de trabajo
git restore --staged f Se restaura desde HEAD No se toca
git restore f No se toca Se restaura desde el área de preparación
git restore --staged --worktree f Se restaura desde HEAD Se restaura desde HEAD
git restore --source=HEAD~2 f Se pone la versión de hace 2 commits

Fíjate en la segunda fila: git restore f a secas restaura desde el área de preparación, no desde el último commit. Si habías preparado una versión intermedia, es a esa a la que vuelves.

Opciones útiles:

# Descartar cambios de TODOS los ficheros del repositorio (peligroso)
git restore :/

# Descartar por fragmentos, revisando cada uno
git restore -p estilos.css

# Traer al disco una versión antigua de un fichero
git restore --source=HEAD~3 app.js

Esta última no es un "deshacer": modifica tu directorio de trabajo con contenido antiguo, que después puedes preparar y confirmar normalmente. Es una forma sencilla de recuperar el estado de un fichero concreto sin tocar el resto del proyecto.

  1. Mover y renombrar con git mv

Ana ha creado un fichero notas-version.md y quiere llamarlo CAMBIOS.md. Podría hacerlo con el explorador de ficheros, pero entonces Git vería un fichero borrado y otro nuevo. Lo correcto es:

git mv notas-version.md CAMBIOS.md
git status -s
R  notas-version.md -> CAMBIOS.md

El código R indica un renombrado, y el cambio ya está preparado: git mv prepara el resultado automáticamente.

git mv es un atajo

Este comando no es magia. Es exactamente equivalente a tres órdenes:

mv notas-version.md CAMBIOS.md
git rm --cached notas-version.md
git add CAMBIOS.md

De hecho, si hubieras usado mv a secas, Git detectaría el renombrado igualmente al preparar ambos cambios:

mv notas-version.md CAMBIOS.md
git add -A
git status -s
# → R  notas-version.md -> CAMBIOS.md

Esto conecta con algo fundamental del modelo de datos que vimos en 01-04: Git no almacena renombrados. Un blob guarda contenido, y el nombre vive en el tree. Lo que Git hace es detectar renombrados a posteriori, comparando el contenido de los ficheros borrados y añadidos. Si el contenido es idéntico, la detección es infalible; si cambiaste mucho el fichero al moverlo, quizá no lo detecte y lo muestre como un borrado más un fichero nuevo.

Usos habituales

# Renombrar
git mv estilos.css estilos-principal.css

# Mover a un subdirectorio (debe existir)
mkdir css
git mv estilos.css css/estilos.css

# Mover varios ficheros a un directorio
git mv app.js utilidades.js src/

# Forzar la sobrescritura del destino
git mv -f borrador.md CAMBIOS.md

Y el caso incómodo: cambiar solo mayúsculas y minúsculas en macOS o Windows, donde el sistema de ficheros no distingue entre Leeme.md y LEEME.md. Como Bruno trabaja en macOS, le pasará:

git mv leeme.md LEEME.md
# → fatal: destination exists

La solución es el paso intermedio:

git mv leeme.md temporal.md
git mv temporal.md LEEME.md

O usar -f, que en las versiones recientes de Git resuelve este caso concreto.

  1. Borrar con git rm (y el caso especial --cached)

Para eliminar un fichero del proyecto:

git rm antiguo.js
rm 'antiguo.js'
git status -s
# → D  antiguo.js

Hace dos cosas a la vez: borra el fichero del disco y prepara el borrado. Equivale a rm antiguo.js && git add antiguo.js.

Si Git detecta que el fichero tiene cambios sin confirmar, se niega:

git rm app.js
error: the following file has local modifications:
    app.js
(use --cached to keep the file, or -f to force removal)

Es una protección deliberada: te avisa de que estás a punto de perder trabajo. Si estás seguro, -f.

git rm --cached: el caso realmente importante

Esta variante quita el fichero del control de versiones pero lo deja en el disco. Es la respuesta a un problema muy frecuente: un fichero que nunca debió versionarse y que ya está rastreado.

Recuerda del apartado 6 de la lección anterior: .gitignore solo actúa sobre ficheros sin seguimiento. Si config.local.json ya está en el historial, añadirlo al .gitignore no hace nada. La secuencia correcta es:

# 1. Dejar de rastrearlo, sin borrarlo del disco
git rm --cached config.local.json
rm 'config.local.json'
# 2. Asegurarse de que no vuelva
echo "config.local.json" >> .gitignore

# 3. Confirmar ambas cosas
git add .gitignore
git commit -m "Dejar de versionar la configuración local del entorno"
ls config.local.json
# → config.local.json     ← sigue ahí, intacto
git status -s
# → (vacío: ahora está ignorado)

Para un directorio entero hace falta -r (recursivo):

git rm -r --cached node_modules/

Un truco muy útil cuando has arreglado el .gitignore y quieres que se aplique a todo lo ya rastreado:

git rm -r --cached .        # deja de rastrear todo (sin borrar nada del disco)
git add .                   # vuelve a añadir, ahora respetando el .gitignore
git status -s               # revisa qué se ha quedado fuera
git commit -m "Aplicar el .gitignore a los ficheros ya versionados"

Advertencia importante. git rm --cached deja de rastrear un fichero desde ahora, pero no lo borra del historial. Todas las confirmaciones anteriores lo siguen conteniendo, y cualquiera con acceso al repositorio puede recuperarlo. Si el fichero contenía credenciales, esto no las protege: hay que rotarlas y, si es imprescindible, reescribir el historial. Lo trataremos en Mejores Prácticas de Seguridad.

Resumen de borrado

Comando Fichero en el disco Rastreado por Git
rm f (del sistema) Borrado Sigue rastreado; borrado sin preparar
git rm f Borrado Borrado, preparado
git rm --cached f Se conserva Deja de rastrearse
git rm -f f Borrado aunque tenga cambios Borrado, preparado
git rm -r --cached dir/ Se conserva Todo el directorio deja de rastrearse

  1. git commit: todas las formas de confirmar

Con el área de preparación lista, toca registrar. La forma que ya conoces:

git commit -m "Marcar tareas como completadas al hacer clic"

Y estas son las variantes que merece la pena conocer.

Sin -m: el editor

git commit

Git abre tu editor con una plantilla:

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch main
# Changes to be committed:
#	modified:   app.js
#	modified:   estilos.css
#

Escribes arriba, guardas y cierras. Si guardas sin escribir nada, el commit se cancela. Las líneas que empiezan por # se descartan.

Esta forma tiene dos ventajas sobre -m: te muestra qué vas a confirmar mientras escribes el mensaje (una última oportunidad de detectar un error), y facilita escribir mensajes de varias líneas.

Mensajes multilínea

Un mensaje de confirmación tiene dos partes: una primera línea de resumen y, opcionalmente, un cuerpo separado por una línea en blanco.

Desde el editor es natural. Desde la línea de órdenes, hay dos formas:

# Con varios -m: cada uno es un párrafo, separado por línea en blanco
git commit -m "Marcar tareas como completadas al hacer clic" \
           -m "El estado se guarda en el array en memoria; al recargar la página se pierde. La persistencia se abordará cuando exista el almacenamiento local."
# Con un salto de línea real (Bash y Zsh)
git commit -m "Marcar tareas como completadas al hacer clic

El estado se guarda en el array en memoria; al recargar la
página se pierde."

El resultado es idéntico. Muchos comandos —git log --oneline, la lista de confirmaciones de las plataformas web— muestran solo la primera línea, así que esa línea debe funcionar por sí sola.

-a: preparar y confirmar de un golpe

git commit -a -m "Ajustar el color del texto secundario"
git commit -am "Ajustar el color del texto secundario"     # abreviado

Prepara automáticamente todos los ficheros rastreados que estén modificados o borrados, y confirma.

Su riesgo es que se salta el área de preparación, es decir, se salta la decisión de qué entra. Con -a confirmas todo lo que hayas tocado, incluidos:

  • El console.log de depuración que dejaste en app.js.
  • El ajuste de tres líneas en un fichero que no tiene nada que ver con lo que estás confirmando.
  • El cambio que ibas a revisar mejor antes de registrarlo.

Además, no incluye ficheros sin seguimiento, lo que produce el error contrario: crees haber confirmado todo y el fichero nuevo se queda fuera, el proyecto no compila en la máquina de tu compañero y nadie entiende por qué.

git add + git commit git commit -a
Ficheros nuevos Se incluyen si los añades Nunca se incluyen
Ficheros modificados Los que elijas Todos
Ficheros borrados Los que elijas Todos
Control sobre el contenido Total Ninguno
Riesgo de commit revuelto Bajo Alto

Úsalo cuando estés seguro de que todo lo modificado pertenece a la misma confirmación, y aun así mira git status antes. Como práctica por defecto, no lo recomiendo.

Otras opciones útiles

# Ver el diff de lo que vas a confirmar mientras escribes el mensaje
git commit -v

# Confirmar aunque no haya nada preparado (útil en scripts y para marcar hitos)
git commit --allow-empty -m "Disparar el despliegue"

# Fijar una fecha concreta de autoría
git commit --date="2026-07-15 10:00:00" -m "..."

# Confirmar en nombre de otra persona (trabajo en pareja)
git commit --author="Bruno Salas <[email protected]>" -m "..."

# Firmar criptográficamente la confirmación
git commit -S -m "..."

git commit -v merece un comentario aparte: añade al pie de la plantilla el diff completo de lo preparado. Es la mejor forma de revisar tu propio trabajo justo antes de registrarlo, y muchos desarrolladores lo activan siempre:

git config --global commit.verbose true

  1. --amend: corregir la última confirmación

Ana confirma y, un segundo después, ve la errata en el mensaje:

git log --oneline -1
# → 9d1e4b7 Marcar tareas como completdas al hacer clic

Solución:

git commit --amend -m "Marcar tareas como completadas al hacer clic"
git log --oneline -1
# → 5c2e8b4 Marcar tareas como completadas al hacer clic

Corregido. Fíjate en que el hash ha cambiado: era 9d1e4b7 y ahora es 5c2e8b4.

--amend sirve para tres cosas:

# 1. Cambiar solo el mensaje
git commit --amend -m "Mensaje corregido"

# 2. Añadir un fichero olvidado al último commit
git add fichero-que-olvide.js
git commit --amend --no-edit          # --no-edit conserva el mensaje actual

# 3. Corregir la autoría
git commit --amend --author="Ana Ferrer <[email protected]>" --no-edit

El segundo caso es el más útil en el día a día: acabas de confirmar y descubres que faltaba un fichero. En lugar de crear una confirmación "Añadir el fichero que faltaba", lo incorporas al commit anterior y el historial queda limpio.

--amend reescribe el historial

Esto es lo que hay que entender bien, y conecta directamente con El Modelo de Datos de Git.

--amend no modifica la confirmación existente. No puede: los objetos de Git son inmutables, y su identificador es el hash de su contenido. Lo que hace es:

  1. Crear un commit nuevo con el mismo padre que el anterior y el contenido/mensaje corregidos.
  2. Mover la rama actual a ese commit nuevo.
  3. Dejar el commit antiguo huérfano, sin nada que apunte a él.
graph LR
    subgraph "Antes"
        A1["c5d9b1e"] --> B1["2f6a3c8"] --> C1["9d1e4b7<br/>(mensaje con errata)"]
        M1["main"] -.-> C1
    end
    subgraph "Después de --amend"
        A2["c5d9b1e"] --> B2["2f6a3c8"] --> C2["9d1e4b7<br/>(huérfano)"]
        B2 --> D2["5c2e8b4<br/>(mensaje corregido)"]
        M2["main"] -.-> D2
    end

De ahí la regla de oro:

Nunca hagas --amend sobre una confirmación que ya has compartido.

Si la confirmación 9d1e4b7 ya estaba en el servidor y Bruno la había descargado, al reescribirla tu historial y el suyo divergen: él tiene un commit que tú ya no tienes, y tu commit nuevo le resulta desconocido. Arreglarlo requiere trabajo y coordinación. Mientras la confirmación esté solo en tu máquina, --amend es seguro y muy recomendable.

La regla general —reescribe libremente lo local, nunca lo compartido— vale también para el rebase (módulo 5) y para todas las técnicas de limpieza de historial (08-02).

  1. El commit atómico

Todo lo anterior existe para una cosa: poder crear confirmaciones atómicas.

Un commit atómico es aquel que contiene un cambio completo y solo uno. Dos condiciones simultáneas:

  • Completo: incluye todo lo necesario para que ese cambio funcione. Si añades una función y su estilo, ambos van juntos; una confirmación que deja el proyecto roto no es atómica.
  • Único: no incluye nada que no pertenezca a ese cambio. Ni el arreglo de ortografía de paso, ni el console.log de depuración.

La regla mínima

Aquí va la única regla que necesitas por ahora, y que además funciona como test de atomicidad:

Si al describir tu confirmación necesitas la palabra "y", probablemente deberían ser dos confirmaciones.

Aplicado al caso de Ana: "Marcar tareas como completadas y corregir el mensaje de consola y ajustar el color". Tres "y" implícitos, tres confirmaciones. Por eso usó git add -p.

Por qué merece la pena

Con commits atómicos Con commits revueltos
git log cuenta la historia real del proyecto El historial es una lista de "cambios varios"
Deshacer un cambio concreto es trivial Deshacer arrastra cosas que no querías
git bisect encuentra el commit culpable exacto El commit culpable contiene diez cambios
Las revisiones de código son rápidas Nadie quiere revisar 40 ficheros mezclados
git blame explica de verdad cada línea git blame apunta a "cambios del viernes"

Los beneficios se cobran meses después, cuando algo falla y hay que averiguar por qué. Herramientas como Git Bisect y Git Blame dependen por completo de esta disciplina.

Sobre los mensajes

Un commit atómico necesita un mensaje que lo describa. Por ahora, tres normas suficientes:

  1. La primera línea, corta (unos 50 caracteres) y descriptiva por sí sola.
  2. En imperativo o infinitivo, describiendo lo que hace la confirmación: "Añadir el contador de tareas", no "Añadido el contador" ni "cambios".
  3. El qué en la primera línea, el porqué en el cuerpo si hace falta explicarlo.

Las convenciones completas —Conventional Commits, longitudes, cuerpo, pies, referencias a incidencias— se tratan en Escribiendo Buenos Mensajes de Confirmación. Con estas tres normas ya escribirás mejores mensajes que la media.

Errores Comunes y Consejos

  • Usar git add . desde un subdirectorio creyendo que añade todo. Solo añade desde ahí hacia abajo. Si quieres todo el repositorio, es git add -A.
  • Esperar que -u incluya ficheros nuevos. No lo hace, por diseño. Es su ventaja, no su defecto.
  • Confiar en git commit -a. Confirma todo lo modificado sin filtro y deja fuera lo nuevo. Es la receta de los commits revueltos y de los ficheros olvidados.
  • git restore <fichero> a la ligera. Destruye tus cambios sin copia de seguridad. Es de los poquísimos comandos de Git sin red. Si dudas, git stash.
  • Confundir git restore --staged con git restore. El primero quita del área de preparación y no toca el disco; el segundo sobrescribe el disco. La diferencia es un cambio deshecho frente a un cambio perdido.
  • Creer que git rm --cached borra el fichero del historial. No lo borra: sigue en todas las confirmaciones anteriores. Para secretos, esto no basta.
  • Hacer --amend sobre una confirmación ya compartida. Reescribe el historial y provoca divergencia con tus compañeros. Solo sobre lo que aún está en tu máquina.
  • Confirmar el trabajo de tres días en un solo commit. Nadie puede revisarlo, deshacerlo ni entenderlo. Confirma pronto y a menudo.
  • Consejo: activa git config --global commit.verbose true. Ver el diff mientras escribes el mensaje detecta muchísimos errores antes de que entren en el historial.
  • Consejo: si git add -p te resulta lento al principio, insiste una semana. Es el hábito que más mejora la calidad de un historial, y acaba siendo automático.
  • Consejo: cuando dudes de qué vas a confirmar, git diff --staged te lo enseña exactamente. Es el tema de la siguiente lección.

Ejercicios

Ejercicio 1: Dominar -A, -u y .

Monta este escenario:

mkdir -p ~/practica/add-test/componentes && cd ~/practica/add-test
git init
echo "inicial" > raiz.txt
echo "inicial" > componentes/boton.js
git add -A && git commit -m "Estado inicial"

echo "cambio" >> raiz.txt
echo "cambio" >> componentes/boton.js
echo "nuevo" > raiz-nuevo.txt
echo "nuevo" > componentes/menu.js
rm raiz.txt

Sitúate dentro de componentes/ y, ejecutando git reset entre pruebas para volver al punto de partida, determina qué prepara exactamente cada uno de estos comandos. Anota la salida de git status -s en cada caso y explica el porqué:

  1. git add .
  2. git add -A
  3. git add -u
  4. git add ..

Ejercicio 2: Separar dos cambios de un mismo fichero

En el repositorio de gestor-tareas (o en uno de prueba con un app.js similar), realiza estos dos cambios a la vez en app.js:

  • Cambio A (funcionalidad): añade una función borrarTarea(id) que elimine una tarea del array y repinte la lista.
  • Cambio B (mantenimiento): corrige un comentario mal escrito en la cabecera del fichero.

Después:

  1. Usa git add -p para preparar solo el cambio A.
  2. Demuestra con git status -s que el fichero está en estado MM.
  3. Confirma el cambio A con el mensaje "Añadir borrado de tareas al listado".
  4. Confirma el cambio B en una segunda confirmación.
  5. Comprueba con git log --oneline que hay dos confirmaciones y con git show --stat que cada una toca lo que debe.

Ejercicio 3: Rescatar un repositorio mal preparado

Un compañero te pasa un repositorio en este estado, justo después de haber confirmado:

git log --oneline -1
# → 7f3c9a2 arreglos varios

git show --stat HEAD
 .env                  |  3 +
 app.js                | 24 ++++++++---
 estilos.css           |  6 ++--
 node_modules/lib/a.js | 99 +++++++++++++++++
 README.md             |  2 +-
 5 files changed, 128 insertions(+), 6 deletions(-)

La confirmación no se ha compartido con nadie todavía. Resuelve:

  1. ¿Cuáles son los tres problemas de esta confirmación?
  2. Escribe la secuencia de comandos que deja de versionar .env y node_modules/, los añade al .gitignore y conserva ambos ficheros en el disco.
  3. Corrige el mensaje de la confirmación por uno descriptivo, sin crear una confirmación adicional.
  4. ¿Qué habría cambiado en tu respuesta si la confirmación ya se hubiera compartido? ¿Y qué medida adicional habría que tomar por el hecho de que .env haya estado versionado?

Soluciones

Solución al Ejercicio 1

Estado de partida (visto desde la raíz):

 M componentes/boton.js
 D raiz.txt
?? componentes/menu.js
?? raiz-nuevo.txt

1. git add . desde componentes/:

cd ~/practica/add-test/componentes
git add .
git status -s
M  componentes/boton.js
A  componentes/menu.js
 D raiz.txt
?? raiz-nuevo.txt

Prepara todo (nuevos, modificados y borrados) pero solo desde el directorio actual hacia abajo. Los cambios de la raíz quedan fuera.

2. git add -A:

git reset
git add -A
git status -s
M  componentes/boton.js
A  componentes/menu.js
D  raiz.txt
A  raiz-nuevo.txt

Prepara todo el repositorio, ignorando por completo dónde estás. Las cuatro entradas tienen la primera columna ocupada.

3. git add -u:

git reset
git add -u
git status -s
M  componentes/boton.js
D  raiz.txt
?? componentes/menu.js
?? raiz-nuevo.txt

Actúa sobre todo el repositorio, pero solo sobre ficheros rastreados: la modificación de boton.js y el borrado de raiz.txt. Los dos ficheros nuevos siguen sin seguimiento.

4. git add ..:

git reset
git add ..
git status -s
M  componentes/boton.js
A  componentes/menu.js
D  raiz.txt
A  raiz-nuevo.txt

Mismo resultado que -A, pero por otra razón: .. es la raíz del repositorio, así que "desde ahí hacia abajo" es todo. Confirma que la diferencia entre . y -A es exclusivamente el punto de partida del recorrido.

Resumen:

Comando (desde componentes/) boton.js menu.js raiz.txt raiz-nuevo.txt
git add . No No
git add -A
git add -u No No
git add ..

Solución al Ejercicio 2

Los dos cambios en app.js. Cabecera (cambio B):

// gestor-tareas — logica prinicpal        ← antes
// gestor-tareas — lógica principal        ← después

Y la función nueva (cambio A):

function borrarTarea(id) {
  const indice = tareas.findIndex(function (t) { return t.id === id; });
  if (indice !== -1) {
    tareas.splice(indice, 1);
    pintarLista();
    actualizarContador();
  }
}

1. Preparar solo el cambio A:

git add -p app.js

El primer fragmento que muestra Git es el de la cabecera (está más arriba en el fichero):

@@ -1,3 +1,3 @@
-// gestor-tareas — logica prinicpal
+// gestor-tareas — lógica principal
 const tareas = [];

(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? n

Respondemos n: no pertenece a esta confirmación. El segundo es la función:

@@ -30,6 +30,15 @@ function pintarLista() {
+function borrarTarea(id) {
+  const indice = tareas.findIndex(function (t) { return t.id === id; });
...

(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? y

2. Comprobar el estado MM:

git status -s
# → MM app.js

Primera columna M: hay una versión preparada (con la función nueva, sin la corrección del comentario). Segunda columna M: el disco difiere de ella.

Se puede verificar con más precisión:

git diff --staged --stat
# → app.js | 9 +++++++++
git diff --stat
# → app.js | 2 +-

Nueve líneas preparadas (la función) y una línea pendiente (el comentario). Exactamente la separación buscada.

3 y 4. Las dos confirmaciones:

git commit -m "Añadir borrado de tareas al listado"
# → [main 4e7f2a9] Añadir borrado de tareas al listado
# →  1 file changed, 9 insertions(+)

git add app.js
git commit -m "Corregir una errata en el comentario de cabecera"
# → [main 8a1d5c3] Corregir una errata en el comentario de cabecera
# →  1 file changed, 1 insertion(+), 1 deletion(-)

5. Verificación:

git log --oneline -2
8a1d5c3 (HEAD -> main) Corregir una errata en el comentario de cabecera
4e7f2a9 Añadir borrado de tareas al listado
git show --stat 4e7f2a9 | tail -2
# →  app.js | 9 +++++++++
# →  1 file changed, 9 insertions(+)

git show --stat 8a1d5c3 | tail -2
# →  app.js | 2 +-
# →  1 file changed, 1 insertion(+), 1 deletion(-)

Dos confirmaciones atómicas sobre el mismo fichero. Sin git add -p esto solo se conseguiría deshaciendo un cambio a mano, confirmando y volviéndolo a escribir.

Solución al Ejercicio 3

1. Los tres problemas:

  1. .env está versionado. Contiene credenciales; ahora están en el historial y las verá cualquiera con acceso al repositorio.
  2. node_modules/ está versionado. Son dependencias reinstalables: hinchan el repositorio, ensucian los diffs y no aportan nada.
  3. El mensaje "arreglos varios" no dice nada. Y, de fondo, la confirmación no es atómica: mezcla lógica (app.js), estilos (estilos.css) y documentación (README.md) con ficheros que no debían estar.

2. Dejar de versionar .env y node_modules/:

git rm --cached .env
git rm -r --cached node_modules/
rm '.env'
rm 'node_modules/lib/a.js'

--cached es la clave: los ficheros siguen en el disco, solo dejan de rastrearse.

cat >> .gitignore <<'FIN'
.env
node_modules/
FIN

git add .gitignore
git status -s
A  .gitignore
D  .env
D  node_modules/lib/a.js

Comprobación de que los ficheros siguen ahí:

ls -la .env && ls node_modules/lib/
# → -rw------- 1 ana ana 87 jul 28 10:12 .env
# → a.js

3. Corregir el mensaje sin crear una confirmación nueva.

Como los cambios del punto 2 están preparados y la confirmación no se ha compartido, se pueden incorporar a la misma confirmación con --amend, arreglando de paso el mensaje:

git commit --amend -m "Añadir el borrado de tareas y ajustar los estilos del listado"
[main 3b9e7d1] Añadir el borrado de tareas y ajustar los estilos del listado
 4 files changed, 29 insertions(+), 6 deletions(-)
git show --stat HEAD
 .gitignore   | 2 ++
 app.js       | 24 ++++++++---
 estilos.css  | 6 ++--
 README.md    | 2 +-
 4 files changed, 29 insertions(+), 6 deletions(-)

.env y node_modules/ han desaparecido de la confirmación, y el mensaje ya describe algo. El hash ha cambiado (7f3c9a23b9e7d1) porque, como sabemos, --amend crea una confirmación nueva.

Nota crítica: este --amend funciona limpiamente porque 7f3c9a2 era la única confirmación que contenía .env. Si el fichero llevara veinte confirmaciones en el historial, --amend solo lo sacaría de la última y seguiría estando en las diecinueve anteriores.

Sobre el mensaje: sigue teniendo una "y", así que lo verdaderamente correcto habría sido separar la confirmación en dos (funcionalidad y estilos). Con git reset y un par de git add -p se podría hacer, pero eso ya entra en el terreno de Manteniendo un Historial Limpio.

4. Si la confirmación ya se hubiera compartido.

Cambiarían dos cosas:

  • Nada de --amend. Reescribir una confirmación que otros ya tienen provoca divergencia entre historiales. La solución correcta sería una confirmación nueva encima:
git rm --cached .env
git rm -r --cached node_modules/
git add .gitignore
git commit -m "Dejar de versionar credenciales y dependencias"

El mensaje malo del commit anterior se queda ahí. Es el precio de haber publicado: el historial compartido es inmutable en la práctica.

  • Y, sobre todo, hay que rotar las credenciales. Este es el punto que no se puede pasar por alto: .env estuvo en el historial y, si ese historial se compartió, las claves se consideran comprometidas. Sacarlo del seguimiento no las protege; cualquiera que haya clonado el repositorio las tiene en su disco, y git show 7f3c9a2:.env las muestra. La única respuesta válida es invalidar esas credenciales y generar unas nuevas. Limpiar el historial (con git filter-repo o similar) es un paso complementario, nunca un sustituto. Lo veremos en Mejores Prácticas de Seguridad.

Conclusión

Esta lección te ha dado el control fino sobre qué entra en el historial y cómo. Recapitulando:

  • git add sincroniza rutas con el área de preparación, y eso incluye ficheros nuevos, modificados y borrados.
  • -A, -u y . se diferencian en dos ejes: -u es el único que excluye ficheros nuevos, y . es el único limitado al directorio actual. -A es "todo, en todas partes".
  • git add -p prepara por fragmentos y es la herramienta que permite separar dos cambios mezclados en un mismo fichero. Es el hábito que más mejora un historial.
  • git restore --staged quita del área de preparación sin tocar el disco; git restore sobrescribe el disco y destruye tus cambios sin red de seguridad. Sus equivalentes antiguos son git reset HEAD y git checkout --.
  • git mv y git rm hacen la operación del sistema de ficheros y la preparan de una vez. git rm --cached deja de rastrear conservando el fichero, y es la pieza que falta cuando un .gitignore "no funciona".
  • git commit admite mensaje en línea o en el editor, mensajes multilínea y la opción -a, que es cómoda y arriesgada: confirma todo lo modificado y deja fuera lo nuevo.
  • --amend corrige la última confirmación creando una nueva y descartando la anterior. Es seguro y muy útil mientras esa confirmación no se haya compartido.
  • El commit atómico —un cambio completo y solo uno— es el objetivo de todo lo anterior. La regla mínima: si necesitas "y" para describirlo, sepáralo.

Ana ya sabe elegir con precisión qué confirma. Pero hay una pieza que hemos usado sin explicar: en el modo -p, Git le mostraba los cambios en un formato con líneas + y -, cabeceras @@ y fragmentos. Ese formato aparece por todas partes en Git y saber leerlo es imprescindible.

En la siguiente lección, Inspeccionando Cambios con git diff, aprenderemos a ver exactamente qué ha cambiado antes de confirmar: la diferencia entre git diff, git diff --staged y git diff HEAD según qué zonas comparan, cómo leer el formato unified diff línea a línea, cómo comparar dos confirmaciones concretas o un fichero suelto, y las opciones que hacen la salida legible en los casos difíciles.

Dominando Git: De Principiante a Avanzado

Módulo 1: Introducción a Git

Módulo 2: Operaciones Básicas de Git

Módulo 3: Ramas y Fusión

Módulo 4: Trabajando con Repositorios Remotos

Módulo 5: Operaciones Avanzadas de Git

Módulo 6: Herramientas y Técnicas de Git

Módulo 7: Estrategias de Colaboración y Flujo de Trabajo

Módulo 8: Mejores Prácticas y Consejos de Git

Módulo 9: Solución de Problemas y Depuración

Módulo 10: Git en el Mundo Real

© Copyright 2026. Todos los derechos reservados