Одним из важным инструментом разработчика, в не зависимости от языка (и религиозных убеждений), является система контроля версий (VCS). И практически промышленным стандартом стала такая распределенная система как GIT. В повседневной работе мы (разработчики, DevOps инженеры, технические писатели и все причастные) используем ее чтобы нести людям добро и свет объединять усилия команд в работе над нашими проектами. И все давно уже выучили «на зубок» основные команды (если не выучили то бегом учить, тут есть отличная книжка) и превратили в рутину то что совсем недавно (олды тут?) казалось гениальным, сложным, а кому то магическим. А современные IDE еще больше нам упростили жизнь, спрятав от нас командную строку и git команды, заменив на возможность кликать мышкой. Но постойте, разве вам в детстве не было интересно понять как та или иная игрушка устроена внутри, как работает холодильник или мотор в папиных жигулях (олды все же тут?)? Так вот и мне стало интересно заглянуть под капот GITу. Конечно как и с любым сложным механизмом уровень этого «заглядывания» под капот может быть разным, кому то будет достаточно увидеть крышку мотора и отверстие куда лить незамерзайку, а кому то «заглянуть под капот» это дойти до марки стали используемой для изготовления той или иной детали жигулей. Поэтому давайте сразу обозначим уровень нашего погружения в этой статье. В статье мы рассмотрим в деталях что происходит когда мы делаем привычные нам «git clone/push», посмотрим как этот процесс устроен и какие есть в нем возможности. Сущности и процессы, которые конечно же останутся за рамками этого повествования, можно будет самостоятельно найти (ссылку я указал выше), ибо охватить такую обширную тему как Git, а тем более его подкапотное пространство, не представляется возможным за раз. Так что все кому интересно прошу под кат.

Итак, начнем с того, что заглядывать под капот мы будет по следующему плану:

  1. Какие типы протоколов есть в Git.

  2. Как происходит общение по протоколу ssh.

  3. Рассмотрим возможности протокола Git.

Типы протоколов GIT

Git умеет работать с четырьмя сетевыми протоколами для передачи данных: локальный, HTTP, Secure Shell (SSH) и Git. В этой части мы обсудим только протокол SSH и HTTP, оставшиеся являются "нишевыми" и широкого распространения не имеют, но пару слов для общего представления скажем.

Локальный протокол

Базовым протоколом является Локальный протокол, для которого удалённый репозиторий — это другой каталог на диске. И это все что мы о нем скажем, потому что по сути этот протокол является архаизмом. 

Протоколы HTTP

"Умный" HTTP

«Умный» протокол HTTP работает поверх стандартных HTTP/S портов и может использовать различные механизмы аутентификации HTTP, это часто проще для пользователя, чем что-то вроде SSH, так как можно использовать аутентификацию по логину/паролю вместо установки SSH-ключей. Наверное, сейчас он стал наиболее популярным способом использования Git, так как может использоваться и для анонимного доступа (если это разрешено конечно), и для отправки изменений с аутентификацией и шифрованием как протокол SSH. Вместо использования разных адресов URL для этих целей, можно использовать один URL адрес для всего. Если вы пытаетесь отослать изменения и репозиторий требует аутентификации (обычно так и есть), сервер может спросить логин и пароль. То же касается и доступа на чтение.

"Тупой" HTTP

Если сервер не отвечает на умный запрос Git по HTTP, клиент Git попытается откатиться на более простой Тупой HTTP-протокол. Тупой протокол ожидает, что голый (ой..) репозиторий Git будет обслуживаться веб-сервером как набор файлов. Прелесть тупого протокола HTTP — в простоте настройки. По сути, всё, что необходимо сделать — поместить голый репозиторий в корневой каталог любого веб-сервера, умеющего раздавать статику по HTTP/S. Теперь каждый может клонировать репозиторий, если имеет доступ к веб-серверу, на котором он был размещен. По сути такой протокол предназначен только для чтения, но в теории в этой одной задаче он может быть быстрее для высоко нагруженных git хостингов.

Git-протокол

