Esta es la lección más importante del módulo y, probablemente, del primer tercio del curso. Todo lo que Git hace —confirmar, ramificar, fusionar, reescribir el historial— es una consecuencia directa de cómo guarda la información. Quien entiende el modelo de datos deja de memorizar comandos y empieza a deducirlos: sabe qué es posible, qué es peligroso y por qué un comando tiene el efecto que tiene.

La idea central es sorprendentemente simple: Git es un almacén de contenido direccionable por hash, es decir, una tabla clave-valor donde la clave es la huella criptográfica del contenido y el valor es el contenido mismo. Sobre esa base mínima se construyen cuatro tipos de objeto y un puñado de referencias, y con eso está todo. En esta lección abriremos la caja: veremos los objetos por dentro con comandos de bajo nivel, exploraremos la carpeta .git y entenderemos por qué el historial es inmutable y qué significa realmente "reescribirlo".

Contenido

  1. Almacén de contenido direccionable por hash
  2. Los cuatro tipos de objeto
  3. Cómo se relacionan: el grafo de commits
  4. SHA-1 y la transición a SHA-256
  5. Qué hay dentro de .git/
  6. Exploración práctica con comandos de fontanería
  7. Inmutabilidad y reescritura del historial
  8. Por qué este modelo lo explica todo

  1. Almacén de contenido direccionable por hash

La idea en una frase

Git guarda objetos en una base de datos donde la clave de cada objeto es el hash SHA de su propio contenido.

Un hash es el resultado de una función criptográfica que convierte cualquier cantidad de datos en una cadena de longitud fija. Tiene tres propiedades que Git aprovecha:

  1. Determinismo. El mismo contenido produce siempre el mismo hash.
  2. Dispersión. Cambiar un solo bit produce un hash completamente distinto.
  3. Resistencia a colisiones. Es inviable encontrar dos contenidos distintos con el mismo hash.

De la primera propiedad se derivan dos consecuencias enormes:

  • Deduplicación automática. Si estilos.css no cambia entre veinte confirmaciones, su contenido se guarda una sola vez. Las veinte confirmaciones apuntan al mismo objeto.
  • Verificación de integridad. Para comprobar que un objeto no se ha corrompido basta con recalcular su hash y compararlo con su clave. Si no coinciden, hay corrupción.

De la segunda se deriva otra: cualquier alteración del contenido cambia el identificador, así que no se puede modificar nada en silencio.

Instantáneas, no diferencias

Muchos sistemas de control de versiones guardan, para cada versión, la lista de diferencias respecto a la anterior. Reconstruir un fichero antiguo implica aplicar la cadena de diferencias desde el origen.

Git funciona al revés: cada confirmación registra una instantánea completa del estado del proyecto. Conceptualmente, es como si guardara una foto de todos los ficheros en ese momento.

graph TD
    subgraph "Modelo de diferencias (SVN, CVS)"
        D1["v1: fichero completo"] --> D2["v2: +3 líneas, -1 línea"]
        D2 --> D3["v3: +8 líneas"]
    end
    subgraph "Modelo de instantáneas (Git)"
        S1["c1: foto completa"] --> S2["c2: foto completa"]
        S2 --> S3["c3: foto completa"]
    end

La objeción obvia es el espacio: ¿no ocupa muchísimo guardar el proyecto entero cada vez? No, por dos motivos:

  1. Reutilización por hash. Los ficheros que no cambian no se guardan de nuevo; la instantánea simplemente los referencia. Si en una confirmación Ana solo toca estilos.css, solo se crea contenido nuevo para ese fichero.
  2. Empaquetado. Periódicamente, Git comprime los objetos sueltos en packfiles que sí usan compresión con diferencias entre objetos parecidos. Pero eso es una optimización de almacenamiento invisible al modelo: conceptualmente siguen siendo instantáneas.

  1. Los cuatro tipos de objeto

La base de datos de Git contiene exactamente cuatro tipos de objeto. Todos se identifican por su hash y todos son inmutables.

Tipo Representa Contiene
blob El contenido de un fichero Los bytes del fichero, y nada más
tree Un directorio Lista de nombres con su modo, tipo y hash
commit Una confirmación Hash del árbol raíz, padres, autor, fecha y mensaje
tag anotado Una etiqueta con metadatos Objeto apuntado, nombre, etiquetador, fecha, mensaje y firma opcional

