Capítulo 8. Secretos Revelados

Tomemos un vistazo bajo la mesa y expliquemos cómo realiza sus milagros. No escatimare sobre los detalles. Para descripciones detalladas referirse a el manual de usuario.

Invisibilidad

¿Cómo puede ser Git tan discreto? Aparte de los "commit" y "merge" ocasionales, usted puede trabajar inconscientemente de que un control de versiones existe. Esto sucede hasta que usted lo necesita, y esto es cuando esta agradecido de que Git este viendo por usted todo el tiempo.

Otros sistemas de control de versiones te fuerzan a luchar constantemente con cinta roja y burocracia. Los permisos de archivos podría ser de solo lectura a menos que usted explícitamente le diga a un servidor central cuales archivos intenta editar. Los comandos más básicos podrían retardarse al punto de arrastrarse cuando el número de usuarios se incrementa. El trabajo se paraliza cuando la red o el servidor central deja de funcionar.

En contraste, Git simplemente mantiene la historia de su proyecto en el directorio .git ubicado en su directorio de trabajo. Ésta es su propia copia de la historia, de esta manera usted puede trabajar desconectado hasta que desee comunicarse con otros. Usted tiene control total del destino de sus archivos ya que Git puede fácilmente recrear un estado guardado de .git en cualquier momento.

Integridad

Muchas personas asocian criptografía con manterner la información secreta, pero otro objetivo importante es mantener la información segura. El uso apropiado de las funciones "hash" en criptografía puede prevenir corrupción de datos accidental o intencional.

Un hash SHA1 puede pensarse como un identificador numérico único de 160-bit para cualquier linea de caracteres que usted pueda encontrar en su vida. Pero mas que eso: cualquier linea de caracteres que cualquier ser humano vaya a usar en muchas vidas.

Como un "hash" SHA1 es una linea de "bytes", podemos dividir lineas de "bytes" que contienen otros "hash". Esta simple observación es sorpresivamente útil: buscar cadenas de "hash". Mas adelante veremos como Git lo utiliza para garantizar eficientemente la integridad de datos.

Brevemente, Git mantiene sus datos en el subdirectorio .git/objects, donde en lugar de archivos normales, usted encontrara solo identificadores. Al usar identificadores como nombres de archivos, asi como algunos trucos de bloqueo de archivos y sellos de tiempo, Git transforma cualquier humilde sistema de archivos en una eficiente y robusta base de datos.

Inteligencia

¿Cómo sabe Git que usted renombró un archivo, incluso pensando que usted nunca menciono el hecho explícitamente? Seguramente, usted podria haber ejecutado git mv, pero eso es exactamente lo mismo a git rm seguido de git add.

Git heuristicamente averigua los cambios de nombre y copias entre versiones sucesivas. ¡De hecho, puede detectar pedazos de codigo que estan siendo movidos o copiados entre archivos! Al pensar que no puede cubrir todos los casos, realiza un trabajo decente, y esta caracteristica esta siendo mejorada constantemente. Si falla al trabajar por usted, intente habilitar deteccion de copias mas caras y considere actualizar.

Indexacion

Para cada archivo rastreado, Git guarda informacion como su tamano, fecha de creacion y ultima fecha de modificacion en un archivo conocido como index. Para determinar cuando un archivo ha cambiado, Git compara su estado actual con lo que estan acumulados en el indice. Si son iguales, entonces Git omite leer el archivo nuevamente.

Partiendo del hecho de que las llamadas de estado son considerablemente mas rapidas que leer archivos, si usted edita unicamente algunos archivos, Git puede actualizar su estado en casi nada de tiempo.

Hemos insistido anteriormente que el indice es un area de escenificacion. ¿Porque un monton de estados de archivo es un area de escenificacion? Porque el comando "add" pone archivos en la based de datos de Git y actualiza los estados, mientras que el comando "commit", sin opciones, crea una actualizacion basada unicamente en estos estados y los archivos que ya estan en la base de datos.

Los origenes de Git

Esto anuncio: Lista de Correos de "Linux Kernel" Describe la cadena de eventos que llevaron a Git. El hilo entero es un fascinante sitio para los historiadores de Git.

La Base de Datos de Objetos

Toda version de sus datos is mantenida en la "Base de Datos de Objetos", la cual vive en el subdirectorio git./objects; los otros residentes de .git mantienen menos datos: el indice, nombres de ramas, etiquetas, opciones de configuracion, logs, la localizacion actual del primer "commit", y asi sucesivamente. La Base de Datos de Objetos is elemental pero elegante, y la fuente del poder de Git.