Следующий протокол — Git-протокол. Вместе с Git поставляется специальный демон, который слушает отдельный порт (9418) и предоставляет сервис, схожий с протоколом SSH, но абсолютно без аутентификации. Чтобы использовать Git-протокол для репозитория, вы должны создать файл в этом репозитории git-export-daemon-ok, иначе демон не будет работать с этим репозиторием. Соответственно, любой репозиторий в Git может быть либо доступен для клонирования всем, либо нет. Как следствие, обычно отправлять изменения по этому протоколу нельзя. Вы можете открыть доступ на запись, но из-за отсутствия аутентификации в этом случае кто угодно, зная URL вашего проекта, сможет его изменить. В общем, это редко используемая возможность.

Протокол SSH

Часто используемый транспортный протокол для самостоятельного хостинга Git ‑ это SSH. Причина этого в том, что доступ по SSH уже есть на многих серверах, а если его нет, то его очень легко настроить. К тому же, SSH ‑ протокол с аутентификацией, и благодаря его распространённости обычно легко настроить и использовать. Данные передаются зашифрованными по авторизованным каналам. Наконец, так же как и протоколы HTTP/S, Git и локальный протокол, SSH эффективен благодаря максимальному сжатию данных перед передачей.

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

Далее рассмотрим как происходит общение сервисов git по протоколу ssh, так как это общение точно такое же что и в случае HTTP.

Общение по протоколу SSH

Для общения по протоколу SSH чаще всего (нотариально заверенные 99,9%) используется «умный» протокол. «Умный» он потому что требует наличия на сервере специального процесса, знающего о структуре Git репозитория, умеющего выяснять, какие данные необходимо отправить клиенту и генерирующего отдельный pack‑файл с недостающими изменениями для него (тем самым оптимизируя пересылку данных). Работу умного протокола обеспечивают несколько процессов: два для отправки данных на сервер и два для загрузки с него.

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

Для загрузки данных на удалённый сервер используются процессы send-pack и receive-pack (на самом деле это команды утилиты git, но если вы «пошаритесь» у себя в системе, в которой установлен git, то найдете отдельный исполняемый файл: git-receive-pack). Процесс send-pack запускается на клиенте и подключается к receive-pack на сервере. И выполняется это без какой либо магии, а все лишь используя возможность протокола ssh для удаленного запуска команды на сервере. То есть например вы хотите сделать отправку ваших локальных изменений в репозитории в удаленный репозиторий, ваша команда «git push origin» под капотом выполнит следующий вызов: «$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"». Таким образом на стороне сервера произойдет запуск исполняемого файла git-receive-pack и начнется общение между двумя этими процессами по определенном протоколу, о котором мы поговорим далее.

Процесс общения между git клиентом и git сервером построен на довольно специфичном git протоколе. Данные передаются пакетами. Каждый пакет начинается с 4-байтового шестнадцатеричного значения, определяющего его размер (включая эти 4 байта). Пакеты обычно содержат одну строку данных и завершающий символ переноса строки. Рассмотрим пример (все имена вымышлены, а совпадения случайны):

$ 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

Первый пакет начинается с 00a5, что в десятичной системе равно 165 и означает, что размер пакета составляет 165 байт. Следующее значение в примере "ca82a6dff817ec66f4437202690a93763949" это SHA-1 коммита git объекта, а затем указана ссылка на него. После идет перечесление воможностей сервера, о них мы поговорим чуть позже (кстати в рамках этого протокола есть договоренность что возможности передаются только в первом пакете). Следующий пакет начинается с 0000, что говорит об окончании передачи списка ссылок сервером.

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

076ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 refs/heads/master report-status

006c0000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d refs/heads/experiment

0000

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

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

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

Для получения данных из удаленного репозитория используются вторая пара процессов: fetch-pack и upload-pack (аналогично тоже можно найти отдельный исполняемый файл git-upload-pack). И так же аналогично отправке ваша команда "git clone" под капотом выполнит: "$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"" и начнется процесс общения между этими процессами. Как только связь будет установлена то сервер отсылает клиенту следующее:

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