El blob

Un blob (binary large object) guarda el contenido de un fichero. Nada más: ni el nombre, ni los permisos, ni la ruta, ni la fecha. Solo los bytes.

Esto tiene una consecuencia elegante: si Ana copia estilos.css a estilos-backup.css sin cambiar nada, Git no guarda contenido nuevo. Hay un blob y dos entradas de directorio que lo referencian. El nombre del fichero vive en el árbol, no en el blob.

El tree

Un tree representa un directorio. Es una lista de entradas, cada una con cuatro datos: modo (permisos), tipo (blob o tree), hash y nombre.

Así se vería el árbol raíz de gestor-tareas en su primera confirmación:

100644 blob a3f5c9e2b1d4...    README.md
100644 blob 7b2e8f1a5c3d...    app.js
100644 blob 2c9d4e6f8a1b...    estilos.css
100644 blob e1f3a7b9c2d5...    index.html

Los modos que verás en la práctica son pocos:

Modo Significado
100644 Fichero normal
100755 Fichero ejecutable
120000 Enlace simbólico
040000 Subdirectorio (otro tree)

Si el proyecto tuviera una carpeta img/, el árbol raíz incluiría una entrada de tipo tree apuntando al árbol de esa carpeta. Los árboles se anidan y forman la estructura de directorios completa.

El commit

Un commit es el objeto que da sentido al historial. Contiene:

  • El hash del árbol raíz, es decir, la instantánea completa del proyecto.
  • El hash de su padre o padres: cero si es la primera confirmación, uno en el caso normal, dos o más si es una fusión.
  • El autor (quien escribió el cambio) con su fecha.
  • El confirmador (quien lo registró) con su fecha. Normalmente coinciden; se separan en escenarios como el rebase o la aplicación de parches ajenos.
  • El mensaje.

Un commit real, mostrado en crudo, tiene este aspecto:

tree 9f4c2a8e1b7d3f5a6c9e2b4d8f1a3c5e7b9d2f4a
parent 4d8f1a3c5e7b9d2f4a6c8e1b3d5f7a9c2e4b6d8f
author Ana Ferrer <[email protected]> 1751328000 +0200
committer Ana Ferrer <[email protected]> 1751328000 +0200

Añadir borrado de tareas al listado

Fíjate en lo que no hay: ni un solo cambio, ni una diferencia, ni una lista de ficheros modificados. Un commit no guarda "qué cambió", guarda "cómo quedó todo". Cuando Git te muestra las diferencias de una confirmación, las calcula en ese momento comparando su árbol con el de su padre. Este detalle explica por qué git show es una operación de cómputo y no una simple lectura.

El tag anotado

Un tag anotado es un objeto que apunta a otro objeto (casi siempre un commit) y añade metadatos: el nombre de la etiqueta, quién la creó, cuándo, un mensaje y opcionalmente una firma GPG.

Es el único de los cuatro tipos que tiene una alternativa ligera: una etiqueta ligera no crea objeto, es solo un fichero en .git/refs/tags/ con un hash dentro. Por eso, para versiones publicadas se recomiendan las anotadas: dejan rastro auditable. El módulo 5 lo desarrolla.

  1. Cómo se relacionan: el grafo de commits

Los cuatro tipos de objeto se enlazan por hash y forman un grafo dirigido acíclico (DAG). Este diagrama muestra dos confirmaciones consecutivas de gestor-tareas, con la segunda modificando solo app.js:

graph RL
    C2["commit c2<br/>'Añadir borrado de tareas'"] --> C1["commit c1<br/>'Versión inicial'"]
    C2 --> T2["tree raíz (c2)"]
    C1 --> T1["tree raíz (c1)"]

    T2 --> B_HTML["blob index.html"]
    T2 --> B_CSS["blob estilos.css"]
    T2 --> B_JS2["blob app.js (v2)"]
    T2 --> B_MD["blob README.md"]

    T1 --> B_HTML
    T1 --> B_CSS
    T1 --> B_JS1["blob app.js (v1)"]
    T1 --> B_MD

