Git умеет передавать данные между репозиториями двумя способами: используя «глупый» и «умный» протоколы. В этой главе мы рассмотрим, как они работают.
Если вы разрешили доступ на чтение к вашему репозиторию через HTTP, то скорее всего будет использован «глупый» протокол.
Протокол назвали «глупым», потому что для его работы не требуется выполнение специфичных для Git операций на стороне сервера: весь процесс получения данных представляет собой серию HTTP GET
запросов, при этом клиент ожидает наличия на сервере структуры каталогов аналогичной Git репозиторию.
Note
|
Глупый протокол довольно редко используется в наши дни. При использовании глупого протокола сложно обеспечить безопасность передачи и приватность данных, поэтому большинство 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 <[email protected]> 1205815931 -0700
committer Scott Chacon <[email protected]> 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
на сервере.
Допустим, вы выполняете 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, но установка соединения слегка отличается. Всё начинается с такого запроса:
=> 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, 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 запроса.
Первый — это 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.