Git 🌙
Chapters ▾ 2nd Edition

10.6 Git изнутри - Протоколы передачи данных

Протоколы передачи данных

Git умеет передавать данные между репозиториями двумя способами: используя «глупый» и «умный» протоколы. В этой главе мы рассмотрим, как они работают.

Глупый протокол

Если вы разрешили доступ на чтение к вашему репозиторию через HTTP, то скорее всего будет использован «глупый» протокол. Протокол назвали «глупым», потому что для его работы не требуется выполнение специфичных для Git операций на стороне сервера: весь процесс получения данных представляет собой серию HTTP GET запросов, при этом клиент ожидает наличия на сервере структуры каталогов аналогичной Git репозиторию.

Примечание

Глупый протокол довольно редко используется в наши дни. При использовании глупого протокола сложно обеспечить безопасность передачи и приватность данных, поэтому большинство Git серверов (как облачных, так и тех, что требуют установки) откажутся работать через него. Рекомендуется использовать умный протокол, который мы рассмотрим далее.

Давайте рассмотрим процесс получения данных из репозитория simplegit-progit:

$ git clone http://server/simplegit-progit.git

Первым делом будет загружен файл info/refs. Данный файл записывается командой update-server-info, поэтому для корректной работы HTTP-транспорта необходимо выполнять её в post-receive триггере.

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

Теперь у нас имеется список удалённых веток и их хеши. Далее, надо посмотреть, куда ссылается HEAD, чтобы знать на что переключиться после завершения работы команды.

=> GET HEAD
ref: refs/heads/master

Итак, нужно переключится на ветку master после окончания работы. На данном этапе можно начинать обход репозитория. Начальной точкой является коммит ca82a6, о чём мы узнали из файла info/refs, поэтому мы начинаем с его загрузки:

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

Объект получен, он был в рыхлом формате на сервере, и мы получили его по HTTP, используя GET-запрос. Теперь можно его разархивировать, обрезать заголовок и посмотреть на содержимое:

$ 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

Change version number

Далее, необходимо загрузить ещё два объекта: дерево cfda3b — содержимое только что загруженного коммита, и 085bb3 — родительский коммит:

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

Вот мы и получили следующий объект коммита. Теперь получим содержимое коммита:

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

Упс, похоже, этого дерева нет на сервере в рыхлом формате, поэтому мы получили ответ 404. Возможны два варианта: объект в другом репозитории или в упакованном файле текущего репозитория. Сначала Git проверяет список альтернативных репозиториев:

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

Если бы этот запрос вернул непустой список альтернатив, Git проверил бы указанные репозитории на наличие файла в «рыхлом» формате — довольно полезная возможность для проектов-форков, позволяющая устранить дублирование объектов на диске. Так как в данном случае альтернатив нет, объект должен быть упакован в pack-файле. Чтобы посмотреть доступные на сервере pack-файлы, нужно скачать файл objects/info/packs, содержащий их список (также генерируется командой update-server-info):

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

На сервере имеется только один pack-файл, поэтому объект точно там, но необходимо проверить индексный файл, чтобы в этом убедиться. Если бы на сервере было несколько pack-файлов, загрузив сначала индексы, мы смогли бы определить, в каком именно pack-файле находится нужный нам объект:

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

Так как в индексе содержится список SHA-1 хешей объектов и соответствующих им смещений объектов внутри pack-файла, то можно проверить наличие объекта в этом pack-файле. Наш объект там присутствует, так что продолжим и скачаем весь pack-файл:

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

Итак, мы получили наше дерево, можно продолжить обход списка коммитов. Все они содержатся внутри только что скачанного pack-файла, так что снова обращаться к серверу не надо. Git извлекает рабочую копию ветки master, так как на неё указывает ссылка HEAD, которая была скачана в самом начале.

Умный протокол

Глупый протокол прост, но неэффективен и не позволяет производить запись в удалённые репозитории. Гораздо чаще для обмена данными используют «умный» протокол, но это требует наличия на сервере специального процесса, знающего о структуре Git репозитория, умеющего выяснять, какие данные необходимо отправить клиенту и генерирующего отдельный pack-файл с недостающими изменениями для него. Работу умного протокола обеспечивают несколько процессов: два для отправки данных на сервер и два для загрузки с него.

Загрузка данных на сервер

Для загрузки данных на удалённый сервер используются процессы send-pack и receive-pack. Процесс send-pack запускается на клиенте и подключается к receive-pack на сервере.

SSH

Допустим, вы выполняете git push origin master и origin задан как URL, использующий протокол SSH. Git запускает процесс send-pack, который устанавливает соединение с сервером по протоколу SSH. Он пытается запустить команду на удалённом сервере через вызов SSH команды, который выглядит следующим образом:

$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"
00a5ca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status \
	delete-refs side-band-64k quiet ofs-delta \
	agent=git/2:2.1.1+github-607-gfba4028 delete-refs
0000

Команда git-receive-pack тут же посылает в ответ по одной строке на каждую из имеющихся в наличии ссылок — в данном случае только ветку master и её SHA-1. Первая строка также содержит список возможностей сервера (здесь это report-status, delete-refs и парочка других, включая идентификатор клиента).

Данные передаются пакетами. Каждый пакет начинается с 4-байтового шестнадцатеричного значения, определяющего его размер (включая эти 4 байта). Пакеты обычно содержат одну строку данных и завершающий символ переноса строки. Первый пакет начинается с 00a5, что в десятичной системе равно 165 и означает, что размер пакета составляет 165 байт. Следующий пакет начинается с 0000, что говорит об окончании передачи списка ссылок сервером.

Теперь, когда send-pack выяснил состояние сервера, он определяет коммиты, которые есть локально, но отсутствуют на сервере. Эту информацию процесс send-pack передаёт процессу receive-pack по каждой ссылке, которая подлежит отправке. Например, если мы обновляем ветку master и добавляем ветку experiment, ответ send-pack будет выглядеть следующим образом:

0076ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 \
	refs/heads/master report-status
006c0000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d \
	refs/heads/experiment
0000

Для каждой обновляемой ссылки Git посылает по строке, содержащей собственную длину, старый хеш, новый хеш и имя ссылки. В первой строке также посылаются возможности клиента. Хеш, состоящий из нулей, говорит о том, что раньше такой ссылки не было — вы ведь добавляете новую ветку experiment. При удалении ветки всё было бы наоборот: нули были бы справа.

Затем клиент посылает pack-файл c объектами, которых нет на сервере. Наконец, сервер передаёт статус операции — успех или ошибка:

000eunpack ok
HTTP(S)

Этот процесс похож на HTTP, но установка соединения слегка отличается. Всё начинается с такого запроса:

=> 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

Это всё, что передаётся в ответ на первый запрос. Затем клиент делает второй запрос, на этот раз POST, передавая данные, полученные от команды git-upload-pack.

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

Этот запрос включает в себя результаты send-pack и собственно pack-файлы. Сервер, используя код состояния HTTP, возвращает результат операции.

Имейте ввиду, что HTTP протокол может дополнительно кодировать данные внутри каждого пакета.

Скачивание данных

Для получения данных из удалённых репозиториев используются процессы fetch-pack и upload-pack. Клиент запускает процесс fetch-pack, который подключается к процессу upload-pack на сервере для определения подлежащих передаче данных.

SSH

Если вы работаете через SSH, fetch-pack выполняет примерно такую команду:

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

Как только fetch-pack подключается к upload-pack, тот отсылает обратно следующее:

00dfca82a6dff817ec66f44342007202690a93763949 HEAD\0multi_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
003fe2409a098dc3e53539a9028a94b6224db9d6a6b6 refs/heads/master
0000

Это очень похоже на ответ receive-pack, но только возможности другие. Вдобавок upload-pack отсылает обратно ссылку HEAD (symref=HEAD:refs/heads/master), чтобы клиент понимал, на какую ветку переключиться, если выполняется клонирование.

На данном этапе процесс fetch-pack смотрит на имеющиеся в наличии объекты, а для недостающих объектов отвечает словом «want» с указанием SHA-1 необходимого объекта. Для каждого из имеющихся объектов процесс отправляет слово «have» с указанием SHA-1 объекта. В конце списка он пишет «done», что указывает процессу upload-pack начать отправлять pack-файл с необходимыми данными:

003cwant ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0009done
0000
HTTP(S)

«Рукопожатие» для процесса получения недостающих данных занимает два HTTP запроса. Первый — это GET запрос на тот же URL, что и в случае глупого протокола:

=> GET $GIT_URL/info/refs?service=git-upload-pack
001e# service=git-upload-pack
00e7ca82a6dff817ec66f44342007202690a93763949 HEAD\0multi_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

Это очень похоже на использование git-upload-pack по SSH, вот только обмен данными производится отдельным запросом:

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

Используется тот же формат, что и ранее. В ответ сервер посылает статус операции и сгенерированный pack-файл.

Заключение

В этом разделе мы вкратце рассмотрели протоколы передачи данных. Протоколы обмена данных в Git включают в себя множество возможностей, таких как multi_ack или side-band, но их рассмотрение выходит за пределы этой книги. Мы описали формат сообщений между клиентом и сервером не вдаваясь в детали, если хотите покопаться в этой теме глубже — обратитесь к исходному коду Git.

scroll-to-top