Observa tres cosas fundamentales en el diagrama:

  1. Los blobs de index.html, estilos.css y README.md se comparten entre las dos confirmaciones. No se ha duplicado nada. Solo app.js tiene dos blobs distintos porque su contenido cambió.
  2. Las flechas apuntan hacia atrás. Un commit conoce a su padre, pero un padre no conoce a sus hijos. Por eso Git recorre el historial del presente hacia el pasado, y por eso git log empieza por lo más reciente.
  3. Se crearon dos árboles raíz distintos aunque tres de los cuatro ficheros sean idénticos: el árbol contiene el hash de app.js, que cambió, así que el contenido del árbol cambió y su hash también.

Sobre este grafo se apoyan las referencias que vimos en la lección Terminología Básica de Git:

graph RL
    HEAD["HEAD<br/>(fichero .git/HEAD)"] -.apunta a.-> MAIN["refs/heads/main"]
    MAIN -.apunta a.-> C2["commit c2"]
    TAG["refs/tags/v1.0"] -.apunta a.-> C1["commit c1"]
    C2 --> C1

Una rama no es más que un nombre que apunta a un nodo del grafo. Confirmar significa crear un nodo nuevo y mover ese nombre. Nada más. Por eso las ramas son baratas, como adelantamos en la lección ¿Qué es Git?.

  1. SHA-1 y la transición a SHA-256

SHA-1: el identificador clásico

Git usa históricamente SHA-1, que produce hashes de 160 bits representados como 40 caracteres hexadecimales:

a3f5c9e2b1d47f8e0c6a2b9d3e5f7a1c4b6d8e0f

En la práctica se usan formas abreviadas de 7 a 12 caracteres (a3f5c9e), suficientes para identificar un objeto sin ambigüedad en repositorios normales. Git alarga automáticamente la abreviatura si detecta riesgo de ambigüedad.

Un detalle importante: el hash no se calcula sobre el contenido puro, sino sobre una cabecera concatenada con el contenido. Para un blob, el dato que se pasa por SHA-1 es literalmente:

blob <longitud en bytes>\0<contenido>

Ese \0 es un byte nulo separador. Gracias a la cabecera, un blob y un tree con los mismos bytes producen hashes distintos.

La colisión de SHA-1 y la respuesta de Git

En 2017, el ataque SHAttered demostró una colisión práctica de SHA-1: dos ficheros PDF distintos con el mismo hash. Esto no rompió Git de inmediato —explotarlo contra un repositorio requiere condiciones muy específicas y las referencias cruzadas del grafo lo dificultan mucho—, pero encendió las alarmas.

La respuesta tuvo dos fases:

  1. Detección de colisiones. Desde la versión 2.13, Git incorpora una implementación endurecida de SHA-1 (SHA-1 collision detection) que reconoce los patrones del ataque conocido y aborta si los encuentra. Es una mitigación, no una solución.
  2. Migración a SHA-256. Git soporta desde la versión 2.29 repositorios con hashes SHA-256 de 64 caracteres hexadecimales. Se elige al crear el repositorio y no se puede cambiar después.
Aspecto SHA-1 SHA-256
Longitud del hash 40 caracteres hex (160 bits) 64 caracteres hex (256 bits)
Estado en Git Por defecto Experimental pero funcional
Resistencia a colisiones Comprometida en 2017 Sin ataques prácticos conocidos
Interoperabilidad Universal Limitada: no puede comunicarse con repositorios SHA-1
Soporte en plataformas Total Parcial o inexistente en 2026

La transición es lenta precisamente por la interoperabilidad: un repositorio SHA-256 no puede sincronizarse con uno SHA-1, y todo el ecosistema —GitHub, GitLab, herramientas de CI— tendría que soportarlo simultáneamente. Existe un diseño de repositorios con doble hash para permitir la convivencia, pero aún no está completo.

Para este curso y para tu trabajo diario: usa SHA-1, que es lo que Git hace por defecto. Lo relevante es que conozcas el porqué de esta transición y que entiendas que el modelo de datos no depende del algoritmo concreto.

  1. Qué hay dentro de .git/

Un repositorio recién creado tiene esta estructura. Vamos a recorrerla:

