Estructura del Repositorio Git: Una Guía Técnica Completa
Git es un sistema de control de versiones distribuido que almacena el historial del proyecto como un grafo acíclico dirigido (DAG) de objetos de instantánea inmutables. Cada repositorio Git se construye a partir de tres zonas lógicas — el directorio de trabajo, el índice de preparación y el almacén de objetos dentro de .git/ — más un conjunto de punteros ligeros (ramas, etiquetas, remotos) que navegan por ese historial. Comprender cómo interactúan estas capas es la diferencia entre usar Git mecánicamente y usarlo con precisión quirúrgica.
Si alojas tus repositorios en un VPS, dominar esta estructura interna te permite recuperarte de desastres, diseñar pipelines de CI/CD eficientes y auditar cada byte del historial de tu proyecto sin depender de una plataforma de terceros.
El Modelo de Tres Zonas: Cómo Git Mueve los Datos
Antes de profundizar en los componentes individuales, interioriza el modelo de flujo de datos que gobierna cada operación de Git:
Working Directory --> Staging Area (Index) --> .git/ Object Store
(edit) (git add) (git commit)Los cambios viajan de izquierda a derecha cuando construyes un commit, y de derecha a izquierda cuando restauras o restableces. Cada comando de Git es esencialmente una operación de lectura o escritura en una o más de estas zonas.
Directorio de Trabajo
El directorio de trabajo (también llamado árbol de trabajo) es la vista del sistema de archivos de tu proyecto en un estado de checkout específico. Cuando ejecutas git clone o git checkout, Git reconstruye los archivos a partir de objetos comprimidos en .git/objects/ y los escribe en este directorio.
Los archivos en el directorio de trabajo existen en uno de cuatro estados:
- Sin seguimiento — Git nunca ha visto este archivo; solo existe en el disco.
- Con seguimiento, sin modificar — el archivo coincide exactamente con la última instantánea confirmada.
- Con seguimiento, modificado — el archivo difiere de la última instantánea confirmada pero no ha sido preparado.
- Con seguimiento, eliminado — el archivo fue eliminado del disco pero la eliminación no ha sido preparada.
Un matiz crítico que confunde a muchos desarrolladores: el directorio de trabajo no es una simple copia del repositorio. Git lo reconstruye leyendo objetos de árbol y descomprimiendo objetos blob. Si .git/ está intacto, siempre puedes regenerar el directorio de trabajo desde cero — lo inverso no es cierto.
Sparse Checkout para Grandes Monorepos
En repositorios con decenas de miles de archivos (común en arquitecturas de monorepo), puedes limitar qué rutas Git materializa en el directorio de trabajo:
git sparse-checkout init --cone
git sparse-checkout set services/api services/authEsto es invaluable en un VPS con I/O de disco limitado, porque Git omite la descompresión de blobs para rutas fuera del cono.
Área de Preparación (Índice)
El área de preparación, llamada internamente el índice, es un archivo binario ubicado en .git/index. Actúa como el próximo commit propuesto — una instantánea mutable que se sitúa entre tu directorio de trabajo y el almacén de objetos permanente.
git add <file> # Stage a specific file
git add -p # Interactively stage hunks within a file
git add -u # Stage all tracked modifications and deletions
git status # Compare working directory and index against HEAD
git diff --cached # Show diff between index and HEADPor Qué Existe el Índice
El índice resuelve un problema que las herramientas VCS más simples ignoran: los commits parciales. Puedes haber modificado cinco archivos pero solo querer tres de ellos en el próximo commit. El índice te permite componer exactamente la instantánea que pretendes registrar, independientemente de lo que tu editor tenga abierto.
Caso extremo — corrupción del índice: Si un fallo del sistema interrumpe un git add, el archivo de índice puede corromperse. Los síntomas incluyen que git status se cuelgue o reporte resultados extraños. Recuperación:
rm .git/index
git resetGit reconstruye el índice desde HEAD sin tocar tu directorio de trabajo.
El Índice como Registro de Conflictos de Fusión
Durante un conflicto de fusión, el índice almacena tres versiones de cada archivo en conflicto simultáneamente (etapas 1, 2 y 3 — base, la nuestra, la de ellos). Por eso git diff --cached no muestra nada útil en medio de un conflicto; necesitas git diff --cc o una herramienta de fusión para inspeccionar las tres etapas.
El Directorio .git/: Anatomía del Almacén de Objetos
El directorio .git/ es el repositorio. Todo lo demás — el directorio de trabajo, los clones remotos — se deriva de él. Eliminar .git/ convierte un repositorio en un directorio simple sin historial.
.git/
├── HEAD
├── config
├── description
├── index
├── COMMIT_EDITMSG
├── hooks/
├── info/
├── logs/
│ ├── HEAD
│ └── refs/
├── objects/
│ ├── info/
│ └── pack/
└── refs/
├── heads/
├── remotes/
└── tags/HEAD
HEAD es un archivo de texto plano que contiene una referencia simbólica (apuntando a una rama) o un hash SHA-1 sin procesar (estado HEAD desconectado).
cat .git/HEAD
# ref: refs/heads/main <-- on a branch
# a3f1c9d... <-- detached HEADEl HEAD desconectado no es un estado de error — es intencional cuando haces checkout de una etiqueta o un commit específico para inspección. El peligro es hacer commits en HEAD desconectado: esos commits solo son accesibles a través del reflog hasta que los adjuntes a una rama.
git checkout -b rescue-branch # Attach detached commits to a new branchconfig
El archivo de configuración del repositorio local. Anula la configuración global (~/.gitconfig) y del sistema (/etc/gitconfig). Entradas comunes:
[core]
repositoryformatversion = 0
filemode = true
bare = false
[remote "origin"]
url = git@github.com:user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/mainEn un servidor autoalojado, frecuentemente editarás este archivo directamente cuando rotas URLs remotas o configuras uploadpack.allowReachableSHA1InWant para clones parciales.
refs/
El directorio refs/ contiene archivos de texto plano, cada uno con un único hash SHA-1. Son los punteros con nombre que hacen navegable el DAG de Git.
| Tipo de Ref | Ruta | Descripción |
|---|---|---|
| Rama local | refs/heads/<name> | Apunta al commit en la punta de una rama |
| Rama de seguimiento remoto | refs/remotes/<remote>/<name> | Caché local de la punta de una rama remota |
| Etiqueta ligera | refs/tags/<name> | Apunta directamente a un objeto commit |
| Etiqueta anotada | refs/tags/<name> | Apunta a un objeto etiqueta, que apunta a un commit |
| Stash | refs/stash | Apunta al commit del stash |
Por rendimiento, Git empaqueta las refs en .git/packed-refs una vez que un repositorio acumula muchas. Siempre verifica ambas ubicaciones cuando escribas scripts que trabajen con refs.
Objetos Git: El Núcleo Inmutable
Todo lo almacenado en .git/objects/ está direccionado por contenido: el nombre del archivo es el hash SHA-1 (o SHA-256 en versiones más nuevas de Git) del contenido del objeto. Esto hace que Git sea inherentemente resistente a manipulaciones — cambiar cualquier byte cambia el hash, rompiendo la cadena.
Los Cuatro Tipos de Objetos
| Tipo de Objeto | Qué Almacena | Apunta A |
|---|---|---|
| Blob | Contenido de archivo sin procesar (sin nombre de archivo, sin permisos) | Nada |
| Tree | Listado de directorio: nombres de archivo, permisos, SHAs de blob/tree | Blobs y otros trees |
| Commit | Autor, committer, marca de tiempo, mensaje, SHA(s) padre | Un tree + cero o más commits padre |
| Tag | Identidad del tagger, marca de tiempo, mensaje, firma GPG | Generalmente un commit |
Inspeccionando Objetos Directamente
# Show the type of any object
git cat-file -t a3f1c9d
# Show the content of any object
git cat-file -p a3f1c9d
# Show the tree of the current HEAD commit
git ls-tree HEAD
# Show a specific blob's content
git show HEAD:src/main.pyObjetos Sueltos vs. Archivos Pack
Inicialmente, cada objeto se almacena como un archivo comprimido individual bajo .git/objects/<2-char-prefix>/<38-char-suffix>. Estos son objetos sueltos. Con el tiempo, Git ejecuta git gc (recolección de basura) para agrupar objetos sueltos en archivos pack (.git/objects/pack/*.pack) con un índice correspondiente (.pack.idx).
Los archivos pack usan compresión delta — almacenando la diferencia entre objetos similares en lugar de copias completas. Un repositorio con miles de archivos de texto similares puede reducirse drásticamente después del empaquetado. En un VPS con capacidad NVMe limitada, ejecutar git gc --aggressive en repositorios grandes antes de archivarlos es una práctica estándar.
git count-objects -vH # Show loose object count and disk usage
git gc --aggressive # Repack aggressively (CPU-intensive)
git verify-pack -v .git/objects/pack/*.idx | sort -k3 -n | tail -20
# Find the 20 largest objects in the packHistorial de Commits: El Grafo Acíclico Dirigido
Cada objeto commit contiene exactamente un puntero a un objeto tree (la instantánea del directorio raíz) y cero o más punteros a commits padre. Esto forma un DAG donde:
- Cero padres = el commit inicial (commit raíz)
- Un padre = un commit normal
- Dos padres = un commit de fusión
- Tres o más padres = una fusión octopus (rara, usada para integrar muchas ramas de características simultáneamente)
git log --oneline --graph --all # Visualize the full DAG
git log --format="%H %P" # Show each commit's SHA and parent SHA(s)Inmutabilidad de Commits y Reescritura del Historial
Dado que el SHA de un commit se deriva de su contenido (incluyendo los SHAs padre), cualquier reescritura crea un nuevo commit con un nuevo SHA. Operaciones como git rebase, git commit --amend y git filter-repo no modifican el historial — crean un historial paralelo. Los commits antiguos permanecen en el almacén de objetos hasta que son recolectados por el recolector de basura.
Por eso hacer force-push de historial reescrito a una rama compartida es destructivo: las ramas locales de los colaboradores aún apuntan a la cadena de commits antigua.
Ramas: Punteros Ligeros
Una rama no es más que un archivo de 41 bytes que contiene un hash SHA-1. Crear una rama es instantáneo independientemente del tamaño del repositorio porque Git solo escribe un archivo pequeño.
git branch feature/auth # Create branch at current HEAD
git checkout -b feature/auth # Create and switch in one step
git switch -c feature/auth # Modern equivalent (Git 2.23+)
git branch -d feature/auth # Delete (safe: refuses if unmerged)
git branch -D feature/auth # Delete (force: regardless of merge status)Internos de las Ramas
cat .git/refs/heads/main
# a3f1c9d8e2b1f4c7d9e0a1b2c3d4e5f6a7b8c9d0Cuando haces un commit en una rama, Git escribe el nuevo SHA del commit en este archivo. Eso es todo lo que significa “avanzar un puntero de rama”.
Ramas de Seguimiento y Configuración Upstream
Una relación de seguimiento le dice a Git con qué rama remota debe comparar una rama local para el reporte de divergencia git status y el comportamiento de git pull.
git branch --set-upstream-to=origin/main main
git branch -vv # Show tracking relationships and ahead/behind countsEtiquetas: Marcadores Permanentes en el Historial
Las etiquetas marcan commits específicos como significativos — típicamente versiones de software. A diferencia de las ramas, las etiquetas no se mueven con los nuevos commits.
| Característica | Etiqueta Ligera | Etiqueta Anotada |
|---|---|---|
| Almacenamiento | Un archivo ref apuntando a un commit | Un objeto etiqueta en el almacén de objetos |
| Metadatos | Ninguno | Nombre del tagger, email, fecha, mensaje |
| Firma GPG | No es posible | Compatible mediante git tag -s |
| Recomendado para versiones | No | Sí |
Transferencia con git push --tags | Sí | Sí |
git tag v2.1.0 # Lightweight tag at HEAD
git tag -a v2.1.0 -m "Release 2.1.0" # Annotated tag
git tag -s v2.1.0 -m "Signed release" # GPG-signed annotated tag
git push origin --tags # Push all tags to remote
git push origin v2.1.0 # Push a specific tagError crítico: git push no envía etiquetas por defecto. Los equipos frecuentemente olvidan esto y publican notas de versión que hacen referencia a una etiqueta que no existe en el remoto.
Remotos: Colaboración Distribuida
Un remoto es una URL con nombre almacenada en .git/config. Las ramas de seguimiento remoto (bajo refs/remotes/) son instantáneas locales de solo lectura de las ramas del remoto, actualizadas solo cuando haces fetch explícitamente.
git remote add origin git@github.com:user/repo.git
git remote -v # List remotes with URLs
git remote set-url origin <new-url> # Change a remote URL
git fetch origin # Update remote-tracking branches
git fetch --prune # Remove stale remote-tracking branches
git push origin main # Push local main to remote
git push -u origin feature/auth # Push and set upstream trackingMúltiples Remotos
Un único repositorio puede rastrear múltiples remotos — común cuando se mantiene un fork junto al upstream:
git remote add upstream git@github.com:original/repo.git
git fetch upstream
git merge upstream/mainCuando alojas repositorios bare en un servidor dedicado para tu equipo, cada desarrollador agrega el servidor como remoto y usa autenticación por clave SSH para acceso de push.
Hooks: Automatización en Cada Evento de Git
Los hooks son scripts ejecutables en .git/hooks/. Git los llama en puntos definidos del flujo de trabajo. No se transfieren mediante git clone o git push — cada desarrollador (o servidor) debe instalarlos de forma independiente. Esta es una fuente frecuente de confusión en entornos de equipo.
Hooks del Lado del Cliente
| Hook | Disparador | Uso Común |
|---|---|---|
pre-commit | Antes del prompt del mensaje de commit | Linting, escaneo de secretos, ejecución de pruebas |
prepare-commit-msg | Después de crear el mensaje predeterminado | Inyectar nombre de rama en el mensaje |
commit-msg | Después de que el usuario escribe el mensaje | Aplicar formato de commit convencional |
post-commit | Después de registrar el commit | Notificaciones locales |
pre-push | Antes de que se ejecute git push | Ejecutar suite completa de pruebas |
pre-rebase | Antes de que comience el rebase | Prevenir rebase de ramas publicadas |
Hooks del Lado del Servidor
| Hook | Disparador | Uso Común |
|---|---|---|
pre-receive | Antes de que se actualicen las refs | Aplicar protección de ramas, rechazar force-push |
update | Por ref durante la recepción | Aplicación de políticas por rama |
post-receive | Después de actualizar todas las refs | Disparar CI/CD, enviar notificaciones |
Ejemplo: Hook Pre-commit para Detección de Secretos
#!/usr/bin/env bash
# .git/hooks/pre-commit
if git diff --cached --name-only | xargs grep -lE '(AKIA|passwords*=|api_keys*=)' 2>/dev/null; then
echo "ERROR: Potential secret detected in staged files. Commit aborted."
exit 1
fi
exit 0Hazlo ejecutable:
chmod +x .git/hooks/pre-commitPara la distribución de hooks en todo el equipo, usa una herramienta como Husky (proyectos Node.js) o almacena los hooks en un directorio hooks/ en la raíz del repositorio y crea enlaces simbólicos durante la configuración del proyecto.
Reflog: La Red de Seguridad
El reflog registra cada movimiento de HEAD y los punteros de rama, incluyendo operaciones que parecen destruir el historial (resets duros, rebases, commits enmendados). Se almacena en .git/logs/.
git reflog # Show HEAD movement history
git reflog show main # Show movement history for a specific branch
git checkout HEAD@{3} # Check out the state HEAD was in 3 moves ago
git branch recovered HEAD@{5} # Recover commits by branching from a reflog entryLas entradas del reflog expiran después de 90 días por defecto (gc.reflogExpire). En un servidor de producción, considera extender esto:
git config gc.reflogExpire 180
git config gc.reflogExpireUnreachable 30Repositorios Bare: Alojamiento del Lado del Servidor
Un repositorio bare no tiene directorio de trabajo. Contiene solo el contenido de .git/ en el nivel raíz. Los repositorios bare son el formato correcto para el alojamiento centralizado — aceptan pushes sin las complicaciones de una rama con checkout.
git init --bare /srv/repos/myproject.gitCuando haces push a GitHub, GitLab o un servidor Git autoalojado, estás haciendo push a un repositorio bare. Si alojas tu propio servidor Git en un VPS con cPanel o un VPS Linux sin configuración adicional, los repositorios bare bajo /srv/repos/ con acceso SSH son la arquitectura estándar.
Inicializando un Repositorio Bare Compartido
# On the server
git init --bare --shared=group /srv/repos/project.git
chown -R git:developers /srv/repos/project.git
# On a developer's machine
git remote add origin git@yourserver.com:/srv/repos/project.git
git push -u origin mainAlmacenamiento de Objetos Git: Tamaño, Integridad y Mantenimiento
Verificando la Salud del Repositorio
git fsck --full # Verify object integrity (finds dangling and corrupt objects)
git fsck --lost-found # Write dangling objects to .git/lost-found/Encontrando y Eliminando Objetos Grandes
Los archivos binarios grandes confirmados accidentalmente son una causa común de repositorios inflados. Identifícalos antes de usar git filter-repo para eliminarlos:
# Find the 10 largest objects by compressed size
git verify-pack -v .git/objects/pack/*.idx
| sort -k3 -rn
| head -10
| awk '{print $1}'
| xargs -I{} git cat-file -p {}# Remove a file from all history (requires git-filter-repo)
git filter-repo --path path/to/large-file.bin --invert-pathsDespués del filtrado, todos los colaboradores deben volver a clonar — sus repositorios locales hacen referencia a hashes SHA que ya no existen en el historial reescrito.
Comparación: Conceptos Clave del Repositorio Git
| Concepto | Tipo | Mutable | Almacenado En | Transferido por Push/Fetch |
|---|---|---|---|---|
| Blob | Objeto | No | .git/objects/ | Sí (cuando es accesible) |
| Tree | Objeto | No | .git/objects/ | Sí (cuando es accesible) |
| Commit | Objeto | No | .git/objects/ | Sí (cuando es accesible) |
| Etiqueta Anotada | Objeto | No | .git/objects/ | Solo con --tags |
| Rama | Ref | Sí | .git/refs/heads/ | Sí |
| Rama de seguimiento remoto | Ref | Sí (en fetch) | .git/refs/remotes/ | No (caché local) |
| Etiqueta Ligera | Ref | No | .git/refs/tags/ | Solo con --tags |
| HEAD | Symref/hash | Sí | .git/HEAD | No |
| Índice | Archivo binario | Sí | .git/index | No |
| Hooks | Scripts | Sí | .git/hooks/ | No |
| Reflog | Log | Sí (expira automáticamente) | .git/logs/ | No |
Matriz de Decisión Práctica y Conclusiones Clave
Usa esta lista de verificación al configurar o auditar un repositorio Git en tu infraestructura:
Inicialización del repositorio
- Usa
git init --bare --shared=grouppara cualquier repositorio que recibirá pushes de múltiples usuarios. - Almacena repositorios bare fuera de directorios accesibles desde la web (nunca bajo
/var/www/).
Salud del almacén de objetos
- Ejecuta
git fsck --fulldespués de cualquier incidente de almacenamiento o error del sistema de archivos. - Programa
git gcperiódicamente en repositorios de larga duración; automatízalo mediante cron en tu servidor. - Monitorea el tamaño de los archivos pack con
git count-objects -vH; investiga si el recuento de objetos sueltos supera 1.000.
Higiene de ramas y refs
- Elimina las ramas fusionadas rápidamente; las refs obsoletas se acumulan y ralentizan las operaciones de
git fetch --prune. - Usa
git fetch --pruneen pipelines de CI para evitar actuar sobre ramas remotas eliminadas.
Despliegue de hooks
- Nunca dependas de
.git/hooks/para políticas de todo el equipo — los hooks no se clonan. Usa hookspre-receivedel lado del servidor o una puerta de CI en su lugar. - Audita los hooks del lado del servidor después de cada actualización del servidor Git; las rutas del intérprete de hooks pueden cambiar.
Seguridad en servidores autoalojados
- Restringe el acceso SSH al usuario
gitcon comandos forzados (command=enauthorized_keys). - Usa
git-shellcomo shell de inicio de sesión para el usuariogitpara prevenir la ejecución de comandos arbitrarios. - Combina tu servidor de repositorios con un certificado SSL válido si expones alguna interfaz web (Gitea, GitLab, cgit).
Reescritura del historial
- Nunca reescribas el historial en ramas compartidas con otros sin un plan de migración coordinado.
- Después de
git filter-repo, todos los colaboradores deben volver a clonar; actualiza las URLs remotas de CI/CD inmediatamente.
Recuperación ante desastres
- Extiende la expiración del reflog en servidores de producción (
gc.reflogExpire = 180). - Mantén un clon bare secundario en un host separado como respaldo; un simple
git fetchdesde el primario es suficiente.
Preguntas Frecuentes
¿Cuál es la diferencia entre un repositorio Git bare y uno no bare?
Un repositorio no bare tiene un directorio de trabajo donde se hace checkout de los archivos, más un subdirectorio .git/ que contiene el almacén de objetos. Un repositorio bare contiene solo el almacén de objetos en su raíz (sin directorio de trabajo) y es el formato correcto para un servidor compartido que recibe pushes.
¿Puedo recuperar commits después de ejecutar git reset --hard?
Sí, siempre que los commits no hayan sido recolectados por el recolector de basura. Ejecuta git reflog para encontrar el SHA del commit que deseas recuperar, luego git checkout -b recovery-branch <SHA> para adjuntarlo a una nueva rama. Las entradas del reflog se retienen durante 90 días por defecto.
¿Por qué git push no transfiere mis etiquetas?
Por diseño, git push solo transfiere commits accesibles desde las refs que explícitamente envías. Las etiquetas son refs separadas y deben enviarse con git push origin --tags (todas las etiquetas) o git push origin <tagname> (una etiqueta específica).
¿Qué sucede con el índice durante un conflicto de fusión?
El índice almacena las tres versiones de cada archivo en conflicto simultáneamente: etapa 1 (ancestro común/base), etapa 2 (tu versión) y etapa 3 (su versión). El git add normal solo escribe la etapa 0 (resuelta). Hasta que todos los conflictos estén resueltos y preparados, git commit se negará a continuar.
¿Cómo difieren los hooks de Git entre despliegues del lado del cliente y del servidor?
Los hooks del lado del cliente se ejecutan en la máquina del desarrollador y no se aplican de forma centralizada — cualquier desarrollador puede omitirlos eliminando el archivo del hook. Los hooks del lado del servidor (pre-receive, update, post-receive) se ejecutan en el servidor de alojamiento y no pueden ser omitidos por el cliente, lo que los convierte en el punto de aplicación correcto para políticas de protección de ramas, requisitos de revisión de código y disparadores de CI/CD.
en todos los servicios de hosting