Git 🌙
Chapters ▾ 2nd Edition

10.6 Mechanizmy wewnętrzne w Git - Protokoły transferu

Protokoły transferu

Git może przesyłać dane pomiędzy dwoma repozytoriami na dwa główne sposoby: wykorzystując protokół "prosty" (dumb) lub protokół "inteligentny" (smart). W tej sekcji pokrótce omówimy jak działają te dwa główne protokoły.

Protokół prosty

Jeśli konfigurujesz repozytorium, które ma być obsługiwane tylko do odczytu przez HTTP, prawdopodobnie użyty zostanie "prosty" protokół. Protokół ten nazywany jest "prostym", ponieważ nie wymaga żadnego specyficznego dla Git kodu po stronie serwera podczas procesu transportu danych; proces pobierania danych jest serią żądań HTTP GET, gdzie klient może przyjąć pewną strukturę repozytorium Git umieszczonego na serwerze.

Note

Protokół "prosty" jest obecnie dość rzadko używany. Trudno go zabezpieczyć lub uczynić prywatnym, więc większość hostów Git (zarówno w chmurze, jak i w instancjach lokalnych) odmówi jego użycia. Ogólnie zaleca się używanie protokołu "inteligentnego", który opisujemy nieco dalej.

Prześledźmy proces http-fetch dla biblioteki simplegit:

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

Pierwszą rzeczą jaką wykonuje ta komenda, jest pobranie pliku info/refs. Plik ten jest zapisywany przez komendę update-server-info, dlatego też musisz włączyć komendę post-receive, aby przesyłanie danych przez HTTP działało poprawnie:

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

Masz teraz listę zdalnych referencji oraz ich sumy SHA-1. Następnie sprawdzasz co znajduje się w HEAD, tak aby było wiadomo jaką gałąź pobrać po zakończeniu:

=> GET HEAD
ref: refs/heads/master

Musisz pobrać gałąź master po ukończeniu całego procesu. W tym momencie możesz rozpocząć proces odnajdowania struktury repozytorium. Elementem początkowym jest commit ca82a6, który zobaczyłeś w pliku info/refs, pobierz go jako pierwszego:

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

Otrzymujesz w odpowiedzi obiekt – pobrany z serwera obiekt jest w luźnym formacie i został pobrany poprzez zapytanie HTTP GET. Możesz rozpakować ten plik, usunąć nagłówki i odczytać jego zawartość:

$ 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

W następnej kolejności masz dwa obiekty do pobrania – cfda3b, który jest obiektem tree z zawartością na którą wskazuje pobrany commit; oraz 085bb3, który jest poprzednim commitem:

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

Otrzymałeś więc kolejny obiekt commit. Pobierz zawartość obiektu tree:

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

Ojej – wygląda na to, że obiekt tree nie jest w luźnym formacie na serwerze, dlatego otrzymałeś odpowiedź 404. Przyczyn takiego stanu rzeczy może być kilka – obiekt może być w alternatywnym repozytorium, lub może być w pliku packfile w tym samym repozytorium. Git najpierw sprawdza czy są jakieś alternatywne repozytoria dodane:

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

Jeżeli zwrócona zostanie lista alternatywnych adresów URL, Git sprawdzi czy istnieją w nich szukane pliki w luźnym formacie lub spakowane pliki packfile – jest to bardzo fajny mechanizm umożliwiający współdzielenie plików dla projektów które rozwidlają się (ang. fork) jeden od drugiego. Jednak, ze względu na to, że nie ma żadnych alternatywnych plików w tym przykładzie, szukany obiekt musi być w spakowanym pliku packfile. Aby zobaczyć jakie pliki packfile są dostępne na serwerze, musisz pobrać plik objects/info/packs zawierający ich listę (ten plik jest również tworzony przez update-server-info):

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

Jest tylko jeden plik packfile na serwerze, więc szukany obiekt jest na pewno w nim, sprawdź jednak plik indeks aby mieć pewność. Jest to również przydatne, gdy masz wiele plików packfile na serwerze, tak abyś mógł zobaczyć który z nich zawiera obiekt którego szukasz:

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

Teraz, gdy pobrałeś już indeks pliku packfile, możesz zobaczyć jakie obiekty się w nim znajdują – ponieważ zawiera on listę sum SHA-1 obiektów oraz informacje o tym w którym miejscu w pliku packfile ten obiekt się znajduje. Twój obiekt w nim jest, pobierz więc cały plik packfile:

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

Masz już obiekt tree, możesz więc kontynuować przechodzenie przez wszystkie zmiany. Wszystkie one zawarte są również w pliku packfile który właśnie pobrałeś, nie musisz więc wykonywać żadnych dodatkowych zapytań do serwera. Git pobierze kopię roboczą z gałęzi master, na którą wskazywała referencja pobrana z HEAD na początku całego procesu.

Protokół inteligentny

Protokół "prosty" jest nomen omen prosty, ale trochę nieefektywny i nie radzi sobie z zapisem danych od klienta do serwera. Protokół "inteligentny" jest bardziej powszechną metodą przesyłania danych, ale wymaga on procesu na serwerze zdalnym, który rozumnie ogarnia Gita – może on odczytywać lokalne dane, oraz może wygenerować dane dla konkretnego klienta na podstawie tego jakie informacje on już posiada. Są dwa rodzaje procesów do przesyłania danych: para procesów do wgrywania danych, oraz para do pobierania.

Wgrywanie danych