.git/
├── HEAD                 # dónde estoy: ref: refs/heads/main
├── config               # configuración local del repositorio
├── description          # descripción, solo la usa GitWeb
├── index                # el área de preparación (binario)
├── hooks/               # scripts automáticos (plantillas de ejemplo)
├── info/
│   └── exclude          # ficheros ignorados solo en local
├── logs/                # el reflog: historial de movimientos de las refs
├── objects/             # LA BASE DE DATOS: todos los objetos
│   ├── a3/
│   │   └── f5c9e2b1d4...
│   ├── info/
│   └── pack/            # objetos empaquetados y comprimidos
└── refs/
    ├── heads/           # ramas locales: un fichero por rama
    ├── tags/            # etiquetas
    └── remotes/         # ramas de seguimiento de los remotos

Los elementos que importan de verdad:

Elemento Qué es Consecuencia práctica
objects/ La base de datos de objetos Es el repositorio de verdad; si se pierde, se pierde el historial
refs/heads/ Un fichero por rama, con un hash dentro Comprueba con tus propios ojos que una rama son 41 bytes
HEAD Fichero de texto con ref: refs/heads/main Cambiar de rama es reescribir esta línea
index El área de preparación Explica por qué preparar es una operación separada
config Configuración de nivel local Lo veremos en la lección 01-05
logs/ El reflog Es la red de seguridad para recuperar trabajo perdido (módulo 9)
hooks/ Scripts que Git ejecuta automáticamente Módulo 6

Cómo se guardan los objetos sueltos

Fíjate en objects/a3/f5c9e2b1d4...: los dos primeros caracteres del hash son el nombre del directorio y los treinta y ocho restantes, el del fichero. Es una optimización sencilla: evita tener cientos de miles de ficheros en un solo directorio, algo que degrada gravemente el rendimiento de muchos sistemas de ficheros.

Cada fichero de objeto está comprimido con zlib, así que abrirlo con un editor de texto solo muestra ruido binario. Para leerlos hay que usar los comandos de Git, que es justo lo que haremos ahora.

  1. Exploración práctica con comandos de fontanería

Git distingue dos capas de comandos:

  • Porcelana (porcelain): la interfaz de usuario habitual (git status, git commit, git log). Su salida está pensada para humanos y puede cambiar entre versiones.
  • Fontanería (plumbing): comandos de bajo nivel que operan directamente sobre la base de datos (git hash-object, git cat-file, git rev-parse). Su salida es estable y está pensada para scripts.

En el día a día usarás la porcelana. Aquí vamos a usar la fontanería precisamente porque muestra el modelo sin adornos.

git hash-object: calcular el hash de un contenido

echo "Gestor de tareas" | git hash-object --stdin

Salida:

2b1a3c5e7d9f0b2a4c6e8d0f2a4b6c8e0d2f4a6b

Desglose:

  • echo "Gestor de tareas" envía ese texto por la salida estándar.
  • | lo canaliza hacia el siguiente comando.
  • git hash-object --stdin lee de la entrada estándar y calcula el hash que tendría ese contenido como blob. No guarda nada: solo calcula.

Comprueba tú mismo el determinismo: ejecuta el comando dos veces, y después cámbiale una sola letra al texto.

echo "Gestor de tareas" | git hash-object --stdin
echo "Gestor de tareaz" | git hash-object --stdin

Los dos hashes serán completamente distintos, no parecidos. Esa es la propiedad de dispersión en acción.

Para guardar el objeto en la base de datos se añade -w (write):

echo "Gestor de tareas" | git hash-object -w --stdin

Ahora el objeto existe en .git/objects/. Este comando requiere estar dentro de un repositorio.

git cat-file: leer un objeto

Es el comando inverso: dado un hash, muestra qué hay dentro.

# Tipo del objeto: blob, tree, commit o tag
git cat-file -t 2b1a3c5

# Contenido del objeto, en formato legible
git cat-file -p 2b1a3c5

# Tamaño en bytes
git cat-file -s 2b1a3c5

Las tres opciones:

Opción Significa Devuelve
-t type blob, tree, commit o tag
-p pretty-print El contenido formateado según el tipo
-s size Tamaño en bytes

Aplicado a un commit, -p produce exactamente el bloque tree/parent/author/committer/mensaje que vimos en el apartado 2. Aplicado a un árbol, produce la lista de entradas. Aplicado a un blob, el contenido del fichero.

git rev-parse: resolver nombres a hashes

