En la lección anterior aprendimos a decidir con precisión qué entra en cada confirmación. Pero hay una pregunta previa que git status no responde: ¿qué he cambiado exactamente?
git status te dice qué ficheros han cambiado y en qué zona están. No te dice qué líneas. Y esa diferencia es enorme: confirmar sin haber leído tus propios cambios es la forma más común de meter en el historial un console.log de depuración, una contraseña de pruebas, un bloque de código comentado o un cambio que creías haber deshecho.
git diff es la herramienta que responde a esa pregunta. En esta lección veremos sus tres formas fundamentales —y por qué producen resultados distintos según qué zonas comparen—, aprenderemos a leer el formato unified diff línea a línea (un formato que aparece en Git, en las revisiones de código, en los parches y en media informática), y recorreremos las opciones que hacen legible una salida difícil.
Al terminar deberías haber adquirido un hábito: git diff --staged justo antes de cada git commit.
Contenido
- Las tres formas de
git diffy qué compara cada una git diff: directorio de trabajo contra área de preparacióngit diff --staged: área de preparación contra el último commitgit diff HEAD: todo lo que ha cambiado- Leer el formato unified diff paso a paso
- Comparar confirmaciones concretas
- Limitar la comparación a ficheros o rutas
- Opciones que hacen legible la salida
git difftool: comparar con una herramienta visual- El hábito de revisar antes de confirmar
- Las tres formas de
git diff y qué compara cada una
git diff y qué compara cada unaTodo el aparente misterio de git diff se disuelve con una idea: siempre compara dos de las tres zonas, y la forma que uses determina cuáles.
graph LR
WT["DIRECTORIO<br/>DE TRABAJO"]
IDX["ÁREA DE<br/>PREPARACIÓN"]
REPO["ÚLTIMO COMMIT<br/>(HEAD)"]
WT ---|"git diff"| IDX
IDX ---|"git diff --staged"| REPO
WT -.-|"git diff HEAD"| REPO
En forma de tabla:
| Comando | Compara | Responde a la pregunta |
|---|---|---|
git diff |
Directorio de trabajo ↔ Área de preparación | ¿Qué he cambiado y todavía no he preparado? |
git diff --staged |
Área de preparación ↔ HEAD |
¿Qué va a entrar exactamente en el próximo commit? |
git diff --cached |
Idéntico a --staged |
(Sinónimo antiguo, sigue funcionando) |
git diff HEAD |
Directorio de trabajo ↔ HEAD |
¿Qué ha cambiado en total desde el último commit? |
git diff <sha1> <sha2> |
Dos confirmaciones | ¿Qué cambió entre estas dos versiones? |
Dos consecuencias que conviene interiorizar desde el principio:
-
git diffa secas no muestra los cambios preparados. Si preparas todo congit add -Ay luego ejecutasgit diff, la salida estará vacía. No es que no hayas cambiado nada: es que no hay diferencia entre tu disco y el área de preparación. Este es el desconcierto número uno congit diff. -
git diff HEADes la suma de las otras dos. Preparado o no, si difiere del último commit, aparece. -
Ninguna de las tres muestra ficheros sin seguimiento. Un fichero nuevo que nunca has añadido no tiene "versión anterior" con la que compararlo, así que
git difflo ignora por completo. Para eso estágit status.
git diff: directorio de trabajo contra área de preparación
git diff: directorio de trabajo contra área de preparaciónVolvamos al repositorio de Ana. Acaba de retocar estilos.css y todavía no ha preparado nada:
diff --git a/estilos.css b/estilos.css
index 2c9d4e6..8f1a3b7 100644
--- a/estilos.css
+++ b/estilos.css
@@ -12,4 +12,5 @@ body {
#lista-tareas li {
padding: 0.5rem 0;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid #e5e7eb;
+ cursor: pointer;
}En tres segundos sabe qué ha hecho: ha suavizado el color del borde y ha añadido el cursor de puntero. Ninguna sorpresa, ningún resto de depuración.
Ahora prepara el cambio y repite:
Exacto: el disco y el área de preparación coinciden, así que no hay nada que mostrar. Para ver ese cambio hay que preguntar por la otra comparación.
git diff --staged: área de preparación contra el último commit
git diff --staged: área de preparación contra el último commitEsta es la forma más importante de todas, porque muestra literalmente lo que vas a confirmar:
diff --git a/estilos.css b/estilos.css
index 2c9d4e6..8f1a3b7 100644
--- a/estilos.css
+++ b/estilos.css
@@ -12,4 +12,5 @@ body {
#lista-tareas li {
padding: 0.5rem 0;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid #e5e7eb;
+ cursor: pointer;
}--cached es un sinónimo exacto y más antiguo. --staged se añadió en Git 1.6 porque resultaba más comprensible; usa el que prefieras, aunque --staged es más claro.
El caso MM visto con diff
Aquí es donde git diff demuestra su valor. Ana sigue trabajando y añade una línea más a estilos.css después de haber preparado:
#lista-tareas li {
padding: 0.5rem 0;
border-bottom: 1px solid #e5e7eb;
cursor: pointer;
transition: opacity 0.2s;
}Ahora las tres formas dan tres respuestas distintas:
@@ -12,4 +12,5 @@
#lista-tareas li {
padding: 0.5rem 0;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid #e5e7eb;
+ cursor: pointer;
}@@ -12,4 +12,6 @@
#lista-tareas li {
padding: 0.5rem 0;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid #e5e7eb;
+ cursor: pointer;
+ transition: opacity 0.2s;
}Tres preguntas, tres respuestas precisas. El MM de git status te avisa de que hay algo raro; git diff te dice exactamente qué.
git diff HEAD: todo lo que ha cambiado
git diff HEAD: todo lo que ha cambiadoCompara tu directorio de trabajo con la última confirmación, ignorando el área de preparación. Es la respuesta a "¿qué he tocado desde el último commit?", que es la pregunta que uno se hace al volver de comer o al retomar el trabajo por la mañana.
HEAD no tiene nada de especial aquí: es simplemente una referencia a una confirmación, y git diff acepta cualquiera. Estas variantes son igual de válidas:
git diff HEAD~1 # contra la confirmación anterior a la última
git diff HEAD~5 # contra la de hace cinco confirmaciones
git diff 4e7f2a9 # contra una confirmación concretaLa notación HEAD~n y el resto de formas de referirse a una confirmación las veremos en detalle en Visualizando el Historial de Confirmaciones.
- Leer el formato unified diff paso a paso
El formato que produce git diff se llama unified diff y es un estándar de la informática desde los años ochenta. Lo verás en Git, en las revisiones de código de GitHub y GitLab, en los parches de correo y en la salida de decenas de herramientas. Saber leerlo es una habilidad transferible.
Vamos a diseccionar una salida completa, línea por línea. Ana ha modificado app.js:
diff --git a/app.js b/app.js
index 7b2e8f1..3c9d4a2 100644
--- a/app.js
+++ b/app.js
@@ -28,8 +28,12 @@ function pintarLista() {
lista.innerHTML = '';
for (const tarea of tareas) {
const li = document.createElement('li');
li.textContent = tarea.texto;
- li.className = 'tarea';
+ li.className = tarea.hecha ? 'tarea hecha' : 'tarea';
+ li.addEventListener('click', function () {
+ tarea.hecha = !tarea.hecha;
+ pintarLista();
+ });
lista.appendChild(li);
}
}Línea 1: la cabecera
Indica que empieza la comparación de un fichero. a/ es la versión antigua y b/ la nueva; son prefijos convencionales, no directorios reales. En un renombrado verías nombres distintos a cada lado.
Si el diff incluye varios ficheros, verás una de estas líneas por cada uno: es el separador que te dice dónde empieza cada bloque.
Línea 2: los identificadores de objeto
Los hashes abreviados de los blobs antiguo y nuevo, y el modo de fichero (100644 = fichero normal). Esto conecta directamente con El Modelo de Datos de Git: el diff no es una entidad guardada en Git, sino algo que Git calcula al vuelo comparando dos blobs. Si el modo cambiara (por ejemplo, al hacer un fichero ejecutable), verías old mode y new mode en líneas aparte.
Líneas 3 y 4: los marcadores de fichero
--- marca la versión antigua y +++ la nueva. Por eso, en el cuerpo del diff, - significa "estaba en la versión antigua" y + significa "está en la nueva".
Casos especiales que aclaran mucho:
Línea 5: la cabecera de fragmento (@@)
Esta es la línea que más cuesta al principio y la que más información contiene. Se lee así:
@@ -<línea inicial antigua>,<nº de líneas antiguas> +<línea inicial nueva>,<nº de líneas nuevas> @@ <contexto>
Aplicado al ejemplo:
| Parte | Valor | Significado |
|---|---|---|
-28,8 |
antigua | En la versión antigua, este bloque empieza en la línea 28 y abarca 8 líneas |
+28,12 |
nueva | En la versión nueva, empieza en la línea 28 y abarca 12 líneas |
function pintarLista() { |
contexto | La función o sección en la que está el cambio |
De los números se deduce todo: 8 líneas se han convertido en 12, luego el bloque ha crecido en 4. Y efectivamente, si cuentas el cuerpo: 7 líneas de contexto, 1 eliminada y 5 añadidas. Así se verifica cualquier cabecera @@:
- Líneas antiguas = contexto + eliminadas → 7 + 1 = 8.
- Líneas nuevas = contexto + añadidas → 7 + 5 = 12.
El texto del final no es una línea del fichero: Git lo extrae buscando hacia arriba la última línea que parece una declaración de función o sección. Sirve para orientarte cuando el diff es largo. Se puede afinar por lenguaje con .gitattributes, que veremos en Atributos de Fichero con .gitattributes.
Cuando el número de líneas es 1, se omite: @@ -12 +12 @@ significa "una línea a cada lado".
El cuerpo: las líneas del fragmento
Cada línea del cuerpo empieza con un carácter que indica su naturaleza:
| Primer carácter | Significa |
|---|---|
(espacio) |
Contexto: la línea existe igual en ambas versiones |
- |
Eliminada: estaba en la versión antigua, ya no está |
+ |
Añadida: no estaba antes, ahora sí |
\ |
Nota especial, casi siempre \ No newline at end of file |
Aplicado a nuestro ejemplo:
lista.innerHTML = ''; ← contexto
for (const tarea of tareas) { ← contexto
const li = document.createElement('li'); ← contexto
li.textContent = tarea.texto; ← contexto
- li.className = 'tarea'; ← ELIMINADA
+ li.className = tarea.hecha ? 'tarea hecha' : 'tarea'; ← AÑADIDA
+ li.addEventListener('click', function () { ← AÑADIDA
+ tarea.hecha = !tarea.hecha; ← AÑADIDA
+ pintarLista(); ← AÑADIDA
+ }); ← AÑADIDA
lista.appendChild(li); ← contextoDos observaciones importantes:
- Git no entiende de "líneas modificadas". Una modificación se representa siempre como una eliminación seguida de una adición. Por eso el primer par de líneas aparece como
-y+aunque conceptualmente sea "la misma línea, cambiada". - Las líneas de contexto están por algo. Por defecto Git muestra 3 líneas de contexto a cada lado del cambio, para que puedas situarlo. Se ajusta con
-U<n>:
git diff -U0 # sin contexto: solo las líneas cambiadas
git diff -U10 # diez líneas de contexto a cada ladoEl detalle de \ No newline at end of file
Significa que ese fichero no termina con un salto de línea. Es una convención POSIX que muchas herramientas dan por hecha, y su ausencia genera diffs ruidosos: si alguien añade el salto final, todas las líneas cercanas pueden aparecer como cambiadas. Configurar el editor para que añada siempre el salto final evita este ruido.
Ejercicio mental rápido
Antes de seguir, lee este fragmento y responde: ¿cuántas líneas tenía el bloque antes y cuántas tiene ahora?
@@ -45,5 +45,3 @@ function actualizarContador() {
const pendientes = tareas.filter(t => !t.hecha).length;
- console.log('DEBUG pendientes:', pendientes);
- console.log('DEBUG total:', tareas.length);
document.querySelector('#contador').textContent = pendientes;
}Cinco antes, tres después: se han eliminado dos líneas de depuración. Este es exactamente el tipo de hallazgo que justifica revisar el diff antes de confirmar.
- Comparar confirmaciones concretas
git diff también compara dos puntos cualesquiera del historial:
Muestra todo lo que cambió entre esas dos confirmaciones, agregado en un solo diff. No importa cuántas confirmaciones haya en medio: compara los dos estados finales.
Formas equivalentes y muy usadas:
# Qué introdujo la última confirmación
git diff HEAD~1 HEAD
# Los cambios de las últimas tres confirmaciones, juntos
git diff HEAD~3 HEAD
# Desde una confirmación hasta el estado actual del disco
git diff 1a4c8d6Esta última merece una nota: cuando le das un solo argumento, git diff compara esa confirmación con tu directorio de trabajo, no con HEAD. Es la misma lógica que git diff HEAD.
Ver qué introdujo una confirmación concreta
Para inspeccionar una confirmación aislada, lo idiomático es:
que muestra los metadatos (autor, fecha, mensaje) y el diff. Es el tema de la lección siguiente.
Una nota sobre ramas
git diff acepta nombres de rama exactamente igual que hashes:
Y existe una notación de tres puntos, git diff main...desarrollo, que compara desde el punto en que ambas se separaron. Como en este módulo trabajamos siempre sobre main, lo dejamos apuntado: se desarrolla en el módulo 3.
- Limitar la comparación a ficheros o rutas
En un cambio que toca quince ficheros, ver el diff completo es inmanejable. Se acota con -- seguido de rutas:
# Un fichero concreto
git diff -- app.js
# Varios
git diff -- app.js estilos.css
# Un directorio entero
git diff -- src/
# Un patrón
git diff -- "*.css"El doble guion -- separa las opciones de las rutas. Es opcional cuando no hay ambigüedad, así que git diff app.js funciona igual. Se vuelve obligatorio cuando un nombre de fichero podría confundirse con un nombre de rama o de confirmación:
Sin el --, Git intentaría interpretar main como una referencia y, si existiera la rama, obtendrías algo completamente distinto de lo que pedías. Acostumbrarse a poner -- antes de las rutas es un buen hábito.
Se combina con todo lo anterior:
- Opciones que hacen legible la salida
--stat: el resumen
app.js | 27 +++++++++++++++++++++----- estilos.css | 9 ++++++++- index.html | 3 ++- 3 files changed, 33 insertions(+), 6 deletions(-)
Cada línea muestra el fichero, el número total de líneas afectadas y una barra proporcional de + (adiciones) y - (eliminaciones). Es la primera vista que conviene mirar ante un cambio grande: te dice dónde mirar después.
Variantes:
git diff --shortstat HEAD~3 HEAD
# → 3 files changed, 33 insertions(+), 6 deletions(-)
git diff --numstat HEAD~3 HEAD
# → 22 5 app.js
# → 8 1 estilos.css
# → 2 1 index.html--numstat da adiciones, eliminaciones y nombre separados por tabuladores: ideal para procesar con scripts.
--name-only y --name-status: solo los ficheros
--name-status añade una letra por fichero: M modificado, A añadido, D borrado, R renombrado (el número indica el porcentaje de similitud: R100 es un renombrado sin cambios de contenido).
Estas opciones son muy prácticas encadenadas con otros comandos:
--word-diff: comparar por palabras
En un fichero de texto (documentación, README, contenido HTML), cambiar una palabra hace que toda la línea aparezca como eliminada y añadida. Es ilegible:
-El gestor de tareas permite añadir y listar tareas pendientes del equipo.
+El gestor de tareas permite añadir, listar y borrar tareas pendientes del equipo.Con --word-diff el cambio real salta a la vista:
El gestor de tareas permite [-añadir y listar-]{+añadir, listar y borrar+} tareas pendientes del equipo.Lo eliminado va entre [- -] y lo añadido entre {+ +}. Con color en el terminal es todavía más claro.
Variantes:
git diff --word-diff=color # solo color, sin marcadores
git diff --color-words # equivalente abreviadoEs imprescindible para revisar textos y muy útil en CSS y HTML.
-w: ignorar espacios en blanco
Un reformateo automático, un cambio de indentación de tabuladores a espacios o un ajuste del editor pueden generar un diff de doscientas líneas donde el cambio real es de dos. La familia de opciones de espacios en blanco lo resuelve:
| Opción | Efecto |
|---|---|
-w, --ignore-all-space |
Ignora todos los espacios, estén donde estén |
-b, --ignore-space-change |
Ignora cambios en la cantidad de espacios, no su aparición o desaparición |
--ignore-space-at-eol |
Ignora espacios al final de línea |
--ignore-blank-lines |
Ignora líneas en blanco añadidas o eliminadas |
Un aviso: -w sirve para leer, no para decidir. Si el fichero es sensible a la indentación (Python, YAML, Makefile), esconder los cambios de espaciado puede ocultarte un error real. Úsalo para localizar el cambio de fondo y después vuelve al diff completo.
Otras opciones útiles
# Detectar código movido y mostrarlo con otro color
git diff --color-moved
# Diferencias entre caracteres, no palabras
git diff --word-diff-regex=.
# Forzar el color aunque la salida vaya a un fichero o a una tubería
git diff --color=always
# Mostrar también los ficheros binarios como diferencia binaria
git diff --binary
# Comparación de mayor calidad (algoritmo patience)
git diff --patience--color-moved es una joya poco conocida: cuando refactorizas moviendo un bloque de un sitio a otro, distingue visualmente "esto se ha movido" de "esto es nuevo".
git difftool: comparar con una herramienta visual
git difftool: comparar con una herramienta visualPara diffs grandes o para gente que se maneja mejor con una vista de dos columnas, Git puede delegar en una herramienta externa:
Acepta exactamente los mismos argumentos que git diff.
Ver qué herramientas tienes disponibles
'git difftool --tool=<tool>' may be set to one of the following: vimdiff vimdiff2 nvimdiff The following tools are valid, but not currently available: araxis bc kdiff3 meld opendiff vscode ...
Configurarlo
Para Visual Studio Code, que es lo que usa Ana:
git config --global diff.tool vscode
git config --global difftool.vscode.cmd 'code --wait --diff "$LOCAL" "$REMOTE"'Para Meld (multiplataforma, gratuito y muy claro):
Para el opendiff de macOS, que le viene bien a Bruno:
Y un ajuste que casi todo el mundo acaba poniendo:
Sin él, Git pregunta antes de abrir cada fichero, lo cual es agotador cuando el cambio afecta a diez.
Las variables $LOCAL y $REMOTE que aparecen en la configuración son ficheros temporales que Git crea con cada versión y pasa a la herramienta. Todo esto se guarda en el ~/.gitconfig con el mecanismo que vimos en Configurando Git:
[diff]
tool = vscode
[difftool]
prompt = false
[difftool "vscode"]
cmd = code --wait --diff "$LOCAL" "$REMOTE"¿Terminal o herramienta visual?
git diff en el terminal |
git difftool |
|
|---|---|---|
| Velocidad | Instantáneo | Abre una ventana por fichero |
| Cambios pequeños | Perfecto | Excesivo |
| Refactorizaciones grandes | Difícil de seguir | Mucho más claro |
| Uso en scripts | Sí | No |
| Funciona por SSH sin escritorio | Sí | No |
La recomendación práctica: usa git diff como herramienta por defecto —es más rápido y siempre está— y reserva difftool para los cambios grandes.
Mención aparte: existe también
git mergetool, el equivalente para resolver conflictos de fusión. Se configura de forma análoga y lo veremos en Resolviendo Conflictos de Fusión.
- El hábito de revisar antes de confirmar
Todo lo anterior se condensa en una rutina de treinta segundos que conviene automatizar mentalmente:
git status -s # ¿qué ficheros están en juego?
git diff # ¿qué me queda sin preparar?
git add -p # preparar con criterio
git diff --staged # ¿es EXACTAMENTE esto lo que quiero confirmar?
git commit -m "..."El paso realmente valioso es el penúltimo. Lo que detecta habitualmente:
- Restos de depuración:
console.log,print, puntos de interrupción. - Credenciales o URLs de pruebas.
- Bloques de código comentado que ibas a borrar.
- Cambios accidentales por autoformato del editor en ficheros que no tocabas.
- Ficheros que se colaron en un
git add -Aapresurado. - Cambios que creías haber hecho y no estaban.
Si prefieres no ejecutar un comando aparte, la opción -v de git commit incluye el diff en la plantilla del mensaje:
Y para dejarlo activado siempre:
Es probablemente el ajuste con mejor relación entre esfuerzo y beneficio de todo Git.
Errores Comunes y Consejos
- Ejecutar
git diffdespués degit addy creer que no hay cambios. La salida vacía significa "el disco coincide con el área de preparación". Lo que buscas esgit diff --staged. - Esperar ver ficheros nuevos en
git diff. Un fichero sin seguimiento no tiene versión anterior, así que no aparece. Para eso estágit status. Si quieres verlo, prepáralo primero (git add -N ficherolo registra como vacío y hace que sus contenidos salgan en el diff). - Leer al revés la cabecera
@@. El primer número es de la versión antigua y el segundo de la nueva. Confundirlos lleva a interpretar mal el sentido del cambio. - Pensar que Git guarda diffs. No los guarda: guarda instantáneas y calcula el diff cuando se lo pides, como vimos en el modelo de datos. Por eso puedes comparar dos confirmaciones cualesquiera, por lejanas que estén.
- Abusar de
-w. En lenguajes sensibles a la indentación, ignorar espacios puede ocultarte el error que estás buscando. Es una herramienta de lectura, no de revisión final. - No usar
--antes de una ruta ambigua. Si tienes un fichero llamado igual que una rama,git diff mainno hará lo que crees. - Revisar un cambio de 40 ficheros con el diff completo. Empieza por
--statpara ver el mapa, y baja al detalle solo donde importe. - Consejo: define alias para lo que más uses:
git config --global alias.d diffygit config --global alias.ds "diff --staged". - Consejo: si la salida de
git diffse te queda "atrapada" en el paginador, recuerda que se recorre con las flechas o la barra espaciadora y se sale conq. Para desactivarlo puntualmente,git --no-pager diff. - Consejo:
git diff --statjusto después degit add -Aes la forma más rápida de detectar que has añadido algo que no querías.
Ejercicios
Ejercicio 1: Las tres zonas, tres diffs distintos
Monta este escenario y responde razonadamente antes de ejecutar cada comando:
mkdir -p ~/practica/diffs && cd ~/practica/diffs
git init
cat > app.js <<'FIN'
const tareas = [];
function anadirTarea(texto) {
tareas.push(texto);
}
FIN
git add app.js && git commit -m "Versión inicial"
# Cambio 1: se prepara
sed -i 's/tareas.push(texto);/tareas.push({ texto: texto, hecha: false });/' app.js
git add app.js
# Cambio 2: NO se prepara
echo "" >> app.js
echo "function contarTareas() { return tareas.length; }" >> app.js- ¿Qué mostrará
git status -s? - ¿Qué mostrará
git diff? ¿Cuántas líneas+tendrá? - ¿Qué mostrará
git diff --staged? - ¿Qué mostrará
git diff HEAD? - Si confirmas ahora sin más
git add, ¿qué contendrá el fichero en el historial? - Comprueba tus respuestas ejecutándolo todo.
Ejercicio 2: Leer un diff sin ejecutarlo
Interpreta esta salida y responde a las preguntas:
diff --git a/estilos.css b/estilos.css
index a1b2c3d..d4e5f6a 100644
--- a/estilos.css
+++ b/estilos.css
@@ -8,10 +8,8 @@ body {
h1 {
font-size: 1.8rem;
- color: #333;
- margin-bottom: 1rem;
+ color: #1f2937;
}
-.oculto { display: none; }
#contador {
color: #6b7280;
}
diff --git a/config.js b/config.js
new file mode 100644
index 0000000..7a8b9c0
--- /dev/null
+++ b/config.js
@@ -0,0 +1,4 @@
+const API_URL = 'https://api.ejemplo.es';
+const TIEMPO_ESPERA = 5000;
+const CLAVE_DEBUG = 'test-1234';
+export { API_URL, TIEMPO_ESPERA, CLAVE_DEBUG };- ¿Cuántos ficheros afecta este cambio y qué le pasa a cada uno?
- En
estilos.css, ¿cuántas líneas se han eliminado y cuántas se han añadido? ¿Cómo cambia el número total de líneas del bloque? - ¿Qué significa exactamente
--- /dev/nullen el segundo fichero? - ¿Qué significa
index 0000000..7a8b9c0? - ¿Hay algo en este diff que no deberías confirmar? Explica por qué y qué harías.
- Escribe el comando que mostraría solo el resumen por ficheros de este cambio.
Ejercicio 3: Encontrar el cambio real entre el ruido
Simula la situación de un fichero reindentado a la vez que se cambia una línea de lógica:
mkdir -p ~/practica/ruido && cd ~/practica/ruido
git init
cat > app.js <<'FIN'
function calcularTotal(tareas) {
let total = 0;
for (const tarea of tareas) {
if (!tarea.hecha) {
total = total + 1;
}
}
return total;
}
FIN
git add app.js && git commit -m "Versión inicial"
# Reindentar de 4 a 2 espacios Y cambiar la condición
cat > app.js <<'FIN'
function calcularTotal(tareas) {
let total = 0;
for (const tarea of tareas) {
if (!tarea.hecha && !tarea.archivada) {
total = total + 1;
}
}
return total;
}
FIN- Ejecuta
git diffy cuenta cuántas líneas aparecen como cambiadas. - Usa la opción adecuada para ver solo el cambio de lógica.
- Explica por qué el resultado de los dos comandos es tan distinto.
- ¿Qué habría sido mejor hacer desde el principio para que el historial fuera legible? Escribe la secuencia de comandos correcta.
- Usa
--word-diffsobre el mismo cambio y comenta si aporta algo en este caso.
Soluciones
Solución al Ejercicio 1
1. git status -s:
Hay una versión preparada (cambio 1) y el disco difiere de ella (cambio 2).
2. git diff — lo no preparado. Muestra únicamente el cambio 2:
@@ -3,3 +3,5 @@ const tareas = [];
function anadirTarea(texto) {
tareas.push({ texto: texto, hecha: false });
}
+
+function contarTareas() { return tareas.length; }Dos líneas +: la línea en blanco y la función. La línea tareas.push({...}) aparece como contexto, no como añadida, porque en el área de preparación ya está así.
3. git diff --staged — lo que va a entrar. Solo el cambio 1:
@@ -1,5 +1,5 @@
const tareas = [];
function anadirTarea(texto) {
- tareas.push(texto);
+ tareas.push({ texto: texto, hecha: false });
}4. git diff HEAD — el total, ambos cambios juntos:
@@ -1,5 +1,7 @@
const tareas = [];
function anadirTarea(texto) {
- tareas.push(texto);
+ tareas.push({ texto: texto, hecha: false });
}
+
+function contarTareas() { return tareas.length; }5. Al confirmar sin más git add entraría solo el cambio 1. El fichero en el historial tendría el push con el objeto, pero no la función contarTareas, que seguiría pendiente en el directorio de trabajo.
Comprobación:
Solución al Ejercicio 2
1. Dos ficheros:
estilos.css: modificado (existe el par--- a/+++ b/con el mismo nombre y modo100644).config.js: fichero nuevo, como indicannew file mode 100644y--- /dev/null.
2. En estilos.css: 3 líneas eliminadas (color: #333;, margin-bottom: 1rem; y .oculto { display: none; }) y 1 añadida (color: #1f2937;).
La cuenta encaja con la cabecera @@ -8,10 +8,8 @@: el bloque pasa de 10 a 8 líneas, es decir, pierde 2 (−3 +1 = −2). Y se puede verificar contando a mano las líneas del fragmento:
- Versión antigua = líneas de contexto + líneas
-→ 7 de contexto + 3 eliminadas = 10. - Versión nueva = líneas de contexto + líneas
+→ 7 de contexto + 1 añadida = 8.
Este ejercicio de comprobación es la mejor forma de asegurarte de que has entendido la cabecera @@: los dos números siempre deben cuadrar con lo que ves en el cuerpo.
3. --- /dev/null indica que la versión antigua del fichero no existe: es un fichero nuevo. /dev/null es el "dispositivo nulo" de los sistemas Unix, y aquí funciona como "la nada". El caso simétrico, +++ /dev/null, indicaría un borrado.
4. index 0000000..7a8b9c0 son los hashes de los blobs antiguo y nuevo. El de la izquierda es todo ceros porque no hay blob antiguo: coherente con que el fichero sea nuevo.
5. Sí, hay un problema: CLAVE_DEBUG = 'test-1234' en config.js. Aunque sea una clave de pruebas, es un secreto en el código, y confirmarlo lo deja en el historial de forma permanente para cualquiera que clone el repositorio. Aunque sea inofensiva hoy, establece un mal precedente y es exactamente el mecanismo por el que acaban filtrándose claves reales.
Qué haría:
# Sacar la constante del fichero y llevarla a una variable de entorno
# o a un fichero de configuración local ignorado.
# Después, quitar config.js del área de preparación:
git restore --staged config.js
# Editarlo, y volver a preparar solo la parte legítima:
git add -p config.jsSi config.js no debiera versionarse en absoluto:
6. El resumen por ficheros:
Solución al Ejercicio 3
1. El diff completo:
@@ -1,9 +1,9 @@
function calcularTotal(tareas) {
- let total = 0;
- for (const tarea of tareas) {
- if (!tarea.hecha) {
- total = total + 1;
- }
- }
- return total;
+ let total = 0;
+ for (const tarea of tareas) {
+ if (!tarea.hecha && !tarea.archivada) {
+ total = total + 1;
+ }
+ }
+ return total;
}Catorce líneas aparecen como cambiadas (7 eliminadas y 7 añadidas), cuando el cambio real de comportamiento es uno solo.
2. Ver solo la lógica:
@@ -1,7 +1,7 @@
function calcularTotal(tareas) {
let total = 0;
for (const tarea of tareas) {
- if (!tarea.hecha) {
+ if (!tarea.hecha && !tarea.archivada) {
total = total + 1;
}
}Ahora solo hay un par -/+: la condición. La indentación de las demás líneas se ignora, así que el cambio real aparece aislado.
3. Por qué la diferencia es tan grande. Git compara líneas completas, carácter a carácter. Cambiar cuatro espacios por dos al principio de una línea la convierte, a ojos de Git, en una línea distinta: la antigua se elimina y la nueva se añade. Como el reformateo afecta a las siete líneas del cuerpo de la función, las siete aparecen cambiadas y el cambio de lógica se pierde entre ellas. -w le dice a Git que normalice los espacios antes de comparar, con lo que solo queda la diferencia real.
4. Lo correcto desde el principio: dos confirmaciones separadas. Mezclar reformateo con cambios funcionales es una mala práctica bien conocida, porque hace imposible revisar el cambio y arruina el git blame de todo el bloque.
# Confirmación 1: SOLO el reformateo
# (reindentar el fichero, sin tocar la lógica)
git add app.js
git commit -m "Reindentar calcularTotal a 2 espacios"
# Confirmación 2: SOLO el cambio de comportamiento
# (añadir la condición !tarea.archivada)
git add app.js
git commit -m "Excluir las tareas archivadas del total"El diff de la segunda confirmación es de dos líneas y se revisa en cinco segundos. Además, si mañana hay que deshacer el cambio de lógica, se deshace sin arrastrar el reformateo.
Si los dos cambios ya están mezclados en el disco, git add -p con la opción e permite separarlos, aunque en este caso concreto —donde cada línea contiene ambos cambios a la vez— sería más práctico rehacer el trabajo en dos pasos.
5. Con --word-diff:
@@ -1,9 +1,9 @@
function calcularTotal(tareas) {
let total = 0;
for (const tarea of tareas) {
if (!tarea.hecha[- -]{+ && !tarea.archivada +}) {Aporta bastante: al comparar por palabras en lugar de por líneas, la reindentación deja de generar ruido casi por completo y el cambio de la condición queda señalado con precisión quirúrgica. En cambios de texto y en casos como este, --word-diff y -w se complementan bien:
Conclusión
Ya sabes leer los cambios antes de que entren en la historia del proyecto. Recapitulando:
git diffcompara siempre dos zonas, y la forma que uses decide cuáles:git diff(disco ↔ preparación),git diff --staged(preparación ↔HEAD) ygit diff HEAD(disco ↔HEAD, es decir, el total).- Una salida vacía de
git diffno significa "no he cambiado nada": casi siempre significa que ya lo has preparado todo. - Ningún
git diffmuestra ficheros sin seguimiento. Para eso estágit status. - El formato unified diff tiene una estructura fija: cabecera
diff --git, hashes de blob, marcadores---/+++, cabeceras de fragmento@@ -a,b +c,d @@con su contexto, y líneas de contexto, eliminadas (-) y añadidas (+). Git no representa "líneas modificadas": una modificación es una eliminación más una adición. - Se pueden comparar dos confirmaciones cualesquiera (
git diff <sha1> <sha2>) y acotar por ruta con-- <ruta>, usando--siempre que el nombre pueda ser ambiguo. - Las opciones cambian la legibilidad radicalmente:
--statpara el mapa general,--name-only/--name-statuspara la lista,--word-diffpara textos y-wpara separar el cambio real del ruido de espaciado. git difftooldelega en una herramienta visual y acepta los mismos argumentos; compensa en cambios grandes.- El hábito que importa:
git diff --stagedantes de cadagit commit, o directamentecommit.verbose = true.
Con git status sabes dónde estás, con git diff sabes qué has cambiado y con git add/git commit decides qué queda registrado. Falta la última pieza del ciclo básico: mirar hacia atrás.
Un historial solo vale lo que vale la capacidad de consultarlo. Después de doscientas confirmaciones, ¿cómo encuentras cuándo se introdujo una función? ¿Quién tocó estilos.css la semana pasada? ¿En qué confirmación desapareció aquella línea que juras haber escrito?
En la siguiente lección, Visualizando el Historial de Confirmaciones, veremos git log con todos sus formatos y filtros, los formatos personalizados con --pretty, la búsqueda "pickaxe" que encuentra cuándo apareció o desapareció un texto concreto, git show para inspeccionar una confirmación, y las distintas formas de referirse a un commit —HEAD, HEAD~3, HEAD^— que ya hemos ido usando de pasada y que conviene entender bien.
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
