Git은 두 저장소 간 데이터를 전송할 때 주로 두 가지 종류의 프로토콜을 사용한다. 하나는 HTTP이며 다른 종류는 Smart 프로토콜이라고 부를 수 있는 file://
, ssh://
, and git://
프로토콜을 사용한다. 주로 사용하는 이 두 가지 종류의 프로토콜을 통해 Git이 어떻게 데이터를 전송하는지 살펴볼 것이다.
Git이 HTTP로 데이터를 전송할 때 Dumb 프로토콜이라고 부른다. 데이터를 전송할 때 서버에서는 Git만을 위해 특화된 코드를 전혀 사용하지 않기 때문이다. Fetch 하는 과정은 여러 개의 GET 요청을 순서대로 보내고 데이터를 받는다. Git은 서버의 Git 저장소 구성이 일반적인 Git 저장소의 모습이라고 가정한다. simplegit
라이브러리에 대한 http-fetch
과정을 살펴보자:
$ git clone http://github.com/schacon/simplegit-progit.git
처음으로 하는 일은 info/refs
파일을 내려받는 것이다. 이 파일은 update-server-info
명령으로 작성되기 때문에 post-receive
훅에서 update-server-info
명령을 호출해줘야만 HTTP 전송이 잘 이루어진다.
=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949 refs/heads/master
리모트 레퍼런스와 SHA 값이 든 목록을 가져왔고 다음은 HEAD 레퍼런스를 찾는다. 이 HEAD 레퍼런스 덕택에 Git은 데이터를 내려받고 나서 어떤 레퍼런스를 Checkout할 지 알게 된다:
=> GET HEAD
ref: refs/heads/master
데이터 전송을 마치고 나면 master
브랜치를 Checkout할 준비가 끝난다. 이 시점에서 info/refs
에 보면 master
브랜치는 ca82a6
Commit 개체를 기점으로 시작한다. 그래서 그 커밋을 기점으로 Fetch한다:
=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)
개체는 서버에 Loose 형식으로 돼 있기 때문에 HTTP 서버에 있는 정적 파일을 가져오는 것처럼 가져오면 된다. 이렇게 서버로부터 얻어온 개체를 zlib로 압축을 풀고 header를 떼어 내면 다음과 같은 모습이 된다:
$ 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
아직 개체 두 개를 더 내려받아야 한다. cfda3b
개체는 방금 내려받은 Commit 개체의 Tree 개체이고, 085bb3
개체는 부모 Commit 개체이다:
=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)
위와 같이 Commit 개체는 내려받았다. 하지만, Tree 개체를 내려받으려고 하면:
=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)
이런! 존재하지 않는다는 404 메시지를 받았다. 해당 Tree 개체는 서버에 Loose 형식으로 저장돼 있지 않은가보다. 이런 상황이 벌어지는 이유가 좀 있다. 해당 개체가 다른 저장소에 있거나 저장소의 Packfile 속에 들어 있을 때 그렇다. 우선 Git은 다른 저장소 목록에서 찾는다:
=> GET objects/info/http-alternates
(empty file)
다른 저장소 목록이 비워져 있으면 Git은 Packfile에서 해당 개체를 검색한다. 그래서 Git은 프로젝트를 Fork 해도 디스크 공간을 효율적으로 사용할 수 있게 해준다. 우선 서버로부터 받은 다른 저장소 목록이 비어 있기 때문에 개체는 확실히 Packfile 속에 있을 것이다. 서버에 어떤 Packfile이 있는지 살펴보려면 objects/info/packs
파일이 필요하다. 이 파일 또한 update-server-info
명령에 의해 작성된다.
=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
현재 서버에는 Packfile이 하나 있고 당연히 개체는 이 파일 속에 있다. 확실히 확인해보기 위해 Packfile의 인덱스(Packfile이 포함하는 파일의 목록)에서 확인한다. 서버에 Packfile이 여러 개 있으면 이런 식으로 인덱스를 검색해서 원하는 개체가 있는 Packfile을 찾는다:
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)
이제 Packfile의 인덱스를 가져와서 개체가 들어 있는지 확인한다. Packfile Index에 해당 개체의 SHA 값과 오프셋을 파악할 수 있다. 찾았으면 해당 Packfile을 내려받도록 한다:
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)
Tree 개체를 얻어 오고 나면 다시 커밋 데이터를 가져 온다. 아마도 방금 내려받은 Packfile 속에 모든 커밋 데이터가 들어 있을 것이기 때문에 서버로 다시 데이터 전송 요청을 보내지 않아도 된다. Git은 HEAD가 가리키는 master
브랜치에 대한 소스코드를 복원해놓을 것이다.
이 과정에서 출력하는 것을 한 번에 모아 보면 다음과 같다:
$ git clone http://github.com/schacon/simplegit-progit.git
Initialized empty Git repository in /private/tmp/simplegit-progit/.git/
got ca82a6dff817ec66f44342007202690a93763949
walk ca82a6dff817ec66f44342007202690a93763949
got 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Getting alternates list for http://github.com/schacon/simplegit-progit.git
Getting pack list for http://github.com/schacon/simplegit-progit.git
Getting index for pack 816a9b2334da9953e530f27bcac22082a9f5b835
Getting pack 816a9b2334da9953e530f27bcac22082a9f5b835
which contains cfda3bf379e4f8dba8717dee55aab78aef7f4daf
walk 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
walk a11bef06a3f659402fe7563abf99ad00de2209e6
HTTP 프로토콜은 매우 단순하다는 장점이 있으나 전송은 효율적이지 못하다. Smart 프로토콜로 데이터를 전송하는 것이 더 일반적이다. 이 프로토콜은 리모트 서버에서 할 일이 좀 있다. 서버는 클라이언트가 어떤 데이터를 갖고 있고 어떤 데이터가 필요한지 분석하여 실제로 전송할 데이터를 추려낼 수 있다. 데이터 전송을 위해서 크게 두 가지로 나눌 수 있는데 하나는 데이터를 업로드하는 것이고 다른 하나는 다운로드하는 것이다.
리모트 서버로 데이터를 업로드하는 과정에서 Git은 send-pack
과 receive-pack
으로 나눌 수 있다. 클라이언트에서 실행되는 send-pack
과정과 서버의 receive-pack
과정은 서로 연결된다.
예를 들어 origin
이 SSH 프로토콜 URL로 설정된 상태에서 git push origin master
명령을 실행하면 Git은 send-pack
과정을 시작한다. 이 과정은 우선 서버에 SSH 연결을 만든다:
$ ssh -x git@github.com "git-receive-pack 'schacon/simplegit-progit.git'"
005bca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status delete-refs
003e085bb3bcb608e1e84b2432f8ecbe6306e7e7 refs/heads/topic
0000
git-receive-pack
명령은 우선 가진 레퍼런스 정보를 한 줄에 하나씩 보여준다. 이 예제는 첫 번째 줄에 master
브랜치의 이름과 SHA 체크섬을 보여준다. 그리고 첫 번째 줄에는 서버의 Capability도 보여준다(여기서는 report-status
와 delete-refs
이다).
각 줄의 첫 번째 4바이트의 Hex 값은 4바이트를 제외한 각 줄의 나머지 길이를 나타낸다. 첫 번째 줄이 005b로 시작하는데 이 Hex 값은 91을 나타낸다. 즉 첫 번째 줄의 처음 4바이트를 제외한 나머지 길이는 91바이트라는 것이다. 다음 줄의 값은 003b이며 이는 62바이트를 나타낸다. 마지막 줄은 값이 0000이며 이는 서버가 레퍼런스 목록의 출력을 끝냈다는 것을 의미한다.
send-pack
과정에서 이렇게 서버가 가진 정보를 알고 나면 어떤 커밋 데이터가 서버에 없는가를 분석한다. send-pack
은 Push할 레퍼런스에 대한 정보를 서버의 receive-pack
에 전달한다. 예를 들어 master
브랜치를 업데이트하고 experiment
브랜치를 추가하려 한다면 send-pack
은 서버에 다음과 같은 정보를 서버에 보낸다:
0085ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 refs/heads/master report-status
00670000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d refs/heads/experiment
0000
SHA-1 값이 모두 0이면 전에 없었던 것이다. 새로 추가하는 experiment
레퍼런스가 이에 해당한다. 반대로 레퍼런스를 삭제하려면 반대편 즉 업데이트를 할 오른편 위치의 해시 값을 모두 0으로 채운다.
Git은 업데이트할 레퍼런스의 예전 SHA, 새로운 SHA, 레퍼런스 이름을 각 줄에 담아 전송한다. 첫 번째 줄에는 클라이언트의 Capability도 포함된다. 그다음 클라이언트는 서버가 갖고 있지 않은 모든 데이터를 하나의 Packfile에 담아서 전송한다. 마지막으로 서버는 성공적으로 데이터를 처리했다고 응답하거나 아니면 실패했다고 응답한다:
000Aunpack ok
데이터를 다운로드하는 것에는 fetch-pack
과 upload-pack
과정으로 나뉜다. 클라이언트가 fetch-pack
과정을 시작하면 서버의 upload-pack
과정에 연결되고 서로 어떤 데이터를 내려받을지 논의한다.
리모트 저장소에서 upload-pack
과정을 시작하는 방법은 여러 가지다. receive-pack
과정처럼 SSH를 이용할 수도 있고 기본 포트가 9418인 Git 데몬을 이용할 수도 있다. 데몬에 연결되고 나면 fetch-pack
은 다음과 같은 데이터를 전송한다:
003fgit-upload-pack schacon/simplegit-progit.git\0host=myserver.com\0
처음 4바이트는 뒤에 이어지는 데이터의 길이를 나타낸다. 첫 번째 NULL 바이트까지가 실행할 명령이고 다음 NULL 바이트까지는 연결할 서버의 호스트 이름이다. Git 데몬은 실행할 수 있는 명령인지, 저장소가 존재하는지, 권한은 있는지 등을 확인한다. 모든 것이 가능하다면 upload-pack
과정을 시작하고 들어오는 요청 데이터를 처리한다:
SSH 프로토콜을 사용한다면 fetch-pack
은 다음과 같이 실행한다:
$ ssh -x git@github.com "git-upload-pack 'schacon/simplegit-progit.git'"
내부 방식이야 어쨌든, fetch-pack
과 연결된 upload-pack
은 다음과 같은 데이터를 전송한다:
0088ca82a6dff817ec66f44342007202690a93763949 HEAD\0multi_ack thin-pack \
side-band side-band-64k ofs-delta shallow no-progress include-tag
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
003e085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 refs/heads/topic
0000
위 receive-pack
의 응답과 매우 비슷하지만, Capability 부분은 다를 수 있다. HEAD 레퍼런스도 알려주기 때문에 저장소를 복제했을 때 어디부터 시작해야 할지 알 수 있다.
fetch-pack
은 이 정보에서 이미 가지는 개체에는 "have"를 붙이고 내려받아야 하는 개체는 "want"를 붙인 정보를 만든다. 마지막 줄에 "done"이라고 적어서 보내면 서버의 upload-pack
은 해당 데이터를 Packfile로 만들어 전송하기 시작한다:
0054want ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0000
0009done
데이터 전송 프로토콜에 대하여 아주 기초적인 상황을 통해 간단하게 살펴보았다. 클라이언트가 multi_ack
나 side-band
를 지원하는 더 복잡한 시나리오도 있다. 하지만, 여기에서는 Smart 프로토콜 과정을 설명하기 위해 가장 기초적인 시나리오를 설명한다.