Traduce cualquier forma de referirse a un objeto en su hash completo.

# ¿A qué commit apunta HEAD?
git rev-parse HEAD

# ¿A qué commit apunta la rama main?
git rev-parse main

# ¿Y el padre del commit actual?
git rev-parse HEAD~1

# Expandir un hash abreviado a su forma completa
git rev-parse a3f5c9e

La sintaxis de referencia que conviene reconocer desde ya:

Expresión Significa
HEAD La confirmación actual
HEAD~1, HEAD~2 Uno, dos pasos hacia atrás por la primera línea de padres
HEAD^ El primer padre (equivale a HEAD~1)
HEAD^2 El segundo padre; solo existe en confirmaciones de fusión
main La confirmación a la que apunta la rama main
v1.0 La confirmación a la que apunta la etiqueta v1.0

Un comando muy útil para orientarse:

# Ruta absoluta del directorio .git
git rev-parse --git-dir

# ¿Estoy dentro de un repositorio Git?
git rev-parse --is-inside-work-tree

Un recorrido completo del grafo

Reuniendo los tres comandos, así se navega el modelo desde arriba hacia abajo. Los hashes de tu repositorio serán distintos: sustitúyelos por los que devuelva cada comando.

# 1. Qué commit es el actual
git rev-parse HEAD
# → c2a8f1e4b6d9...

# 2. Ver el commit por dentro
git cat-file -p c2a8f1e
# → tree 9f4c2a8...
#   parent 4d8f1a3...
#   author Ana Ferrer <[email protected]> 1751328000 +0200
#   committer Ana Ferrer <[email protected]> 1751328000 +0200
#
#   Añadir borrado de tareas al listado

# 3. Ver el árbol raíz que ese commit referencia
git cat-file -p 9f4c2a8
# → 100644 blob e1f3a7b...    index.html
#   100644 blob 2c9d4e6...    estilos.css
#   100644 blob 7b2e8f1...    app.js
#   100644 blob a3f5c9e...    README.md

# 4. Ver el contenido de app.js en ese commit
git cat-file -p 7b2e8f1
# → el código JavaScript completo

Has recorrido a mano el mismo camino que Git recorre internamente cada vez que ejecutas cualquier comando: commit → tree → blob. Si este recorrido te resulta claro, tienes el modelo mental que sostiene el resto del curso.

Existe un atajo cómodo que combina referencia y ruta:

# El blob de app.js en el commit actual, sin buscar hashes a mano
git cat-file -p HEAD:app.js

La sintaxis <referencia>:<ruta> resuelve el árbol y baja hasta el fichero en un solo paso.

  1. Inmutabilidad y reescritura del historial

Por qué el historial es inmutable

Ya tenemos todas las piezas para entender la propiedad más importante de Git.

El hash de un commit se calcula sobre su contenido, y ese contenido incluye el hash de su padre. Por tanto:

graph RL
    C3["c3<br/>hash depende de c2"] --> C2["c2<br/>hash depende de c1"]
    C2 --> C1["c1<br/>hash depende de su árbol"]

Si alguien modificase el mensaje de c1, su hash cambiaría. Entonces c2 seguiría apuntando a un hash que ya no existe, así que habría que cambiar c2, lo que cambiaría su hash, lo que obligaría a cambiar c3, y así hasta el final del historial.

De aquí salen tres garantías:

  1. No se puede alterar el pasado sin que se note. Cualquier cambio invalida toda la cadena posterior.
  2. El hash de una confirmación identifica todo el historial que lleva hasta ella. Si Ana y Bruno tienen el mismo hash en la punta de main, tienen exactamente el mismo historial hasta ese punto, byte a byte. Es una verificación completa en una comparación de cuarenta caracteres.
  3. Los objetos nunca se modifican, solo se crean. No existe ninguna operación en Git que edite un objeto existente.

Esta es la razón de la "integridad" que citábamos entre las virtudes de Git en la lección 01-01. No es una promesa: es una propiedad matemática del diseño.

Qué significa entonces "reescribir el historial"

Si nada se puede modificar, ¿cómo es posible corregir el mensaje de una confirmación o reordenar el historial, cosas que Git claramente permite?

La respuesta es que no se modifica nada: se crean objetos nuevos y se mueven las referencias.

