Git diff explicado con ejemplos: qué cambiaste antes del commit

Qué compara git diff, cómo leer su salida, git diff --staged, diff entre ramas y por qué a veces sale vacío. Con salida real de un repo de prueba.

Git diff explicado con ejemplos: qué cambiaste antes del commit

git diff te enseña, línea a línea, qué has cambiado en tus archivos y todavía no has decidido guardar. Sin argumentos compara tus archivos con el índice; con --staged compara el índice con el último commit; y con dos ramas compara una rama con otra. Casi toda la confusión con git diff viene de mezclar esas tres comparaciones.

Para seguir el post necesitas saber hacer git add y git commit. Si te suenan pero no tienes claro qué pasa entre uno y otro, lo tienes en add, commit y push explicados paso a paso.

Las tres comparaciones de git diff

Una misma versión de tu archivo puede estar en varios sitios a la vez, y git diff siempre compara dos de ellos[1]. El directorio de trabajo (tu carpeta) son los archivos que ves y editas. El índice (o staging area) es la zona donde dejas preparado lo que irá en el próximo commit: lo que hace git add es copiar ahí tu cambio. Y HEAD es tu último commit. Si el concepto de índice no te termina de cuadrar, empieza por qué es el staging area de Git y para qué sirve.

Con eso, los comandos se leen solos. git diff es carpeta contra índice: lo que podrías añadir y aún no has añadido. git diff --staged es índice contra HEAD: lo que ya has preparado. git diff HEAD es carpeta contra HEAD, todo junto. Y git diff main..mi-rama compara dos commits, que pueden ser las puntas de dos ramas.

En abstracto suena a trabalenguas, así que mejor verlo en un repo.

Un repo de prueba con salida real

Toda la salida de este post es real, de un repo de prueba con git 2.43.0. Partimos de un archivo saludo.js con un commit y cambiamos una línea. Este es su contenido inicial:

function saludar(nombre) {
  return "Hola, " + nombre;
}

console.log(saludar("Sofía"));
# Repo nuevo con un único commit
git init -b main && git add saludo.js && git commit -m "Añade saludo"

# Edito saludo.js: añado una exclamación al return
git diff
diff --git a/saludo.js b/saludo.js
index 1ca8ae3..08befec 100644
--- a/saludo.js
+++ b/saludo.js
@@ -1,5 +1,5 @@
 function saludar(nombre) {
-  return "Hola, " + nombre;
+  return "Hola, " + nombre + "!";
 }
 
 console.log(saludar("Sofía"));

La primera vez impone. Son once líneas para un cambio que cabe en una, pero casi todas dicen algo útil.

Cómo leer la salida de git diff

La salida de git diff tiene una cabecera por archivo y uno o varios bloques de cambios (los hunks)[2]. Línea a línea:

  • diff --git a/saludo.js b/saludo.js: qué archivo comparas. a/ es la versión de antes y b/ la de después.
  • index 1ca8ae3..08befec 100644: identificadores internos del contenido antes y después, y el modo del archivo. Para leer un diff puedes ignorarla.
  • --- a/saludo.js y +++ b/saludo.js: a partir de aquí, - significa “versión de antes” y + “versión de después”.
  • @@ -1,5 +1,5 @@: dónde está el hunk. -1,5 es “desde la línea 1, cinco líneas de la versión vieja” y +1,5 lo mismo en la nueva. Después de las arrobas Git puede escribir la última línea anterior al hunk que parece el inicio de una función. Es una pista orientativa, no siempre exacta.

Debajo vienen las líneas en sí. Las que empiezan por espacio no han cambiado: son contexto, tres líneas por defecto alrededor de cada cambio[3]. Las que empiezan por - existían antes y ya no, y las que empiezan por + son nuevas. Una línea modificada aparece siempre como pareja: la vieja con - y la nueva con +.

Con eso ya sabes leer cualquier diff. Falta saber por qué el mismo cambio a veces aparece y a veces no.

git diff —staged: lo que ya está preparado

Ahora hago git add saludo.js y, sin hacer commit, añado otra línea al final del archivo. Tengo un cambio en el índice y otro solo en mi carpeta. Mira qué responde cada variante:

$ git diff
diff --git a/saludo.js b/saludo.js
index 08befec..7698288 100644
--- a/saludo.js
+++ b/saludo.js
@@ -3,3 +3,4 @@ function saludar(nombre) {
 }
 
 console.log(saludar("Sofía"));
+console.log(saludar("Diego"));

