Internamente, un repositorio de Git es una base de datos de objetos guardada en .git/objects. Cada objeto se identifica por el hash SHA-1 (o SHA-256 en repositorios más nuevos) de su contenido. Hay cuatro tipos de objetos, y entender qué es cada uno es la clave para entender todo lo demás.
Un blob guarda el contenido de un archivo, sin ningún metadato: ni nombre, ni permisos, ni fecha. Dos archivos con el mismo contenido exacto —aunque estén en carpetas distintas o tengan nombres distintos— comparten el mismo blob. Esto es lo que hace que Git sea tan eficiente con el espacio: el contenido nunca se duplica.
Un tree representa un directorio. Es una lista de entradas, y cada entrada asocia un nombre de archivo (o subdirectorio) con:
Un tree es, en definitiva, una fotografía de la estructura de carpetas y archivos en un momento dado.
Un commit apunta a un único tree —la raíz del proyecto en ese momento— y añade:
Un commit normal tiene un solo padre. Un commit de fusión (merge commit) tiene dos o más. Esta cadena de padres es lo que forma el historial del proyecto: no una lista, sino un grafo dirigido acíclico (DAG), como se explica en Referencias y el grafo de commits.
Un punto importante: un commit no guarda una diferencia
(diff) respecto al commit anterior. Guarda una fotografía
completa del proyecto entero en ese momento, en forma de árbol de
trees y blobs. Git calcula las diferencias al vuelo, comparando
dos fotografías, cuando hace falta mostrarlas (por ejemplo con
git diff o git show). Esto explica por qué operaciones
como cambiar de rama son rápidas: Git no tiene que "deshacer"
diferencias acumuladas, solo cambia qué fotografía está activa.
Un tag anotado es un objeto que apunta a un commit específico
(normalmente) y añade un mensaje, un autor y, opcionalmente, una
firma criptográfica. Se usa típicamente para marcar versiones
publicadas, como v2.1.0. Existen también los tags
ligeros, que no son objetos independientes sino simples punteros
directos a un commit, sin metadatos propios.
commit ---> tree (raíz)
|
+-- archivo.txt ---> blob
+-- src/ ---> tree
|
+-- main.c ---> blob
Cada commit apunta a exactamente un tree raíz, que a su vez puede contener otros trees (subdirectorios) y blobs (archivos), formando una jerarquía igual a la del sistema de archivos en ese momento.
Todo lo que hace Git a partir de aquí —ramas, HEAD, el índice,
reset, rebase— consiste en manipular a qué
commit apunta una referencia, o en construir nuevos commits a
partir de combinaciones de trees existentes. No hay magia adicional:
son estructuras de datos relativamente simples combinadas de formas
poderosas.