Capítulo 29 de 33 9 secciones 7 min

Qué hay dentro de la carpeta oculta

Cómo Git guarda todo, para que deje de parecer magia

<strong>Todo el repositorio vive en la carpeta <code>.git</code>, y dentro es más simple de lo que parece.</strong> Cada versión de cada archivo se guarda una vez, con un nombre que sale de su propio contenido: el hash. Las ramas son archivos de texto de una línea con un hash dentro, y <code>HEAD</code> es otro archivo que dice en qué rama estás. Saber esto no hace falta para usar Git, pero quita el miedo, que es de lo que trata todo este libro.

Llevas veintiocho capítulos usando Git. Este es el capítulo donde miramos debajo, no porque haga falta, sino porque lo que se entiende deja de dar miedo 🔦

La carpeta

mkdir ventas-miss-yera
cd ventas-miss-yera
git init -q
printf 'ciudad,monto\nLima,1200\n' > ventas.csv
git add ventas.csv
git commit -q -m "Primeras ventas de Lima"
ls -a
ls .git
.
..
.git
ventas.csv
COMMIT_EDITMSG
HEAD
branches
config
description
hooks
index
info
logs
objects
refs

Eso es todo tu repositorio. Si copias esa carpeta a otro sitio, te llevas la historia completa; si la borras, tu proyecto vuelve a ser una carpeta normal. Es lo que pasó en la trampa de la portada 💀

Una rama es un archivo de texto

Esto es lo que más sorprende:

cat .git/HEAD
cat .git/refs/heads/main
git log --oneline -1
ref: refs/heads/main
54da6e2f9b8dba1c61a0d04ac23844ad5499c36b
54da6e2 Primeras ventas de Lima

HEAD dice en qué rama estás. La rama es un archivo con un hash y nada más. Por eso crear una rama es instantáneo aunque el proyecto pese gigas: se escribe un archivo de 41 caracteres 🪶

git switch -q -c reporte-por-canal
cat .git/HEAD
ls .git/refs/heads
git switch -q main
cat .git/HEAD
ref: refs/heads/reporte-por-canal
main
reporte-por-canal
ref: refs/heads/main

El hash sale del contenido

Esta es la idea de la que cuelga todo lo demás. El nombre de cada cosa guardada se calcula a partir de lo que contiene:

printf 'ciudad,monto\nLima,1200\n' | git hash-object --stdin
printf 'ciudad,monto\nLima,1200\n' | git hash-object --stdin
printf 'ciudad,monto\nLima,1290\n' | git hash-object --stdin
b5256c13309cbcc948c114c9e9ebc2a61de85b07
b5256c13309cbcc948c114c9e9ebc2a61de85b07
75af332b2019896a3976bfa99f247077777e4071

El mismo contenido da el mismo nombre, siempre y en cualquier máquina del mundo. Cambia un dígito y el nombre es otro completamente distinto 🧬

De ahí salen dos cosas importantes: que Git guarde una sola vez un archivo repetido en veinte carpetas, y que no se pueda cambiar el pasado sin que se note, porque cambiar un commit viejo cambiaría su hash y el de todos los que vienen detrás 🔗

Los tres tipos de cosa que guarda

Se llamaGuarda
blobEl contenido de un archivo, sin su nombre
treeUna carpeta: qué nombres hay y a qué blob apunta cada uno
commitUn tree, su padre, el autor, la fecha y el mensaje
git cat-file -t HEAD
git cat-file -p HEAD
commit
tree a062f29a96c59f3fa0b44bb53e1804fcf4a79914
author Miss Yera <hola@missyera.com> 1768485600 -0500
committer Miss Yera <hola@missyera.com> 1768485600 -0500

Primeras ventas de Lima

Ahí está un commit entero, por dentro. Un tree, un autor, un committer y el mensaje. Nada más 📄

git cat-file -p HEAD^{tree}
git cat-file -p $(git rev-parse HEAD:ventas.csv)
100644 blob b5256c13309cbcc948c114c9e9ebc2a61de85b07	ventas.csv
ciudad,monto
Lima,1200

El árbol dice "aquí hay un archivo llamado ventas.csv cuyo contenido es este blob", y el blob es el contenido. Nombre y contenido separados: por eso renombrar un archivo no duplica nada 🪄

Por qué el primer commit no tiene padre

printf 'Arequipa,890\n' >> ventas.csv
git add ventas.csv
git commit -q -m "Entran las ventas de Arequipa"
git cat-file -p HEAD | head -3
git cat-file -p HEAD~1 | head -3
tree d14e7b7375b39679bb634384e6d651f8e899ebf9
parent 54da6e2f9b8dba1c61a0d04ac23844ad5499c36b
author Miss Yera <hola@missyera.com> 1768485600 -0500
tree a062f29a96c59f3fa0b44bb53e1804fcf4a79914
author Miss Yera <hola@missyera.com> 1768485600 -0500
committer Miss Yera <hola@missyera.com> 1768485600 -0500

El segundo commit tiene una línea parent y el primero no. Esa cadena de padres es la historia: no hay una lista guardada en ningún lado, cada commit apunta al anterior 🔗

Cuando pesa de más

