Git 🌙
Chapters ▾ 2nd Edition

10.6 Los entresijos internos de Git - Protocolos de transferencia

Protocolos de transferencia

Git puede transferir datos entre dos repositorios utilizando uno de sus dos principales mecanismos de transporte: sobre protocolo 'tonto', o sobre protocolo 'inteligente'. En esta parte, se verán sucintamente cómo trabajan esos dos tipos de protocolo.

El protocolo tonto

Si vas a configurar un repositorio para ser servido en forma de sólo lectura a través de HTTP, es probable que uses el protocolo tonto. Este protocolo se llama 'tonto' porque no requiere ningún tipo de código Git en la parte servidor durante el proceso de transporte; 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.

Nota

El protocolo tonto es muy poco usado hoy en día. Es difícil dar confidencialidad, por lo que la mayoría de los servidores Git (tanto los basados en la nube como los normales) se negarán a usarlo. Por lo general se recomienda utilizar el protocolo inteligente, que se describe un poco más adelante.

Vamos a revisar el proceso http-fetch para una librería simple de Git:

$ git clone http://server/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 ahí, ya tienes una lista de las referencias remotas y sus SHA-1s. Lo siguiente es mirar cual 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 recuperándola:

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

Cuando recuperas un objeto, dicho objeto se encuentra suelto (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 <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 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 alternativas, 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, que 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 comprobación es sobre todo útil en aquellos casos donde existan múltiples 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 SHA-1s de los objetos dentro del empaquetado y las ubicaciones -offsets- de cada uno de ellos 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 éstas 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.

El protocolo inteligente

El protocolo tonto es simple pero ineficiente, y no puede manejar la escritura de datos desde el cliente al servidor. El protocolo inteligente es un método mucho más común de transmisión de datos, pero requiere un proceso en el lado remoto que es inteligente acerca de Git --puede leer datos localmente, determinar lo que el cliente tiene y necesita, y generar un empaquetado expresamente para él--. Existen dos conjuntos de procesos para transferir datos: uno para enviar y otro para recibir.

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.

SSH

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 git@server "git-receive-pack 'simplegit-progit.git'"
005bca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status \
	delete-refs side-band-64k quiet ofs-delta \
	agent=git/2:2.1.1+github-607-gfba4028 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-1--. La primera línea también tiene una lista de las capacidades del servidor (en este caso, report-status, delete-refs, y algunas otras, incluyendo el identificador del cliente).

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

Con esta información, el proceso send-pack ya puede determinar las confirmaciones de cambios (commits) no 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

Git envía una línea por cada referencia a actualizar, indicando la longitud de la línea, el viejo SHA-1, el nuevo SHA-1, y la referencia a actualizar. La primera línea indica también las capacidades disponibles en el cliente. Una clave SHA-1 con todo '0’s, nos indica que no había nada anteriormente, y que, por tanto, estamos añadiendo una nueva referencia. Si estuvieras borrando una referencia existente, verías lo contrario: una clave todo '0’s en el lado derecho.

A continuación, el cliente envía 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
HTTP(S)

Este proceso es, en general, el mismo con HTTP, aunque la negociación (handshaking) es un poco diferente. La conexión se inicia con esta solicitud:

=> GET http://server/simplegit-progit.git/info/refs?service=git-receive-pack
001f# service=git-receive-pack
000000ab6c5f0e45abd7832bf23074a333f739977c9e8188 refs/heads/master \
	report-status delete-refs side-band-64k quiet ofs-delta \
	agent=git/2:2.1.1~vmg-bitmaps-bugaloo-608-g116744e
0000

Este es el final del primer intercambio cliente-servidor. El cliente, entonces, realiza otra petición, esta vez un POST, con los datos que proporciona git-upload-pack.

=> POST http://server/simplegit-progit.git/git-receive-pack

La solicitud POST incluye la salida de send-pack y el archivo empaquetado como su carga útil. Después, el servidor indica el éxito o el fracaso con su respuesta HTTP.

Recibiendo datos (downloading)

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.

SSH

Si realizas la recuperación (fetch) sobre SSH, entonces fetch-pack ejecuta algo como:

$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"

Después de establecer conexión, upload-pack responderá:

00dfca82a6dff817ec66f44342007202690a93763949 HEADmulti_ack thin-pack \
	side-band side-band-64k ofs-delta shallow no-progress include-tag \
	multi_ack_detailed symref=HEAD:refs/heads/master \
	agent=git/2:2.1.1+github-607-gfba4028
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
0000

La respuesta es muy similar a la dada por receive-pack, pero las capacidades que se indican son diferentes. Además, nos indica a qué apunta HEAD (symref=HEAD:refs/heads/master) 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-1 que necesita. Los objetos que ya tiene, los envía con 'have' (tengo) y la correspondiente clave SHA-1. 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
HTTP(S)

La negociación (handshake) para una operación de recuperación (fetch) requiere dos peticiones HTTP. La primera es un GET al mismo destino usado en el protocolo tonto:

=> GET $GIT_URL/info/refs?service=git-upload-pack
001e# service=git-upload-pack
000000e7ca82a6dff817ec66f44342007202690a93763949 HEADmulti_ack thin-pack \
	side-band side-band-64k ofs-delta shallow no-progress include-tag \
	multi_ack_detailed no-done symref=HEAD:refs/heads/master \
	agent=git/2:2.1.1+github-607-gfba4028
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
0000

Esto es muy parecido a invocar git-upload-pack sobre una conexión SSH, pero el segundo intercambio es realizado como una petición separada:

=> POST $GIT_URL/git-upload-pack HTTP/1.0
0032want 0a53e9ddeaddad63ad106860237bbf53411d11a7
0032have 441b40d833fdfa93eb2908e52742248faf0ee993
0000

De nuevo, este es el mismo formato visto más arriba. La respuesta a esta petición será éxito o fallo, e incluirá el empaquetado.

Resumen

Esta sección contiene una descripción muy básica de los protocolos de transferencia. El protocolo incluye muchas otras características, como las capacidades multi_ack o side-band, pero su tratamiento está fuera del alcance de este libro. Hemos tratado de darte una panorámica de la comunicación entre cliente y servidor; si necesitas profundizar en esto, es probable que desees echar un vistazo al código fuente de Git.

scroll-to-top