git diff solo enseña la línea de Diego. La exclamación ha desaparecido de la salida porque ya está en el índice, y este comando compara la carpeta contra el índice. El function saludar(nombre) { detrás de las arrobas es la pista que Git asocia al hunk, aunque la línea de Diego está fuera de la función.

$ git diff --staged
diff --git a/saludo.js b/saludo.js
index 1ca8ae3..08befec 100644
--- a/saludo.js
+++ b/saludo.js
@@ -1,5 +1,5 @@
 function saludar(nombre) {
-  return "Hola, " + nombre;
+  return "Hola, " + nombre + "!";
 }
 
 console.log(saludar("Sofía"));

git diff --staged (o --cached, que es exactamente lo mismo[1]) enseña lo contrario: solo la exclamación. Esto es lo que entraría si hicieras git commit ahora mismo, y es el comando que yo ejecuto justo antes de cada commit.

git diff HEAD junta las dos cosas en un único hunk @@ -1,5 +1,6 @@ con la exclamación y la línea de Diego, porque compara tu carpeta directamente con el último commit, sin pasar por el índice.

git diff entre ramas: dos puntos o tres

Para comparar ramas pasas dos nombres. git diff main..despedida es lo mismo que git diff main despedida: compara la punta de una rama con la punta de la otra[1]. En mi repo hice commit de esos cambios, creé la rama con git switch -c despedida y añadí en ella despedida.js:

$ git diff main..despedida --stat
 despedida.js | 5 +++++
 1 file changed, 5 insertions(+)

La trampa llega cuando main avanza. Hice un commit en main que añade un README.md y repetí la comparación:

$ git diff main..despedida --stat
 README.md    | 1 -
 despedida.js | 5 +++++
 2 files changed, 5 insertions(+), 1 deletion(-)

$ git diff main...despedida --stat
 despedida.js | 5 +++++
 1 file changed, 5 insertions(+)

Con dos puntos, Git te dice que tu rama “borra” el README, aunque nunca lo has tocado: simplemente tu rama no lo tiene. Con tres puntos compara tu rama con el punto donde se separó de main, así que solo ves lo que hiciste en ella. Si quieres saber qué aporta tu rama, casi siempre quieres los tres puntos.

Variantes de git diff que vas a usar

ComandoQué comparaCuándo lo uso
git diffCarpeta contra índiceVer lo que he cambiado y aún no he añadido
git diff --stagedÍndice contra último commitJusto antes de git commit
git diff HEADCarpeta contra último commitTodo lo que llevo desde el último commit, preparado o no
git diff --statLo mismo, resumidoCuántas líneas por archivo, antes de mirar el detalle
git diff --name-onlyLo mismo, solo nombresPara saber qué archivos han cambiado, sin más
git diff main..ramaPunta contra puntaDiferencia total entre dos ramas
git diff main...ramaAncestro común contra ramaLo que aporta mi rama
git diff -- archivoSolo ese archivoCuando el diff completo es demasiado largo
git diff -wIgnorando espaciosDespués de reindentar o formatear

--stat, --name-only, -w y -- archivo se combinan con cualquiera de las otras: git diff --staged --stat es perfectamente válido[3].

Cuando un agente de IA te ha tocado varios archivos, git diff --stat y luego git diff -- archivo te enseñan qué ha hecho antes de aceptarlo. El proceso completo está en cómo revisar con Git el código que genera la IA.

Por qué git diff sale vacío

Un git diff vacío significa que las dos cosas que compara son iguales. Las causas habituales son cuatro.

Ya hiciste git add. El cambio está en el índice y la carpeta coincide con él. Usa git diff --staged.

El archivo es nuevo y nunca lo has añadido. Git no lo sigue, así que no está en el índice y git diff no tiene con qué compararlo. En mi repo, con un notas.md recién creado, git diff no decía nada y git status --short sí lo listaba con ??. Si quieres verlo en el diff sin prepararlo todavía, git add -N notas.md lo registra en el índice sin contenido y a partir de ahí aparece como archivo nuevo[4].

Ya hiciste commit. Carpeta, índice y HEAD coinciden. Para ver lo que metiste en el último commit, compáralo con el anterior: git diff HEAD~1 HEAD.

Solo has cambiado espacios y usas -w. Al reindentar saludo.js con cuatro espacios, git diff me marcaba la línea como cambiada y git diff -w no imprimía nada. Era correcto: -w ignora las diferencias de espacios[3].

git diff vs git status

git status te dice qué archivos han cambiado y en qué zona están; git diff te dice qué líneas han cambiado dentro de ellos[5]. En el momento en que tenía la exclamación preparada y la línea de Diego sin preparar, esto decía git status --short:

$ git status --short
MM saludo.js

La primera M es lo que enseña git diff --staged y la segunda lo que enseña git diff. Por eso yo los uso en ese orden: git status para ver el mapa y git diff para ver el detalle del archivo que me interesa. Lo que significa cada letra de esa columna lo tienes en cómo entender la salida de git status.

Si quieres practicar estos comandos sobre un repo con ejercicios guiados en lugar de leerlos, en el curso Git de cero a profesional los trabajas uno a uno.

Fuentes

  1. git-diff, documentación oficial de Git (fuente en el repositorio git/git): qué compara cada forma, --staged como sinónimo de --cached, y el significado de A..B y A...B.
  2. diff-generate-patch, documentación oficial de Git: cabecera diff --git, línea index, prefijos -, + y espacio, y el nombre de función en la cabecera del hunk.
  3. diff-options, documentación oficial de Git: tres líneas de contexto por defecto, --stat, --name-only, -w y --exit-code.
  4. git-add, documentación oficial de Git: --intent-to-add (-N) para ver un archivo nuevo en git diff.
  5. git-status, documentación oficial de Git: qué diferencias lista git status entre HEAD, índice y directorio de trabajo.
  6. Configuración core.pager, documentación oficial de Git: el paginador por defecto, normalmente less.

Preguntas Frecuentes

¿Qué diferencia hay entre git diff —staged y git diff —cached?

Ninguna: --staged es un sinónimo de --cached y los dos comparan el índice con el último commit.

¿Cómo salgo de la pantalla de git diff?

Cuando la salida no cabe en la terminal, Git la abre en un paginador, que normalmente es less[6]. Pulsa q para salir y vuelves a la terminal sin haber cambiado nada.

¿Cómo veo con git diff lo que metí en un commit que ya hice?

Compara ese commit con el anterior. Para el último commit:

git diff HEAD~1 HEAD

¿Puedo usar git diff en un script para saber si hay cambios?

Sí. Con git diff --exit-code el comando termina con código 1 si hay diferencias y con 0 si no las hay, y git diff --quiet hace lo mismo sin imprimir nada. Así un script puede negarse a seguir si quedan cambios sin añadir[3]. Para incluir también lo preparado usa git diff HEAD --quiet; los archivos nuevos sin seguimiento no los ve ninguno de los dos (para eso, git status --porcelain).