Cuando dices "voy a corregir el mensaje del último commit", lo que ocurre es:

  1. Git crea un commit nuevo, con el mismo árbol y el mismo padre, pero con el mensaje corregido. Tiene un hash distinto.
  2. Git mueve la rama main para que apunte al nuevo commit.
  3. El commit antiguo sigue existiendo en la base de datos. Simplemente ya no hay ninguna referencia que llegue hasta él.
graph RL
    subgraph "Antes"
        A_MAIN["main"] -.-> A_C2["c2 'Aade borado'"]
        A_C2 --> A_C1["c1"]
    end
    subgraph "Después"
        B_MAIN["main"] -.-> B_C2N["c2' 'Añadir borrado'"]
        B_C2N --> B_C1["c1"]
        B_C2["c2 (huérfano)"] --> B_C1
    end

Los objetos sin referencias se llaman huérfanos (unreachable). Permanecen en la base de datos y siguen siendo accesibles por su hash o a través del reflog —el registro de todos los movimientos de las referencias, que vive en .git/logs/— hasta que la recolección de basura (git gc) los elimina, normalmente pasadas un par de semanas.

Esto tiene dos implicaciones capitales:

  • Casi nada se pierde de verdad. Si crees haber destruido trabajo confirmado, es muy probable que siga ahí. El módulo 9 dedica una lección a recuperarlo.
  • Reescribir el historial compartido es peligroso. Si Ana reescribe confirmaciones que Bruno ya tiene en su repositorio, los hashes de Bruno dejan de coincidir con los de Ana: sus historiales divergen y reconciliarlos es doloroso. De ahí la regla de oro que repetiremos en el módulo 5:

No reescribas historial que ya has compartido con otros.

Las operaciones que reescriben el historial —rebase, commit --amend, reset sobre confirmaciones publicadas, filter-repo— se tratan en los módulos 5, 8 y 9. Lo que necesitas ahora es entender por qué son distintas del resto: crean objetos nuevos y abandonan los antiguos, en lugar de añadir al final.

  1. Por qué este modelo lo explica todo

Cerramos con la utilidad práctica de lo aprendido. Estas preguntas frecuentes se responden solas cuando se conoce el modelo:

Pregunta habitual Respuesta desde el modelo
¿Por qué crear una rama es instantáneo? Es escribir 41 bytes en refs/heads/; no se copia nada
¿Por qué cambiar de rama es tan rápido? Se reescribe HEAD y se ajusta el directorio de trabajo al árbol correspondiente
¿Por qué no puedo confirmar un directorio vacío? Los árboles solo contienen entradas; sin blobs no hay nada que registrar
¿Por qué Git no guarda los permisos completos de un fichero? El árbol solo distingue ejecutable, no ejecutable y enlace simbólico
¿Por qué renombrar un fichero no ocupa espacio? El blob se reutiliza; solo cambia la entrada del árbol
¿Por qué el repositorio crece con imágenes y vídeos? Cada versión de un binario genera un blob nuevo que no comprime bien
¿Por qué al hacer rebase cambian todos los hashes? Se crean confirmaciones nuevas con padres distintos
¿Por qué puedo recuperar una rama borrada? Los commits siguen en objects/; el reflog conserva su hash
¿Por qué dos personas con el mismo hash tienen el mismo código? El hash cubre el árbol completo y toda la cadena de padres

Cada vez que en los módulos siguientes un comportamiento de Git te resulte extraño, vuelve a este modelo y pregúntate: ¿qué objetos se están creando y qué referencias se están moviendo? La respuesta casi siempre aparece sola.

