Git Internals

Objetos y Fontaneria

Fontaneria y Porcelana

Git es fundamentalmente un sistema de archivos de contenido localizable con una interfaz de usuario de VCS escrita sobre el.


El libro Git Pro ha hablado de como utilizar Git con unos 30 verbos, tales como checkout, branch, remote, etc. Pero debido al origen de Git como una caja de herramientas para un VCS en lugar de como un sistema completo y amigable, existen varios verbos que realizan tareas de bajo nivel y que fueron disenados para ser utilizados de forma encadenada al estilo UNIX o para ser utilizados en scripts. Estos comandos son conocidos como los comandos de fontaneria (plumbing), mientras que los comandos mas amigables son conocidos como los comandos de porcelana (porcelain).


Los primeros nueve capitulos del libro Git Pro tratan casi exclusivamente los comandos de porcelana. En las clases de Git Internals trataremos los comandos de fontaneria, que te daran acceso a los recursos internos de Git y te ayudaran a comprender como y por que Git hace lo que hace. Son mas utiles como bloques de construccion para nuevas herramientas y scripts personalizados.


La carpeta .git/

Cuando ejecutas git init sobre una carpeta, Git crea la carpeta auxiliar .git/. Esta es la carpeta donde se ubica practicamente todo lo almacenado y manipulado por Git. Si deseas hacer una copia de seguridad de tu repositorio, con solo copiar esta carpeta ya tienes tu copia completa.

ls -F1
HEAD
config*
description
hooks/
info/
objects/
refs/
  • description — usado solo en GitWeb, no necesitas preocuparte por el.
  • config — contiene las opciones de configuracion especificas del proyecto.
  • info/ — guarda un archivo global de exclusion con patrones a ignorar ademas de los presentes en .gitignore.
  • hooks/ — contiene los scripts de enganche, tanto del lado cliente como del servidor.

Las cuatro entradas importantes son:

  • HEAD — apunta a la rama que tengas activa en este momento.
  • index — donde Git almacena la informacion sobre tu area de preparacion (staging area).
  • objects/ — guarda el contenido de tu base de datos.
  • refs/ — guarda las referencias a las confirmaciones (commits).

Estas cuatro entradas forman el nucleo de Git. Las iremos viendo en detalle a lo largo de estas clases.


Los objetos Git

Git es un sistema de archivos de contenido localizable. Lo que significa que en el nucleo de Git hay un simple almacen de datos clave-valor. Puedes insertar cualquier tipo de contenido en el, y te devolvera una clave que puedes usar para recuperar ese contenido cuando quieras.


Los objetos Blob

Para ver esto en accion, usaremos el comando de fontaneria git hash-object, que toma un contenido, lo almacena en la carpeta .git y devuelve la clave unica con la que fue guardado:

echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

La opcion -w le indica que almacene el objeto, sin ella solo calcularia la clave. La opcion --stdin le indica que lea el contenido desde la entrada estandar. El valor devuelto es un checksum SHA-1 de 40 caracteres. Puedes ver el archivo creado en:

find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Git usa los dos primeros caracteres como nombre de carpeta y los 38 restantes como nombre de archivo.


Para recuperar el contenido, usas git cat-file con la opcion -p (pretty-print):

git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

Tambien puedes preguntar por el tipo del objeto con -t:

git cat-file -t d670460b4b4aece5915caf5c68d12f560a9fe3e4
blob

Esto es un objeto de tipo blob (binary large object): el tipo que Git usa para almacenar el contenido de los archivos. Los blobs almacenan solo el contenido, sin nombre de archivo ni metadata.


Los objetos Tree

Los objetos tree resuelven el problema del nombre de archivo y permiten almacenar grupos de archivos juntos. Un tree es similar a un directorio en UNIX: puede contener referencias a blobs u otros trees.

git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

Cada linea tiene: modo del archivo, tipo de objeto, SHA-1, y nombre.

Modelo de datos de Git: tree con blobs y subtrees

La sentencia master^{tree} indica el objeto tree apuntado por el ultimo commit de master. El modo indica los permisos y tipo de objeto UNIX. Por ejemplo:

  • 100644: Archivo normal no ejecutable con permisos rw-r--r--.
  • 040000: Directorio/carpeta.
  • 100755: Archivo ejecutable.
  • 120000: Enlace simbolico.

Git crea un tree a partir del estado de tu area de preparacion (staging area) o indice (index). Para crear un tree manualmente, podemos usar update-index actualiza al area de preparacion para los archivos con seguimiento (tracked).

git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