То есть используя всего две команды "want" и "have" клиент и сервер могут договориться о том какие данные нужны клиенту, а какие данные у него уже есть, и таким образом сервер сможет подготовить и отправить для него не все данные, а только недостающие, тем самым сильно оптимизируя обмен данными.

Возможности протокола

Выше мы упомянули о том что в первом сообщении как при загрузке так и при скачивании данных передаются возможности которые поддерживает сервер и которые клиент может использовать для получения необходимого результата. Давайте рассмотрим их по ближе.

Напомним пример первого сообщения от сервера при скачивании данных:

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

В этом сообщении сервер перечисляет те возможности которые он поддерживает. Клиент прочитав такое сообщение в ответ может прислать сообщение со списком тех возможностей заявленных сервером, которые он хочет использовать. Рассмотрим указанные в примере возможности:

multi_ack - Данная возможность позволяет в процессе общения клиента и сервера оптимизировать процесс нахождения базового коммита (базовый коммит это коммит который есть как у клиента так и сервера и от которого можно достигнуть другого коммита или коммитов по дереву объектов). Например у нас есть следующее дерево git объектов (Зеленым изображены коммиты, которые есть на сервере, синим коммиты которые есть у клиента):

Дерево git объектов
Дерево git объектов

Допустим что клиент хочет получить коммиты X,Y (и соответственно недостающие предшествующие им коммиты). Клиент в процессе общения "говорит" - "have F,S" (на самом деле двумя сообщениями, но для краткости мы объединили в одно сообщение), но сервер про эти объекты ни чего не знает, так как они есть только у клиента. Тогда клиента "говорит" - "have E,R", про которые сервер так же не знает. Так процесс продолжается пока клиент не "скажет" - "have D", коммит D есть у сервера и сервер отвечает "ACK D continue", тем самым сообщая клиенту что "базовый" коммита найден (коммит D есть у клиента и у сервера, и из него достижим изначально запрошенный коммит Y) и что клиенту не нужно далее запрашивать коммиты этой ветви дерева CBA. Но так как клиент еще хочет коммит X то аналогичным образом происходит запрос для поиска "базового" коммита по ветви SRQ. Если бы сервер не поддерживал возможность multi_ack то потребовалось бы передавать CBA для поиска общего "базового" коммита для XY.

multi_ack_detailed - об этой возможности максимум что удалось выяснить что она позволяет серверу отвечать более детализировано, но в чем эта детализация заключается и на что она влияет не понятно.

thin-pack - возможность сервера и клиента принимать и обрабатывать "тонкие" пакеты. Git хранит данные в pack файле (в некоторых случая это может быть несколько файлов, но которые всегда можно объединить в один), который содержит в себе всю информацию обо всех git объектах. Но в целях оптимизации объема передаваемых данных между клиентом и сервером можно передавать не все объекты каждый раз, а лишь только те которых нет на принимающей стороне, клиенте или сервере. Такой пакет и называется "тонким". Но при этом нужно убедиться что сервер может отправлять "тонкие" pack файлы, а клиент готов их обрабатывать. Данная возможность основа скорости и эффективности работы Git.

side-band side-band-64k - это возможность позволяет серверу отправлять, а клиенту принимать данные с использование мультиплексирования данных, передаваемого pack файла, данных прогресса выполнения и данных об ошибках. Суть в том что, как уже мы говорили выше, данные передаются пакетами, в случае side-band размер пакета равен 1000 байт и 65520 байт в случае side-band-64k (сейчас конечно используется только side-band-64k). Как мы помним каждый пакет начинается с с 4-байтового шестнадцатеричного значения, определяющего его размер (включая эти 4 байта), далее следует 1 байт обозначающий код потока, и далее все оставшееся место в пакете занимают данные. Код потока как раз позволяет указать тип передаваемых данных.

Код

Тип данных

1

Данные pack файла

2

Данные прогресса выполнения операции

3

Данные ошибок

