Pro Git, el libro oficial de Git

9.6. Protocolos de transferencia

Git puede transferir datos entre dos repositorios utilizando uno de sus dos principales mecanismos de transporte: sobre HTTP (protocolo tonto), o sobre los denominados protocolos inteligentes (utilizados en file://, ssh:// o git://). En esta parte, se verán brevemente cómo trabajan esos dos tipos de protocolo.

9.6.1. El protocolo tonto (dumb)

El transporte de Git sobre protocolo HTTP es conocido también como protocolo tonto porque no requiere ningún tipo de codigo Git en la parte servidor. El proceso de recuperación (fetch) de datos se limita a una serie de peticiones GET, siendo el cliente quien ha de conocer la estructura del repositorio Git en el servidor. Vamos a revisar el proceso http-fetch para una libreria simple de Git:

$ git clone http://github.com/schacon/simplegit-progit.git

Lo primero que hace este comando es recuperar el archivo info/refs. Este es un archivo escrito por el comando update-server-info, el que has de habilitar como enganche (hook) post-receive para permitir funcionar correctamente al transporte HTTP:

=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949     refs/heads/master

A partir de ahi, ya tienes una lista de las referencias remotas y sus SHAs. Lo siguiente es mirar cuál es la referencia a HEAD, de tal forma que puedas saber el punto a activar (checkout) cuando termines:

=> GET HEAD
ref: refs/heads/master

Ves que es la rama master la que has de activar cuando el proceso esté completado. En este punto, ya estás preparado para seguir procesando el resto de los objetos. En el archivo info/refs se ve que el punto de partida es la confirmación de cambios (commit) ca82a6, y, por tanto, comenzaremos recuperandola:

=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)

Cuando recuperas un objeto, dicho objeto se encuentra sin comprimir (loose) en el servidor y lo traes mediante una petición estática HTTP GET. Puedes descomprimirlo, quitarle la cabecera y mirar el contenido:

$ git cat-file -p ca82a6dff817ec66f44342007202690a93763949
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <[email protected]> 1205815931 -0700
committer Scott Chacon <[email protected]> 1240030591 -0700
changed the version number

Tras esto, ya tienes más objetos a recuperar — el árbol de contenido cfda3b al que apunta la confirmación de cambios; y la confirmación de cambios padre 085bb3 —.

=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)

El siguiente objeto confirmación de cambio (commit). Y el árbol de contenido:

=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)

Pero... ¡Ay!... parece que el objeto árbol no está suelto en el servidor. Por lo que obtienes una respuesta 404 (objeto no encontrado). Puede haber un par de razones para que suceda esto: el objeto está en otro repositorio alternativo; o el objeto está en este repositorio, pero dentro de un objeto empaquetador (packfile). Git comprueba primero a ver si en el listado hay alguna alternativa:

=> GET objects/info/http-alternates
(empty file)

En el caso de que esto devolviera una lista de ubicaciones (URL) alternativas, Git busca en ellas. (Es un mecanismo muy adecuado en aquellos proyectos donde hay segmentos derivados uno de otro compartiendo objetos en disco). Pero, en este caso, no hay altenativas. Por lo que el objeto debe encontrarse dentro de un empaquetado. Para ver que empaquetados hay disponibles en el servidor, has de recuperar el archivo objects/info/packs. Este contiene una lista de todos ellos: (que ha sido generada por update-server-info)

=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack

Vemos que hay un archivo empaquetado, y el objeto buscado ha de encontrarse dentro de él; pero merece comprobarlo revisando el archivo de índice, para asegurarse. Hacer la comprobacion es sobre todo util en aquellos casos donde existan multiples archivos empaquetados en el servidor, para determinar así en cual de ellos se encuentra el objeto que necesitas:

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)

Una vez tengas el índice del empaquetado, puedes mirar si el objeto buscado está en él, (Dicho índice contiene la lista de SHAs de los objetos dentro del empaquetado y las ubicaciones — offsets — de cada uno de llos dentro de él). Una vez comprobada la presencia del objeto, adelante con la recuperación de todo el archivo empaquetado:

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)

Cuando tengas el objeto árbol, puedes continuar avanzando por las confirmaciones de cambio. Y, como estás también están dentro del archivo empaquetado que acabas de descargar, ya no necesitas hacer mas peticiones al servidor. Git activa una copia de trabajo de la rama master señalada por la referencia HEAD que has descargado al principio.

La salida completa de todo el proceso es algo como esto:

$ git clone http://github.com/schacon/simplegit-progit.git
Initialized empty Git repository in /private/tmp/simplegit-progit/.git/
got ca82a6dff817ec66f44342007202690a93763949
walk ca82a6dff817ec66f44342007202690a93763949
got 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Getting alternates list for http://github.com/schacon/simplegit-progit.git
Getting pack list for http://github.com/schacon/simplegit-progit.git
Getting index for pack 816a9b2334da9953e530f27bcac22082a9f5b835
Getting pack 816a9b2334da9953e530f27bcac22082a9f5b835
 which contains cfda3bf379e4f8dba8717dee55aab78aef7f4daf
walk 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
walk a11bef06a3f659402fe7563abf99ad00de2209e6

9.6.2. El protocolo inteligente (smart)

HTTP es un protocolo simple, pero ineficiente. Es mucho más común utilizar protocolos inteligentes para transferir datos. Estos protocolos suelen tener procesos en el lado remoto y conocen acerca de la estructura de datos Git en ese lado, — pueden leer datos localmente y determinar lo que el cliente tiene ya o necesita a continuación, para generar automáticamente datos expresamente preparados para él —. Existen dos conjuntos de procesos para transferir datos: uno para para enviar y otro par para recibir.