Cada archivo que se encuentre en .git/objects es un objeto. Existen 3 tipos de objetos que nos concierne: objetos blob, objetos tree, y objetos commit.

Blobs

Primero, un truco magico. Elija un archivo, cualquier archivo. En un directorio vacio:

$ echo sweet > NOMBRE_DE_ARCHIVO $ git init $ git add . $ find .git/objects -type f

Usted podra ver .git/objects/aa/823728ea7d592acc69b36875a482cdf3fd5c8d.

¿Cómo se esto sin saber el nombre del archivo? Es porque el "hash" SHA1 de:

"blob" SP "6" NUL "sweet" LF

es aa823728ea7d592acc69b36875a482cdf3fd5c8d, donde SP es un espacio, NUL es un "byte" zero y LF es un avance de linea. Usted puede verificar esto escribiendo:

$ printf "blob 6\000sweet\n" | sha1sum

Git es contenido-direccionable: los archivos no son almacenados de acuerdo con su nombre, sino mas por el "hash" de la informacion que contiene, dentro de un archivo llamamos al objeto "blob". Podemos pensar de un "hash" como el identificador del contenido de un archivo, asi que en un sentido estamos buscando archivos por su contenido. El "blob" inicial es meramente un encabezado que consiste en el typo del objeto y su tamaño en "bytes"; lo que simplifica la contabilidad interna.

De esta manera yo puedo predecir fácilmente lo que usted vera. El nombre del archivo es irrelevante: solo la información interna es usada para construir el objeto "blob".

Usted podría estarse preguntando qué sucede con archivos idénticos. Intente agregar copias de su archivo, con cualquier cosa de nombre. El contenido de .git/objects se queda de la misma manera sin importar cuantos agregue. Git guarda la información solo una vez.

Por cierto, los archivos ubicados en .git/objects estan comprimidos con zlib asi que usted no debería poder verlos directamente. Filtrelos a través de zpipe -d, o digite:

$ git cat-file -p aa823728ea7d592acc69b36875a482cdf3fd5c8d

lo que bellamente imprime el objeto dado.

Árboles

¿Pero dónde están los nombres de archivo? Ellos deberían estar almacenados en algún lugar en algún organizador. Git toma su turno para poner los nombres de archivo durante un "commit":

$ git commit # Escribe algún mensaje. $ find .git/objects -type f

Ahora debería poder ver 3 objetos. Esta vez no puedo decirte qué son esos 2 nuevos archivos, esto debende parcialmente del nombre de archivo que escogió. Procederemos asumiendo que eligió rose. Si usted no lo hizo, podría reescribir la historia para hacer parecer que usted lo hizo:

$ git filter-branch --tree-filter mv YOUR_FILENAME rose $ find .git/objects -type f

Ahora debería poder ver el archivo .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9, porque este es el SHA1 "hash" de su contenido:

"tree" SP "32" NUL "100644 rose" NUL 0xaa823728ea7d592acc69b36875a482cdf3fd5c8d

Verifique que este archivo contiene verdaderamente lo anterior escribiendo:

$ echo 05b217bb859794d08bb9e4f7f04cbda4b207fbe9 | git cat-file --batch

Con zpipe, es fácil verificar el hash:

$ zpipe -d < .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9 | sha1sum

Las verficaciones de "hash" son mas difíciles utilizando cat-file porque su salida contiene más que la fuente del objeto del archivo descomprimido.

Este archivo es un objeto "tree": una lista de tuplas de un tipo de archivo, un nombre de archivo, y un "hash". En nuestro ejemplo, el tipo de archivo es 100644, lo cual significa que rose es un archivo normal, y el "hash" es el objeto "blob" que contiene los contenidos de rose. Otro posible tipo de archivo son los ejecutables, enlaces simbólicos o los directorios. En el último caso, los punteros "hash" a un objeto tree.

Si usted ejecuta "filter-branch", obtendrá objetos viejos que ya no necesita. Aunque serán desechados automáticamente una vez que su periodo de gracia expire, nosotros los borraremos ahora para hacer nuestro ejemplo de prueba más fácil de seguir:

$ rm -r .git/refs/original $ git reflog expire --expire=now --all $ git prune