Errores Comunes y Consejos

  • Creer que Git guarda diferencias. Es el error conceptual más extendido y viene de la experiencia con otros sistemas. Git guarda instantáneas y calcula las diferencias cuando se las pides. Las diferencias que ves en git log -p o en una revisión de código no están almacenadas en ninguna parte.
  • Pensar que el blob guarda el nombre del fichero. No lo guarda. El nombre vive en el árbol. Por eso Git no registra renombramientos explícitamente: los detecta comparando contenidos entre dos árboles.
  • Toquetear .git/ a mano. Explorarla con ls y leer HEAD o refs/heads/main es instructivo y seguro. Editar o borrar ficheros dentro es una forma eficaz de corromper el repositorio. Para todo lo demás existen comandos.
  • Confiar en que los objetos huérfanos duran para siempre. Sobreviven a la reescritura, pero git gc los elimina pasado un tiempo (por defecto, 30 días para los inalcanzables recientes y 90 para el reflog). Si necesitas recuperar algo, hazlo pronto.
  • Asumir que un hash abreviado es único para siempre. Siete caracteres bastan hoy en tu repositorio; en un repositorio con cientos de miles de confirmaciones pueden volverse ambiguos. Al guardar un hash en documentación, usa la forma completa.
  • Consejo: haz este experimento una vez. En cualquier repositorio, ejecuta el recorrido git cat-file -p HEAD → árbol → blob del apartado 6. Diez minutos de exploración manual ahorran meses de confusión.
  • Consejo: cuando algo salga mal, piensa en referencias. La inmensa mayoría de los sustos de Git no son pérdidas de datos, sino referencias que apuntan a un sitio inesperado. Los objetos casi siempre siguen ahí.

Ejercicios

Ejercicio 1: Predecir hashes y objetos

Ana crea un repositorio con index.html, estilos.css, app.js y README.md, y hace una primera confirmación. Después modifica solo estilos.css y hace una segunda confirmación. Finalmente, copia estilos.css a estilos-antiguo.css sin modificar el contenido y hace una tercera confirmación.

Responde:

  1. ¿Cuántos objetos blob distintos hay en la base de datos al final? Enuméralos.
  2. ¿Cuántos objetos tree se han creado en total?
  3. ¿Cuántos objetos commit hay?
  4. En la tercera confirmación, ¿se ha creado algún blob nuevo? ¿Por qué?

Ejercicio 2: Exploración práctica

Sobre cualquier repositorio Git que tengas a mano (si no tienes ninguno todavía, guarda este ejercicio para después del módulo 2), ejecuta la exploración completa y anota los resultados:

  1. Obtén el hash de la confirmación actual.
  2. Muestra ese commit en crudo e identifica el hash de su árbol y el de su padre.
  3. Muestra el árbol e identifica el hash de uno de sus ficheros.
  4. Muestra el contenido de ese fichero desde la base de datos de objetos.
  5. Comprueba que el tipo del árbol es tree y el del fichero, blob.
  6. Averigua el tamaño en bytes de ese blob.

Ejercicio 3: Razonar sobre la reescritura

Ana ha hecho tres confirmaciones en main: c1, c2 y c3. Ha enviado las tres al repositorio compartido y Bruno ya las tiene en su equipo. Ahora Ana se da cuenta de que el mensaje de c2 tiene una falta de ortografía y lo corrige reescribiendo el historial.

Responde:

  1. ¿Qué objetos nuevos se crean y cuáles quedan huérfanos?
  2. ¿Cambia el hash de c1? ¿Y el del árbol de c2? Justifícalo.
  3. ¿Qué verá Bruno cuando intente sincronizarse?
  4. ¿Habría sido distinto si Ana no hubiera enviado nada todavía?

Soluciones

Solución al Ejercicio 1

  1. Cinco blobs. Uno por cada contenido distinto que ha existido:

    • index.html (nunca cambia): 1 blob
    • app.js (nunca cambia): 1 blob
    • README.md (nunca cambia): 1 blob
    • estilos.css versión original: 1 blob
    • estilos.css versión modificada: 1 blob

    Total: 5. El contenido es la clave, no el fichero.

  2. Tres árboles. Uno por confirmación. El árbol raíz cambia en las tres porque en la segunda cambia el hash de estilos.css y en la tercera se añade una entrada nueva (estilos-antiguo.css). Como el proyecto no tiene subdirectorios, solo hay árbol raíz.

  3. Tres commits, uno por confirmación.

  4. No se ha creado ningún blob nuevo en la tercera confirmación. El contenido de estilos-antiguo.css es idéntico al de estilos.css, y Git guarda el contenido indexado por su hash: ya existe. Lo único nuevo es una entrada en el árbol con otro nombre apuntando al mismo blob. Este es el ejemplo canónico de la deduplicación por contenido.

Solución al Ejercicio 2

# 1. Hash de la confirmación actual
git rev-parse HEAD
# → 8c4f2a1e9b7d3f5a6c8e0b2d4f6a8c0e2b4d6f8a