9.6.2.1. Enviando datos, (Uploading)

Para enviar datos a un proceso remoto, Git utliza send-pack (enviar paquete) y receive-pack (recibir paquete). El proceso send-pack corre en el cliente y conecta con el proceso receive-pack corriendo en el lado remoto.

Por ejemplo, si lanzas el comando git push origin master en tu proyecto y origin está definida como una ubicación que utiliza el protocolo SSH. Git lanzará el proceso send-pack , con el que establece conexión SSH con tu servidor. En el servidor remoto, a través de una llamada SSH, intentará lanzar un comando tal como:

$ ssh -x [email protected] "git-receive-pack 'schacon/simplegit-progit.git'"
005bca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status delete-refs
003e085bb3bcb608e1e84b2432f8ecbe6306e7e7 refs/heads/topic
0000

El comando git-receive-pack responde con una linea por cada una de las referencias que tenga, — en este caso, la rama master y su SHA —. La primera linea suele indicar también una lista con las capacidades del servidor, (en este caso: report-status, dar situación, y delete-refs, borrar referencias).

Cada linea comienza con un valor de 4 bytes, en hexadecimal, indicando la longitud del resto de la linea. La primera de las lineas comienza con 005b, que es 91 en decimal, indicandonos que hay 91 bytes más en esa línea. La siguiente línea comienza con 003e, que es 62 en decimal, por lo que has de leer otros 62 bytes hasta el final de la linea. Y la última linea comienza con 0000, indicando así que la lista de referencias ha terminado.

Con esta información, el proceso send-pack ya puede determnar las confirmaciones de cambios (commits) presentes en el servidor. Para cada una de las referencias que se van a actualizar, el proceso send-pack llama al proceso receive-pack con la información pertinente. Por ejemplo, si estás actualizando la rama master y añadiendo otra rama experiment, la respuesta del proceso send-pack será algo así como:

0085ca82a6dff817ec66f44342007202690a93763949  15027957951b64cf874c3557a0f3547bd83b3ff6 refs/heads/master report-status
00670000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d refs/heads/experiment
0000

Una clave SHA-1 con todo ceros, nos indica que no había nada anteriormente, y que, por tanto, estamos añadiendo una nueva referencia. Si estuvieras borrando una referencia existente, verias lo contrario: una clave todo ceros en el lado derecho.

Git envia una linea por cada referencia a actualizar, indicando el viejo SHA, el nuevo SHA y la referencia a actualizar. La primera linea indica también las capacidades disponibles en el cliente. A continuación, el cliente envia un archivo empaquetado con todos los objetos que faltan en el servidor. Y, por ultimo, el servidor responde con un indicador de éxito (o fracaso) de la operación:

000Aunpack ok

9.6.2.2. Descargando datos

Cuando descargas datos, los procesos que se ven envueltos son fetch-pack (recuperar paquete) y upload-pack (enviar paquete). El cliente arranca un proceso fetch-pack, para conectar con un proceso upload-pack en el lado servidor y negociar con él los datos a transferir.

Hay varias maneras de iniciar un proceso upload-pack en el repositorio remoto. Se puede lanzar a través de SSH, de la misma forma que se arrancaba el proceso receive-pack. O se puede arrancar a traves del demonio Git, que suele estar escuchando por el puerto 9418. Tras la conexión, el proceso fetch-pack envia datos de una forma parecida a esta:

003fgit-upload-pack schacon/simplegit-progit.git\0host=myserver.com\0

Como siempre, comienza con cuatro bytes indicadores de cuantos datos siguen a continuación, siguiendo con el comando a lanzar, y terminando con un byte nulo, el nombre del servidor y otro byte nulo más. El demonio Git realizará las comprobaciones de si el comando se puede lanzar, si el repositorio existe y si tenemos permisos. Siendo todo correcto, el demonio lanzará el proceso upload-pack y procesara nuestra petición.

Si en lugar de utilizar el demonio Git, estás utilizando el protocolo SSH. fetch-pack lanzará algo como esto:

$ ssh -x [email protected] "git-upload-pack 'schacon/simplegit-progit.git'"

En cualquier caso, después de establecer conexión, upload-pack responderá:

0088ca82a6dff817ec66f44342007202690a93763949 HEAD\0multi_ack thin-pack \
  side-band side-band-64k ofs-delta shallow no-progress include-tag
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
003e085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 refs/heads/topic
0000

La respuesta es muy similar a la dada por receive-pack, pero las capacidades que se indican son diferentes. Además, nos indica la referencia HEAD, para que el cliente pueda saber qué ha de activar (check out) en el caso de estar requiriendo un clon.

En este punto, el proceso fetch-pack revisa los objetos que tiene y responde indicando los objetos que necesita. Enviando "want" (quiero) y la clave SHA que necesita. Los objetos que ya tiene, los envía con "have" (tengo) y la correspondiente clave SHA. Llegando al final de la lista, escribe "done" (hecho). Para indicar al proceso upload-pack que ya puede comenzar a enviar el archivo empaquetado con los datos requeridos:

0054want ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0000
0009done

Este es un caso muy sencillo para ilustrar los protocolos de trasferencia. En casos más complejos, el cliente explota las capacidades de multi_ack (multiples confirmaciones) o side-band (banda lateral). Pero este ejemplo muestra los intercambios básicos empleados en los protocolos inteligentes.