Para proyectos reales típicamente usted debería evitar comandos como este, ya que está destruyendo respaldos. Si usted quiere un repositorio limpio, usualmente es mejor crear un clon nuevo. También, tenga cuidado cuando está manipulando directamente .git: ¿qué pasaría si un proceso Git está corriendo al mismo tiempo, ó un corte eléctrico ocurre? En general, las referencias deberían ser eliminadas con git update-ref -d, pensando que usualmente es más seguro remover refs/original a mano.

"Commits"

Hemos explicado 2 de 3 objetos. El tercero es un objeto "commit". Sus contenidos dependen del mensaje del "commit" así como de la fecha en el que fué creado. Para contrastar lo que tenemos aquí, deberemos torcerlo un poco:

$ git commit --amend -m Shakespeare # Change the commit message. $ git filter-branch --env-filter export GIT_AUTHOR_DATE="Fri 13 Feb 2009 15:31:30 -0800" GIT_AUTHOR_NAME="Alice" GIT_AUTHOR_EMAIL="alice@example.com" GIT_COMMITTER_DATE="Fri, 13 Feb 2009 15:31:30 -0800" GIT_COMMITTER_NAME="Bob" GIT_COMMITTER_EMAIL="bob@example.com" # Rig timestamps and authors. $ find .git/objects -type f

.git/objects/49/993fe130c4b3bf24857a15d7969c396b7bc187

"commit 158" NUL
"tree 05b217bb859794d08bb9e4f7f04cbda4b207fbe9" LF
"author Alice <alice@example.com> 1234567890 -0800" LF
"committer Bob <bob@example.com> 1234567890 -0800" LF
LF
"Shakespeare" LF

Como antes, usted puede correr zpipe ó cat-file para que vea por usted mismo.

Este es el primer "commit", así que no existe un "commit" padre, pero "commit" sucesivos} siempre van a contener al menos una línea identificando el "commit" padre.

Sin Distinción de la Magia

Los secretos de Git parecen muy simples. Parece ser que usted podría unir algunos comandos y agregar una parte de código C para cocinar algo en cuestión de horas: una mezcla de operaciones básicas con archivos de sistema y SHA1 "hashing", adornado con archivos "lock" y "fsyncs" para robustez. De hecho, esto describe con exactitud las versiones anteriores de Git. Sin embargo, fuera de ingeniosos trucos de empaque para salvar espacio, e ingeniosos trucos de indexación para salvar tiempo, nosotros ahora sabemos definitivamente cómo Git cambia los sistemas de archivo en una base de datos perfecta para el control de versiones.

Por ejemplo, si cualquier archivo dentro de la base de datos es corrompido por un error de disco, entonces su "hash" nunca más podrá ser emparejado, alertándonos del problema. Al crear un "hash" de otros "hash" de otros objetos, mantenemos la integridad a todos los niveles. Los "commit" son atómicos, eso es, un "commit" nunca puede guardar solamente partes de cambios: nosotros podemos únicamente calcular el "hash" de un "commit" y guardarlo en la base de datos después de que ya hemos guardado todos los árboles relevantes, los "blob" y los "commit" padres. La base de datos de objetos es inmune a interrupciones inesperadas como cortes de electricidad.

Hemos derrotado inclusive al más tortuoso adversario. Suponga que alguien intenta modificar furtivamente los contenidos de un archivo en una versión anterior de un proyecto. Para mantener la base de datos de objetos sana, ellos deben cambiar también el "hash" del objeto "blob" correspondiente porque ahora es una cadena de bytes distinta. Esto significa que además tendrán que cambiar el "hash" de cualquier árbol de objetos referenciando el archivo, y por defecto cambiar todos los "hash" de todos los descendientes del árbol de ese "commit". Además de todos los "hash" de todos los descendientes de ese "commit".Esto implica que el "hash" de la cabeza oficial difiere a la del repositorio erróneo. Para seguir el curso de los "hash" que difieren podemos localizar el archivo mutilado, así como el "commit" en donde fue corrompido inicialmente.

En corto tiempo, mientras que los 20 "byte" representando el último "commit" seguro, es imposible de manosear en un repositorio de Git.

¿Qué sucede con las famosas características de Git? ¿Las ramas? ¿Las mezclas? ¿Los tags? Simples detalles. La cabeza actual es mantenida en el archivo .git/HEAD, la cual contiene un "hash" de un objeto "commit". El "hash" es actualizado durante un "commit" así como cualquier otro comando. Las ramas son casi lo mismo: son archivos en .git/refs/heads. Las etiquetas también: se encuentran en .git/refs/tags pero son actualizadas por un juego de comandos distintos.