Usamos --add porque el archivo que queremos agregar no esta bajo seguimiento (untracked). La opcion --cacheinfo se usa cuando el archivo esta en la base de datos de Git pero no en el directorio de trabajo.


Luego especificamos el modo, checksum y nombre del archivo:

  • El modo indica que tipo de objeto es.
  • El checksum es la clave para obtener el contenido del archivo (valor) en la base de datos Git.
  • El nombre del archivo simplemente le dice a Git donde colocar el archivo y como llamarlo.

Luego podemos usar write-tree para escribir el area de preparacion a un objeto tree:

git write-tree # Escribir a un objeto tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
# ---
git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579 # Ver el contenido del objeto
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt

Cuando pedimos el contenido del objeto tree, nos devuelve el objeto blob que esta dentro de nuestro objeto tree.


Tambien podemos comprobar si realmente es un objeto tree:

git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

Ejemplo

Ahora crearemos un nuevo arbol con una segunda version de test.txt y un nuevo archivo new.txt:

echo 'new file' > new.txt # Crear new.txt
git update-index test.txt # Actualizar el area de preparacion para test.txt
git update-index --add new.txt # Agregar new.txt al area de preparacion

Escribimos el area de preparacion a un objeto tree:

git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341

Comprobamos su contenido:

git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

Ahora simplemente por probar, incluiremos el tree del ejemplo anterior en el tree que acabamos de crear. Usamos read-tree que agrega el tree al area de preparacion, y la opcion --prefix para tratarlo como si fuera una subcarpeta. En la opcion --prefix indicamos el nombre del subdirectorio y el checksum del tree.

git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579 # Agregar el tree bajo el nombre de "bak"
git write-tree # Escribir el area de preparacion a un arbol
3c4e9cd789d88d8d89c1073707c3585e41b0e614

Ahora comprobemos que tiene dentro nuestro nuevo objeto tree:

git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6737e589b58f5f8      test.txt
Modelo de datos de Git: tree con subtree bak

Los objetos Commit

Imaginemos que tienes tres trees que representan diferentes snapshots del proyecto, pero el problema sigue siendo el mismo: debes recordar los tres valores checksum SHA-1 para poder acceder a ellas. Tampoco tienes informacion sobre quien guardo las snapshots, cuando, o por que. Para eso existen los objetos commit.


Para crear un objeto commit manualmente, usas git commit-tree pasando el checksum SHA-1 del tree y, opcionalmente, el commit padre:

echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d

Si inspeccionas el objeto:

git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700
#
first commit

Un objeto commit contiene:

  • El tree de nivel superior del proyecto en ese momento.
  • Los commits padre (ninguno en este ejemplo).
  • El autor y committer con su fecha.
  • El mensaje del commit.

Puedes encadenar commits pasando el checksum SHA-1 del anterior como padre:

echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d

echo 'third commit'  | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9

Ahora si ejecutas git log sobre el ultimo checksum SHA-1, veras un historial real:

git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700
#
	third commit
#
 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)
#
commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:14:29 2009 -0700
#
	second commit
#
 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)
#
commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:09:34 2009 -0700
#
    first commit
#
 test.txt | 1 +
 1 file changed, 1 insertion(+)

Esto es exactamente lo que Git hace cuando ejecutas git add y git commit: almacena blobs de los archivos modificados, actualiza el area de preparacion, escribe los trees, y crea los objetos commit con los checksums SHA-1 de los trees de nivel superior y los commits padre.


Ahora comprobemos que todos los objetos blob, tree y commit hasta el momento fueron guardados por Git en .git/objects:

find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

Hasta ahora deberiamos tener algo como esto:


Modelo de datos completo: commits apuntando a trees y blobs

Como Git almacena los objetos internamente

Internamente, Git construye una cabecera comenzando por el tipo de objeto, seguido del tamano del contenido y un byte nulo.

content = "what is up, doc?"
header = f"blob {len(content)}\x00"

Luego concatena la cabecera con el contenido original para calcular el checksum SHA-1, comprime todo con zlib,

store = header + content
sha1 = checksum(store)
zlib_content = zlib.compress(store)

y escribe el resultado en disco usando los dos primeros caracteres del checksum SHA-1 como carpeta y los 38 restantes como nombre de archivo.

path = ".git/objects" + sha1[0:2] + "/" + sha1[2:38]
with open(path) as file: file.write(zlib_content)

Todos los objetos Git (blob, tree, commit, tag) se almacenan de la misma forma. La unica diferencia es que sus cabeceras comienzan con un tipo diferente: blob, tree, commit o tag.


Recursos