# 2. El commit en crudo
git cat-file -p HEAD
# → tree 3f7a9c1e5b8d2f4a6c9e1b3d5f7a9c1e3b5d7f9a
#   parent 1e5b8d2f4a6c9e1b3d5f7a9c1e3b5d7f9a1c3e5b
#   author ...
#   committer ...
#
#   Mensaje de la confirmación

# 3. El árbol
git cat-file -p 3f7a9c1
# → 100644 blob 9c1e3b5...    README.md
#   100644 blob 5f7a9c1...    app.js
#   ...

# 4. Contenido del fichero
git cat-file -p 5f7a9c1
# → (el código fuente)

# Atajo equivalente sin buscar hashes:
git cat-file -p HEAD:app.js

# 5. Tipos
git cat-file -t 3f7a9c1     # → tree
git cat-file -t 5f7a9c1     # → blob

# 6. Tamaño
git cat-file -s 5f7a9c1     # → 412 (por ejemplo)

Solución al Ejercicio 3

  1. Se crea un commit nuevo, llamémoslo c2', con el mismo árbol y el mismo padre (c1) pero distinto mensaje, y por tanto distinto hash. Como c3 apuntaba a c2, hay que crear también c3' apuntando a c2'. Quedan huérfanos los commits c2 y c3 originales. Los árboles y los blobs no cambian: se reutilizan íntegros porque el contenido de los ficheros no se ha tocado.

  2. c1 no cambia: su hash depende de su propio contenido y de su padre, nada de lo cual se ha modificado. El árbol de c2 tampoco cambia: el mensaje forma parte del objeto commit, no del árbol. Es un buen recordatorio de que los cuatro tipos de objeto son independientes entre sí y solo se enlazan por hash.

  3. Bruno verá historiales divergentes. Su main local apunta a c3, mientras que la rama compartida apunta a c3'. Git le dirá que las ramas han divergido y que tienen confirmaciones distintas por cada lado. Si intenta integrar sin cuidado, acabará con el trabajo duplicado: c2, c3, c2' y c3' en el mismo historial. Resolverlo requiere que Bruno alinee su rama con la reescrita, algo que se trata en el módulo 9.

  4. Sí, habría sido completamente distinto. Si las confirmaciones solo existieran en el equipo de Ana, reescribirlas no tendría ninguna consecuencia: nadie más tiene los hashes antiguos, así que no hay divergencia posible. Esta es exactamente la frontera de la regla de oro: reescribir historial local es seguro y recomendable; reescribir historial compartido es peligroso.

Conclusión

Git es, en el fondo, una base de datos clave-valor donde la clave es el hash del contenido. Sobre esa idea mínima se levantan cuatro tipos de objeto: el blob guarda el contenido de un fichero, el tree representa un directorio y da nombre a los blobs, el commit apunta a un árbol raíz y a sus padres añadiendo autor, fecha y mensaje, y el tag anotado marca un punto del historial con metadatos. Enlazados por hash, forman un grafo dirigido acíclico sobre el que las ramas y HEAD no son más que nombres que apuntan a nodos.

De ese diseño se derivan directamente las propiedades que hacen a Git lo que es: deduplicación (el contenido idéntico se guarda una sola vez), integridad verificable (el hash delata cualquier corrupción) e inmutabilidad (cambiar el pasado invalida toda la cadena posterior). Y de la inmutabilidad se deriva el significado exacto de "reescribir el historial": no se modifica nada, se crean objetos nuevos y se abandonan los antiguos, que sobreviven huérfanos hasta la recolección de basura. Esa es la base de la regla de oro —no reescribas lo que ya has compartido— y también de la buena noticia de que casi nada se pierde de verdad.

Has visto además la distinción entre comandos de porcelana y de fontanería, y has recorrido el grafo a mano con git rev-parse, git cat-file y git hash-object.

Con el modelo mental asentado, volvemos a la superficie. Antes de que Ana pueda crear el repositorio de gestor-tareas necesita ajustar cómo se comporta Git en su equipo. En Configurando Git veremos el mecanismo de configuración: los tres niveles de git config, dónde vive cada fichero, qué valor gana cuando hay conflicto y cómo mantener perfiles separados para el trabajo y los proyectos personales.

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