git count-objects -v
git gc -q
git count-objects -v
count: 6
size: 24
in-pack: 0
packs: 0
size-pack: 0
prune-packable: 0
garbage: 0
size-garbage: 0
count: 0
size: 0
in-pack: 6
packs: 1
size-pack: 1
prune-packable: 0
garbage: 0
size-garbage: 0

gc agrupa y comprime los objetos sueltos. Git lo corre solo de vez en cuando, y es lo que hay que usar cuando la carpeta pesa, en vez de borrar nada a mano 🧹

La trampa

Tu repositorio pesa mucho, entras a la carpeta .git a ver qué hay y borras lo que te parece basura.

$ du -sh .git
48M	.git

$ rm -rf .git/objects/pack
$ git status

error: object file .git/objects/pack/pack-3c1f80a.pack is empty
fatal: loose object 8a2b19c is corrupt
Qué está mal

Esa carpeta no es basura, es el repositorio 📦

Los archivos que ves en tu carpeta son una copia de trabajo; el proyecto de verdad, con toda su historia, vive dentro de .git. Borrar ahí dentro no libera espacio, rompe el repositorio.

Si pesa de más, la respuesta casi nunca está en borrar: está en git gc, que compacta, o en dejar de meter archivos que no deberían entrar, que es 9.

La regla: dentro de .git se mira, no se toca. Y todo lo que hay ahí tiene un comando de Git que lo hace bien.

Comprueba que se entendió

Comprueba que lo tienes

Creas una rama nueva en un proyecto de 3 GB. ¿Qué escribe Git en el disco?

  • Un archivo de texto con un hash dentro
  • Una copia del proyecto entero
  • Nada, la rama solo existe en memoria
  • Una copia de los archivos que cambiaron

Ejercicios

1. Mira dentro de la carpeta

Crea el catálogo y lista qué hay en .git.

cd ..
mkdir catalogo
cd catalogo
git init -q
printf 'bodega,ciudad\nBodega Inti,Cusco\n' > bodegas.csv
git add bodegas.csv
git commit -q -m "Catalogo inicial de bodegas"
ls .git
COMMIT_EDITMSG
HEAD
branches
config
description
hooks
index
info
logs
objects
refs

Ahí está todo el repositorio, y nada de eso se toca a mano.

2. Lee la rama y HEAD

Los dos son archivos de texto.

cat .git/HEAD
cat .git/refs/heads/main
git log --oneline -1
ref: refs/heads/main
a9874ae5ade9ebff480ec9f29629b1b651573849
a9874ae Catalogo inicial de bodegas

El hash del archivo y el del log son el mismo.

3. Comprueba que el hash sale del contenido

El mismo texto da siempre el mismo nombre.

printf 'Bodega Inti,Cusco\n' | git hash-object --stdin
printf 'Bodega Inti,Cusco\n' | git hash-object --stdin
printf 'Bodega Inti,Arequipa\n' | git hash-object --stdin
a75ee2f9c7575eade168c118588516918a7b764e
a75ee2f9c7575eade168c118588516918a7b764e
ff769435ae848d1920b43c6efbf36af331a720ab

Dos iguales y uno distinto. Ese es todo el truco 🧬

4. Abre un commit por dentro

Qué guarda exactamente un commit.

git cat-file -t HEAD
git cat-file -p HEAD
commit
tree 4fd89c80cded29655afeee91b1fa83c80d97853d
author Miss Yera <hola@missyera.com> 1768485600 -0500
committer Miss Yera <hola@missyera.com> 1768485600 -0500

Catalogo inicial de bodegas

Un árbol, un autor, un committer y el mensaje.

5. Baja hasta el contenido del archivo

Del commit al árbol, y del árbol al contenido.

git cat-file -p HEAD^{tree}
git cat-file -p $(git rev-parse HEAD:bodegas.csv)
100644 blob 9fe960592f714d295228a7d61e886b631b6d8c12	bodegas.csv
bodega,ciudad
Bodega Inti,Cusco

El nombre vive en el árbol y el contenido en el blob, separados 🪄

6. Cuenta y compacta

Cuántos objetos hay sueltos y qué pasa al compactar.

printf 'Minimarket Sol,Trujillo\n' >> bodegas.csv
git add bodegas.csv
git commit -q -m "Entra el minimarket de Trujillo"
git count-objects -v
git gc -q
git count-objects -v
count: 6
size: 24
in-pack: 0
packs: 0
size-pack: 0
prune-packable: 0
garbage: 0
size-garbage: 0
count: 0
size: 0
in-pack: 6
packs: 1
size-pack: 1
prune-packable: 0
garbage: 0
size-garbage: 0

Los sueltos se agrupan en un paquete. Eso es lo que hay que correr cuando pesa 🧹

7. Pide un objeto que no existe

Un hash inventado.

git cat-file -p 1111111111111111111111111111111111111111
fatal: Not a valid object name 1111111111111111111111111111111111111111

Como el nombre sale del contenido, un hash inventado no corresponde a nada y Git lo dice sin rodeos 🛑

Lo que te llevas

Una rama es un archivo con un hash. El hash sale del contenido. Y dentro de .git se mira, no se toca.

¿Tienes alguna duda o consulta?