Aby wgrać dane do zdalnego repozytorium, Git używa procesów send-pack oraz receive-pack. Proces send-pack uruchomiony jest po stronie klienta i łączy się do procesu receive-pack uruchomionego na zdalnym serwerze.

SSH

Na przykład, załóżmy że uruchamiasz git push origin master w swoim projekcie, a origin jest zdefiniowany jako URL używający protokołu SSH. Git uruchamia proces send-pack, który zainicjuje połączenie przez SSH do Twojego serwera. Stara się on uruchomić komendę na zdalnym serwerze przez SSH, podobną do poniższej:

$ 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

Komenda git-receive-pack od razu odpowiada jedną linią dla każdej referencji którą aktualnie zawiera – w tym przypadku, tylko gałąź master oraz jej SHA-1. Pierwsza linia zawiera również listę funkcji serwera (tutaj report-status, delete-refs i inne, włączając w to identyfikator klienta).

Każda linia rozpoczyna się 4-znakową liczbą szesnastkową wskazującą na to, jak długa jest reszta linii. Pierwsza linia rozpoczyna się 005b, co daje 91 w systemie dziesiętnym, co oznacza że 91 bajtów pozostało w tej linii. Następna linia rozpoczyna się od 003e, czyli 62 w systemie dziesiętnym, odczytujesz więc pozostałe 62 bajty. Kolejna linia to 0000, oznaczająca że serwer zakończył listowanie referencji.

Teraz, gdy zna on już stan który jest na serwerze, Twój proces send-pack ustala które z posiadanych commitów nie istnieją na serwerze. Dla każdej referencji która zostanie zaktualizowana podczas tego pusha, proces send-pack przekazuje receive-pack te informacje. Na przykład, jeżeli aktualizujesz gałąź master oraz dodajesz gałąź experiment, odpowiedź send-pack może wyglądać tak:

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

Git wysyła wiersz dla każdej aktualizowanej referencji z długością wiersza, starym SHA-1, nowym SHA-1 i referencją, która jest aktualizowana. Pierwsza linia zawiera również parametry klienta. Wartość SHA-1 składająca się z samych 0 oznacza że nic nie było wcześniej – ponieważ dodajesz referencję experiment. Jeżeli usuwasz referencję, zobaczyć sytuację odwrotną: same zera po prawej stronie.

Następnie klient wysyła packfile zawierający wszystkie obiekty, których serwer jeszcze nie posiada. Na koniec serwer odpowiada z informacją o powodzeniu (lub niepowodzeniu):

000Aunpack ok
HTTP(S)

Proces ten jest w większości taki sam jak w przypadku protokołu HTTP, choć sposób przekazywania danych jest nieco inny. Połączenie jest inicjowane przez poniższe żądanie:

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

Jest to koniec pierwszej wymiany danych pomiędzy klientem a serwerem. Następnie klient wykonuje kolejne żądanie, tym razem POST, z danymi, które dostarcza git-upload-pack.

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

Żądanie POST zawiera wyjście send-pack i plik packfile jako jego payload. Serwer następnie wskazuje swoją odpowiedzią HTTP sukces lub niepowodzenie.

Pobieranie danych

Podczas pobierania danych, procesy fetch-pack oraz upload-pack są używane. Po stronie klienta uruchamiany jest proces fetch-pack, łączący się do upload-pack na drugim końcu, w celu ustalenia które dane mają być pobrane.

SSH

Jeśli wykonujesz pobieranie przez SSH, fetch-pack uruchamia coś takiego:

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

Po połączeniu się z fetch-pack, upload-pack odsyła coś w tym stylu:

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

Jest to bardzo podobna odpowiedź to tej którą zwrócił receive-pack, ale z innymi obsługiwanymi funkcjami. Dodatkowo, odsyła to, na co wskazuje HEAD (symref=HEAD:refs/heads/master), więc klient wie, co sprawdzić, jeśli jest to klonowanie.

W tym momencie, proces fetch-pack sprawdza jakie obiekty posiada i wysyła odpowiedź z obiektami które potrzebuje za pomocą "want" oraz sumy SHA-1. Wysyła informację o tym jakie obiekty już posiada za pomocą "have" oraz SHA-1. Na końcu listy, wypisuje "done", aby proces upload-pack wiedział że ma rozpocząć wysyłanie spakowanych plików packfile z danymi które są potrzebne:

0054want ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0000
0009done
HTTP(S)

Potwierdzenie nawiązania łączności dla operacji pobierania wymaga dwóch żądań HTTP. Pierwsze z nich to GET do tego samego punktu końcowego, który jest używany w protokole "prostym":

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

Jest to bardzo podobne do wywołania git-upload-pack przez połączenie SSH, ale druga wymiana jest wykonywana jako oddzielne żądanie:

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

Ponownie, jest to taki sam format jak powyżej. Odpowiedź na to żądanie wskazuje sukces lub porażkę, i zawiera plik packfile.

Podsumowanie protokołów

Powyższa sekcja zawiera bardzo podstawowy przegląd protokołów transferu. Protokoły zawierają wiele innych funkcji, takich jak multi_ack czy side-band, ale ich omówienie wykracza poza zakres tej książki. Staraliśmy się dać Ci poczucie ogólnego przepływu informacji pomiędzy klientem a serwerem; jeśli potrzebujesz więcej informacji na ten temat, prawdopodobnie będziesz chciał zajrzeć do kodu źródłowego Git.

scroll-to-top