본문 바로가기
Programming & Etc

Git Internal

by 탁종민 2021. 11. 2.

- 이문서는 git internal pdf문서 (https://github.com/pluralsight/git-internals-pdf)의 Understanding Git파트에서 몇몇 부분을 추가해 정리한 내용입니다.

 

Git Object Type

Git이 repository를 저장할 땐 4가지의 Git Object  Blob, Tree, Commit, Tag 를 이용해 저장한다. 

git object들은.git 디렉토리에 zlib으로 compressed 되어 저장된다. 그리고 그 Object의 이름은 [compress 되기 전의 content데이터+헤더값]의  sha-1 hash가 object의 이름이 된다. 헤더값은 [종류]\x20[decimal ascii size]\x00 포맷이다. 즉 Objec가 blob 타입이고 사이즈가 6이라면 “blob\x206\x00” 이 헤더이다.

 

이제부터 Object의 이름은 sha-1전체가 아니라 sha-1의 첫 6글자로만 표현할거니 유의하면서 이 문서를 읽길 바란다. 그리고 이후 내용들은 다음 디렉터리 구조를 예시로 사용하니 유의하자.

 

Blob

File의 Content만을  골라 Zlib으로 압축해 저장한 것이다. 그래서 만약 다른 권한 및 제목의 file이 있더라도 그 내용이 같으면 같은 Blob이고 따라서 같은 Blob으로 한번만 저장된다.

 

 

Tree

Tree는 Blob, Tree를 리스트를 저장하는 Object이다. 이 리스트의 내용에는 Blob, Tree의 파일 시스템에서의 타입( Directory인가, File인가, Symlink인가…), Permission, 원래 이름, 그리고 Git에서의 sha-1 이름이 포함된다. 그리고 이 리스트 내용의 sha-1 해시가 Tree의 이름이다. 

(https://stackoverflow.com/questions/737673/how-to-read-the-mode-field-of-git-ls-trees-output)

 

 

Commit

Commit은 Tree를 가리키는 Pointer이다. Commit Object의 실제 Content에는 Tree에 대한 Pointer와 함께 Commit Message, Author 등이 저장된다.

 

 

Initial Commit을 제외하고 다른 모든 commit에는 이전 commit가리키는  parent역시 추가된다. 이때 만약 branch에서 merge를 했다면 두 개 이상의 parent를 가지게 된다.

 

Tag

Tag는 특정 git object를 “가리키는” 특별한 Object인데, 보통은 commit을 가리킨다. 

Tag에는 tag가 가리키는 object, 그 object의 type, 그리고  아래 “v0.1” 과 같은 tag 이름 , tagger , tag message가 content로 저장된다.



 

Git Data Model

지금부터 아래와 같은 그래프들을 예로들어 설명한다. 회색 박스는 mutable 참조 ( 변경 가능한 참조 ) , 나머지들은 immutable 참조들이다. 즉 HEAD, remove, branch들은 각각 그 참조값을 변경 가능하지만 tag, commit tree 등등에 존재하는 참조값은 변경 불가능하다.

 

 

다음과 같은 구조의 디렉터리가 있다고 하자.

.
|-- init.rb
`-- lib
    |-- base
    |    `-- base_include.rb
    `-- my_plugin.rb

 위 디렉터리를 커밋때리면 git data model은 아래에서 왼쪽 그림과 같을 것이다. 이제 여기서 /lib/base/base_include.rb를 변경한다음 commit을 해보자. 그러면 base_include.rb의 내용이 바뀌었으니 새로운 blob을 생성한다. 그리고 새로운 blob을 가리키는 새로운 “/lib/base/” 에 해당하는 tree가 생겨난다. 다시 이 새로운 “/lib/base/” tree를 가리키는 /lib/ 에 해당하는 tree가 생겨난다. 이제 마지막으로 다시 새로운 “/lib/” tree를 가리키기 위한 새 root tree가 생긴다. 그 결과는 아래에서 오른쪽 그림과 같다. 

새로운 commit은 root tree를 가리키면서 동시에 parent commit을 가리킨다. Branch가 가리키는 값은 자동으로 새 commit으로 옮겨간다. 여기서 우리가 새로운 commit에 tag를 추가한다면 tag는 아래와 같이 방금 만든 commit을 가리킬 것이다.





여기서 root에 있는 /init.rb를 수정한다음 commit을 해보자. 수정된  /init.rb에 해당하는 새로운 blob이 생겨나고 이를 가리키는 새로운 root tree가 생긴다. 그 이외에는 변한게 없으므로 새로운 commit은 방금 새로 만든 root tree를 가리키는 것으로 이 커밋 작업은 끝난다.

 

Tree Traversal

이제 Commit이 가리키는 Root Tree를 이용해 디렉터리를 재구성하는 과정을 살펴보자.

만약 우리가 git checkout v0.1 이라는 명령어를 치면 git은 v0.1 tag를 찾고, 해당 tag가 가리키는 commit을 찾는다. 이후 Root Tree부터 차근 차근 Tree에 저장된 리스트를 훑어 보면서 파일 모드, 권한을 재구성하고 디렉터리를 재구성한다.

Branching and Merging

Branch

branch는 .git/refs/heads/{branch_name} 에 저장되어 있는 한 commit에 대한 pointer이다.

실제로 .git/refs/heads/{branch_name} 에 있는 특정 branch 파일을 cat 해보면 그냥 sha-1 값 하나가 달랑 들어있다. 이 sha-1값이 그 branch가 가리키고 있는 commit이다.

우리가 branch를 하나 만든다는 건 .git/refs/heads에 현재 commit을 가리키는 branch를 만드는 작업이다.

git checkout으로 branch를 옮기는 작업은 git의 working branch directory를 해당 branch directory로 옮기는 작업이다.

 



Merge

merge는 merge 하는 branch들이 가리키는 commit들을 하나로 합치는 과정이다. commit은 합치는 branch 수 만큼의 parent commit을 가지게 된다. 이를 그래프로 나타내면 아래와 같다.



Detached Head

git으로 checkout할 때 보통 branch로 checkout을 한다. 그렇기 때문에 git의 head는 branch를 가리킨다. 정확히 말하면 git checkout branch_name를 하면 git 의 working branch를 해당 branch_name으로 옮기고 branch가 가리키는 commit을 이용하게 된다.

만약 git으로 branch가 아니라 특정 commit으로 checkout을 하면 어떻게 될까? 그러면 HEAD가 branch를 선택하지 않은 상태 (detached head) 가 된다.

 

아래 그림을 봐보자. 처음 head가 main branch를 가리키는 상태에서 c2로 checkout을 하면 detached된 상태이다. 이 상태에서 c3 커밋을 하나 만들면 detached head가 c3을 가리킨다.

 



이상태에서 또 head를 main branch로 옮겨 버리면 아래에서 오른쪽 그림과 같이 c3 커밋이 dangling(unreachable)한 상태가 된다. 이런 commit은 git prune을 통해 제거할 수 있다.



Rebase

rebase를 알아보기 전에 먼저 merge를 기억해보자. merge는 여러개의 branch가 가리키는 commit들을 가리키는(parent로 가리키는) commit을 생성하는 방식이다.

 

rebase를 이용하면 현재 branch에서 발생한 첫 commit의 내용을 rebase타겟 commit에 patch로 적용해서 새로운 커밋을 생성한다.





아래 그림을 봐보자. rebase를 하면 기존에 존재하던 커밋들(예를들어 c3, c4)의 commit author, commit title등등은 같지만 사실 commit hash가 이전과 다르다. rebase를 하기전에는 부모 관계가 c1 <- c3 이였지만 이후에는 c2 <- c3 이다. 기존의 c3 커밋은 c1 커밋과 특정 tree, blob을 공유했지만 이제는 c1, c2 커밋들의 tree, blob을 공유하게 된다. rebase를 하기 전의 c3 커밋이 가리키던 Root Tree와 Parent 커밋이 rebase의 이후의 c3커밋이 가리키는 것과 다르므로 당연히 커밋의 hash도 다르다.

 

Git Object Directory

https://git-scm.com/book/en/v2/Git-Internals-Git-Objects

.git/objects/ 디렉토리에는 다음과 같은 디렉터리들이 존재한다.

zbvs@ubuntu:~/test/git_test/.git/objects$ ls

13  19  42  44  53  8a  9b  a6  c4  c8  cb  d6  e5  e9  info  pack

 

사실 .git/objects/ 에 존재하는 이 디렉터리들은 git에 존재하는 object들의 sha-1 40글자 중 첫 2글자이다. 위 디렉터리중 하나에 들어가보자. 그러면 git object들의 나머지 38글자가 파일이름으로 존재하고, 그 파일들의 내용은 git object의 content이다. ( content는 원래 데이터를 zlib으로 compress시킨 데이터이다. )

예를들어 13feaabb 라는 git object는 .git/objects/13 디렉터리에 feaabb 라는 파일이름으로 저장된다.

 

'Programming & Etc' 카테고리의 다른 글

Garbage Collection 알고리즘 - 2  (0) 2021.12.31
Garbage Collection 알고리즘 - 1  (0) 2021.12.31
Java synchronized 키워드와 Memory Barrier  (3) 2021.11.03

댓글