Благодаря этой возможности мы и видим процесс обработки нашей команды clone или push, промежуточные сообщения, а так же ошибки и предупреждения.

ofs-delta - эта возможность говорит о том что сервер и клиент могут использовать формат pack v2, который позволяет передавать и принимать объекты по позиции в pack файле, а не по идентификатору объекта, что позволяет повысить скорость обработки.

shallow - с помощью этой возможности клиент может, используя дополнительные команды "deepen", "shallow" and "unshallow" получать не полностью весь репозиторий, а только какое то ограниченное количество, срез на какою то глубину коммитом, или на дату. Именно эта возможность сервера используется клиентом когда мы в команде "git clone" указываем например флаг "–depth 1", что позволит нам получить репозиторий с состоянием на последний коммит нужной ветки. Тем самым не выкачивая полностью всю историю репозитория, что может быть очень полезным в различных CI/CD где как раз как правило нужно лишь последнее состояние какой то конкретной ветки проекта и можно сильно сократить объем скачиваемых данных и увеличить скорость например сборки и выкатки приложения на стенд.

no-progress - возможность по желанию клиента отключить получение типа данных с кодом 2 (выше в side-band side-band-64k мы говорили об этом типе). Когда мы запуская команду "git clone" с помощью флага "-q или --quiet" можем отказаться от получения сообщений о прогрессе выполнения команды, клиент отправит соответствующую команду серверу.

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

symref=HEAD:refs/heads/master - с помощью этой возможности сервер сообщает клиенту на какую ссылку ссылается специальный указатель HEAD, и эту информацию использует клиент что бы понять на какую ветку по умолчанию нужно переключиться при операции "git clone".

agent - сервер с помощью этой возможности передает свою версию git, на что в ответ клиент может передать свою. Но на что либо обмен этой информации не влияет, а используется только исключительно для вывода в лог для возможного использования этих данных в целях отладки. 

Заключение

В этой скромной статье нам удалось заглянуть под капот Git, в части общения по протоколу git между процессами клиента и сервера, происходящего когда мы хотим получить данные и когда хотим отправить свои изменения в удаленный репозиторий. При том что мы заглянули и рассмотрели не все аспекты, нюансы и сущности большого внутреннего мира Git, о чем я вас в самом начале предупредил. Но как говорится большое начинается с малого и осилит дорогу идущий. Настоятельно советую прочесть книгу о Git если вы им пользуетесь, а я уверен пользуетесь, иначе вы бы не дочитали до этих строк. Не поленюсь вставить еще раз ссылку на книгу.

Спасибо за внимание!

Комментарии (4)


  1. Daniel217D
    22.06.2024 09:00
    +1

    Что за "умный" и "тупой" http? Первый раз о таком слышу


    1. artptr86
      22.06.2024 09:00
      +2

      https://git-scm.com/docs/gitprotocol-http

      Git supports two HTTP based transfer protocols. A "dumb" protocol which requires only a standard HTTP server on the server end of the connection, and a "smart" protocol which requires a Git aware CGI (or server module).


  1. AnROm
    22.06.2024 09:00
    +2

    Статья - почти полная копия интернет-ресурсов. У вас оригинальная статья или перевод?

    https://github.com/pawk/git-advanced/blob/master/protocols.md


  1. brozes
    22.06.2024 09:00
    +5

    Внесу свои пять килограмм боли с умным протоколом.

    Протокол настолько "умный", что в отличии от старого "тупого" - не умеет даже в банальную докачку, в частности тех самых pack файлов, размер которых часто превышает сотни мбайт, а старый протокол тот же github например у себя отключил.

    И если у вас вдруг интернет нестабильный или медленный, или git клиент используется через vpn/proxy - то можно реально посидеть, прежде чем склонируешь репу, ибо любой разрыв или потеря пары tcp пакетов приводит к rpc error xx, и нужно начинать все сначала или извращаться с --depth, т.к протоколом даже не предусмотрено восстановление загрузки.

    В range bytes как оказывается с "умным" протоколом не умеет ни сервер, ни клиент, и они только планируют это добавить в будущем.