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
- Almacén de contenido direccionable por hash
- Los cuatro tipos de objeto
- Cómo se relacionan: el grafo de commits
- SHA-1 y la transición a SHA-256
- Qué hay dentro de
.git/ - Exploración práctica con comandos de fontanería
- Inmutabilidad y reescritura del historial
- Por qué este modelo lo explica todo
- 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:
- Determinismo. El mismo contenido produce siempre el mismo hash.
- Dispersión. Cambiar un solo bit produce un hash completamente distinto.
- 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.cssno 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:
- 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. - 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.
- 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.
- 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:
- Los blobs de
index.html,estilos.cssyREADME.mdse comparten entre las dos confirmaciones. No se ha duplicado nada. Soloapp.jstiene dos blobs distintos porque su contenido cambió. - 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 logempieza por lo más reciente. - 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?.
- 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:
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:
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:
- 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.
- 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.
- Qué hay dentro de
.git/
.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 remotosLos 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.
- 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
Salida:
Desglose:
echo "Gestor de tareas"envía ese texto por la salida estándar.|lo canaliza hacia el siguiente comando.git hash-object --stdinlee 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.
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):
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 2b1a3c5Las 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 a3f5c9eLa 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-treeUn 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 completoHas 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:
La sintaxis <referencia>:<ruta> resuelve el árbol y baja hasta el fichero en un solo paso.
- 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:
- No se puede alterar el pasado sin que se note. Cualquier cambio invalida toda la cadena posterior.
- 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. - 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:
- Git crea un commit nuevo, con el mismo árbol y el mismo padre, pero con el mensaje corregido. Tiene un hash distinto.
- Git mueve la rama
mainpara que apunte al nuevo commit. - 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.
- 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 -po 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 conlsy leerHEADorefs/heads/maines 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 gclos 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:
- ¿Cuántos objetos blob distintos hay en la base de datos al final? Enuméralos.
- ¿Cuántos objetos tree se han creado en total?
- ¿Cuántos objetos commit hay?
- 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:
- Obtén el hash de la confirmación actual.
- Muestra ese commit en crudo e identifica el hash de su árbol y el de su padre.
- Muestra el árbol e identifica el hash de uno de sus ficheros.
- Muestra el contenido de ese fichero desde la base de datos de objetos.
- Comprueba que el tipo del árbol es
treey el del fichero,blob. - 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:
- ¿Qué objetos nuevos se crean y cuáles quedan huérfanos?
- ¿Cambia el hash de
c1? ¿Y el del árbol dec2? Justifícalo. - ¿Qué verá Bruno cuando intente sincronizarse?
- ¿Habría sido distinto si Ana no hubiera enviado nada todavía?
Soluciones
Solución al Ejercicio 1
-
Cinco blobs. Uno por cada contenido distinto que ha existido:
index.html(nunca cambia): 1 blobapp.js(nunca cambia): 1 blobREADME.md(nunca cambia): 1 blobestilos.cssversión original: 1 blobestilos.cssversión modificada: 1 blob
Total: 5. El contenido es la clave, no el fichero.
-
Tres árboles. Uno por confirmación. El árbol raíz cambia en las tres porque en la segunda cambia el hash de
estilos.cssy en la tercera se añade una entrada nueva (estilos-antiguo.css). Como el proyecto no tiene subdirectorios, solo hay árbol raíz. -
Tres commits, uno por confirmación.
-
No se ha creado ningún blob nuevo en la tercera confirmación. El contenido de
estilos-antiguo.csses idéntico al deestilos.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
-
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. Comoc3apuntaba ac2, hay que crear tambiénc3'apuntando ac2'. Quedan huérfanos los commitsc2yc3originales. Los árboles y los blobs no cambian: se reutilizan íntegros porque el contenido de los ficheros no se ha tocado. -
c1no cambia: su hash depende de su propio contenido y de su padre, nada de lo cual se ha modificado. El árbol dec2tampoco 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. -
Bruno verá historiales divergentes. Su
mainlocal apunta ac3, mientras que la rama compartida apunta ac3'. 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'yc3'en el mismo historial. Resolverlo requiere que Bruno alinee su rama con la reescrita, algo que se trata en el módulo 9. -
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
- ¿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
