El Viaje de un Commit Git: Del staging al repositorio remoto
Modificas un archivo, ejecutas git add, luego git commit, y finalmente git push. Tu código ahora está en GitHub. Pero, ¿qué pasó realmente? Git no es magia: es un sistema de archivos direccionado por contenido con un modelo de datos elegante. Veamos el viaje.
Capa 1: El Archivo Modificado
Empiezas editando un archivo:
echo "console.log('Hola Mundo');" > app.js
Git detecta el cambio en tu Working Directory (directorio de trabajo):
$ git status
Changes not staged for commit:
modified: app.js
En este punto, Git sabe que algo cambió, pero no ha guardado nada. El archivo solo existe en tu sistema de archivos.
┌─────────────────────────────────────────────────────────┐
│ WORKING DIRECTORY │
│ │
│ app.js (modificado) │
│ └── "console.log('Hola Mundo');" │
│ │
│ Git detecta: "Este archivo es diferente al último │
│ commit" │
│ │
└─────────────────────────────────────────────────────────┘
Capa 2: git add - El Staging Area
Ejecutas git add app.js. Aquí empieza la magia:
Git crea un Blob
Git comprime el contenido del archivo y calcula su SHA-1 (hash de 40 caracteres):
Contenido: "console.log('Hola Mundo');\n"
│
▼ zlib compress + SHA-1
│
Hash: 8ab686eafeb1f44702738c8b0f24f2567c36da6d
Este blob (binary large object) se guarda en .git/objects/:
.git/objects/
└── 8a/
└── b686eafeb1f44702738c8b0f24f2567c36da6d
Nota: Git divide el hash: primeros 2 caracteres = directorio, resto = nombre de archivo. Esto evita tener millones de archivos en un solo directorio.
Se actualiza el Index
El Index (staging area) es un archivo binario en .git/index. Guarda:
- Ruta del archivo (
app.js) - Hash del blob (
8ab686e...) - Metadatos (permisos, timestamps)
┌─────────────────────────────────────────────────────────┐
│ STAGING AREA │
│ (.git/index) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ app.js → 8ab686eafeb1f44702738c8b0f24f2567c36da │ │
│ │ mode: 100644 (archivo normal) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
.git/objects/
└── 8a/b686... (blob con el contenido)
Capa 3: git commit - Creando el Snapshot
Ejecutas git commit -m "Add hello world". Git crea tres tipos de objetos:
1. Tree (Árbol)
Un tree representa un directorio. Lista archivos y subdirectorios con sus hashes:
tree 9f4d...
├── 100644 blob 8ab686... app.js
├── 100644 blob 3c4e21... README.md
└── 040000 tree a1b2c3... src/
├── 100644 blob d4e5f6... index.js
└── 100644 blob e5f6a7... utils.js
2. Commit
El commit es un objeto que apunta a:
- El tree raíz del proyecto
- El commit padre (el anterior)
- Autor y committer (pueden ser diferentes)
- Mensaje del commit
- Timestamp
commit 7c8f9a...
├── tree 9f4d...
├── parent 1a2b3c... (commit anterior)
├── author Walter <walter@ejemplo.com> 1706612400 +0100
├── committer Walter <walter@ejemplo.com> 1706612400 +0100
└── message "Add hello world"
El grafo de objetos
┌─────────────────────────────────────────────────────────┐
│ .git/objects/ │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ commit │────────→│ tree │ │
│ │ 7c8f9a.. │ │ 9f4d... │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ │ parent ├── app.js → blob 8ab6... │
│ ▼ ├── README → blob 3c4e... │
│ ┌──────────┐ └── src/ → tree a1b2... │
│ │ commit │ │ │
│ │ 1a2b3c.. │ ├── blob │
│ │ (previo) │ └── blob │
│ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Actualización de referencias
Git actualiza .git/refs/heads/main (o tu rama actual):
$ cat .git/refs/heads/main
7c8f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f
La rama es simplemente un puntero a un commit.
┌─────────────────────────────────────────────────────────┐
│ REFERENCIAS │
│ │
│ .git/HEAD │
│ └── ref: refs/heads/main │
│ │
│ .git/refs/heads/main │
│ └── 7c8f9a... (nuevo commit) │
│ │
│ .git/refs/heads/feature │
│ └── 5d6e7f... (otro commit) │
│ │
└─────────────────────────────────────────────────────────┘
Capa 4: git push - Preparando la Transferencia
Ejecutas git push origin main. Git necesita:
- Determinar qué enviar: ¿Qué commits tiene el remoto? ¿Cuáles me faltan?
- Empaquetar objetos: Crear un packfile eficiente
- Enviar por red: Usando el protocolo Git
Negociación
Git “negocia” con el servidor:
Cliente Servidor (GitHub)
│ │
│ ── "Quiero pushear a main" ─────────→│
│ │
│ ←── "Mi main está en abc123..." ─────│
│ │
│ (Cliente calcula diferencia) │
│ "Debo enviar commits 7c8f9a, │
│ 6b7c8d, 5a6b7c" │
│ │
Packfile
Git crea un packfile: un archivo comprimido con todos los objetos necesarios.
┌─────────────────────────────────────────────────────────┐
│ PACKFILE │
│ │
│ Header: PACK + version + num_objects │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Object 1: commit 7c8f9a (deltified) │ │
│ │ Object 2: tree 9f4d... (deltified) │ │
│ │ Object 3: blob 8ab6... (base) │ │
│ │ Object 4: blob 3c4e... (delta de otro blob) │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ Checksum SHA-1 del packfile │
│ │
└─────────────────────────────────────────────────────────┘
Deltificación: Git almacena objetos similares como “deltas” (diferencias). Un archivo con un pequeño cambio se guarda como “el blob X más estos bytes diferentes”.
Capa 5: El Protocolo de Red
Git puede usar varios protocolos:
| Protocolo | URL | Puerto | Uso |
|---|---|---|---|
| HTTPS | https://github.com/... | 443 | Más común, requiere auth |
| SSH | git@github.com:... | 22 | Usa llaves SSH |
| Git | git://github.com/... | 9418 | Solo lectura, sin auth |
Flujo HTTPS (simplificado)
Cliente GitHub
│ │
│ ── POST /repo.git/git-receive-pack │
│ Headers: Authorization │
│ Body: capabilities + refs │
│ │
│ ←── 200 OK │
│ "Listo para recibir" │
│ │
│ ── POST (packfile binario) ───────→│
│ │
│ ←── 200 OK │
│ "unpack ok" │
│ "refs/heads/main: ok" │
│ │
El packfile viaja
┌─────────┐ HTTPS/SSH ┌─────────┐
│ Cliente │ ─── [PACKFILE binario] ───→│ GitHub │
│ │ ~50KB │ │
└─────────┘ (comprimido) └─────────┘
Capa 6: El Servidor Recibe
GitHub (o tu servidor Git) procesa el push:
1. Verificación
┌─────────────────────────────────────────────────────────┐
│ GITHUB SERVER │
│ │
│ 1. ¿Usuario autenticado? ✓ │
│ 2. ¿Tiene permisos de push? ✓ │
│ 3. ¿Es fast-forward? (no reescribe historia) │
│ - Si main remoto es ancestro del nuevo → ✓ │
│ - Si no → rechazar (o --force) │
│ │
└─────────────────────────────────────────────────────────┘
2. Hooks (pre-receive)
Antes de aceptar, ejecuta hooks:
# hooks/pre-receive (en el servidor)
# Puede rechazar el push por políticas:
# - No permitir push a main directo
# - Verificar que commits estén firmados
# - Validar formato de mensajes
3. Desempaquetar
Packfile recibido
│
▼
┌─────────────────────────────────────────────────────────┐
│ Descomprimir objetos │
│ │ │
│ ▼ │
│ Verificar integridad (SHA-1 de cada objeto) │
│ │ │
│ ▼ │
│ Almacenar en .git/objects/ │
│ │ │
│ ▼ │
│ Actualizar refs/heads/main │
└─────────────────────────────────────────────────────────┘
4. Hooks (post-receive)
Después de aceptar:
# hooks/post-receive
# - Notificar a CI/CD (GitHub Actions)
# - Enviar webhooks
# - Actualizar caches
# - Trigger deploys
Capa 7: Respuesta al Cliente
GitHub Cliente
│ │
│ ── "ok refs/heads/main" ─────────→│
│ │
│ Actualiza remote tracking:
│ origin/main = 7c8f9a...
│ │
│ $ git push
│ To github.com:user/repo.git
│ abc123..7c8f9a main → main
│ │
El Diagrama Completo
┌─────────────────────────────────────────────────────────────────────┐
│ TU COMPUTADORA │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ WORKING DIRECTORY │ │
│ │ │ │
│ │ app.js (modificado) │ │
│ └─────────────────────────────┬───────────────────────────────┘ │
│ │ git add │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ STAGING AREA │ │
│ │ (.git/index) │ │
│ │ │ │
│ │ app.js → blob 8ab6... │ │
│ └─────────────────────────────┬───────────────────────────────┘ │
│ │ git commit │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ LOCAL REPOSITORY │ │
│ │ (.git/objects) │ │
│ │ │ │
│ │ commit 7c8f → tree 9f4d → blob 8ab6 (app.js) │ │
│ │ │ → blob 3c4e (README) │ │
│ │ │ parent │ │
│ │ ▼ │ │
│ │ commit 1a2b (anterior) │ │
│ │ │ │
│ │ refs/heads/main → 7c8f... │ │
│ └─────────────────────────────┬───────────────────────────────┘ │
│ │ git push │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ PACK & SEND │ │
│ │ │ │
│ │ 1. Negociar con remoto (¿qué objetos faltan?) │ │
│ │ 2. Crear packfile (commits + trees + blobs) │ │
│ │ 3. Comprimir (zlib + deltificación) │ │
│ │ 4. Enviar por HTTPS/SSH │ │
│ └─────────────────────────────┬───────────────────────────────┘ │
│ │ │
└─────────────────────────────────┼───────────────────────────────────┘
│ HTTPS/SSH
▼
┌─────────────────────────────────────────────────────────────────────┐
│ GITHUB │
│ │
│ 1. Autenticar usuario │
│ 2. Verificar permisos │
│ 3. Ejecutar pre-receive hooks │
│ 4. Desempaquetar objetos │
│ 5. Verificar integridad (SHA-1) │
│ 6. Actualizar refs/heads/main │
│ 7. Ejecutar post-receive hooks │
│ └── Trigger GitHub Actions, webhooks, etc. │
│ │
│ refs/heads/main → 7c8f... │
│ │
└─────────────────────────────────────────────────────────────────────┘
¿Cuánto tiempo toma cada paso?
| Paso | Tiempo típico |
|---|---|
| git add (archivo pequeño) | < 10ms |
| git add (muchos archivos) | 100ms - 1s |
| git commit | 10-50ms |
| Crear packfile | 50-500ms |
| Transferencia red (1MB) | 100ms - 2s |
| Servidor procesa | 100-500ms |
| Total push típico | 500ms - 5s |
Repositorios grandes (Linux kernel: 1GB+) pueden tomar minutos en el primer clone, pero pushes incrementales siguen siendo rápidos.
Herramientas para inspeccionar
# Ver el contenido de un objeto
git cat-file -p 7c8f9a2
# Ver tipo de objeto
git cat-file -t 7c8f9a2
# Ver todos los objetos
find .git/objects -type f
# Ver el log con hashes completos
git log --format="%H %s"
# Ver qué se enviaría en un push
git push --dry-run -v origin main
# Ver el contenido del index
git ls-files --stage
# Ver diferencia de objetos con remoto
git rev-list origin/main..main
# Inspeccionar un packfile
git verify-pack -v .git/objects/pack/*.idx
Conclusión
Un simple git commit && git push:
- Staging: Git hashea el contenido y crea blobs
- Index: Actualiza el mapeo ruta → hash
- Commit: Crea tree + commit object, actualiza refs
- Pack: Empaqueta objetos nuevos con compresión delta
- Transfer: Negocia con el servidor, envía el packfile
- Server: Verifica, desempaqueta, actualiza refs, ejecuta hooks
- Done: Tu código está en el remoto
Git es fundamentalmente un sistema de archivos direccionado por contenido. Cada archivo, directorio y commit es un objeto identificado por su hash SHA-1. Esto hace que sea imposible modificar historia sin cambiar los hashes, y permite verificación de integridad en cada paso.
Este post es parte de la serie “El Viaje de la Información”, donde exploramos qué sucede realmente cuando interactuamos con nuestros sistemas.