깃 리포지토리의 동작 방식 이해하기
지금껏 다양한 깃 명령어들을 사용해왔지만, 깃이 정확히 어떻게 동작하는지는 모르고 있었다.
이번에 OSSCA(오픈소스 컨트리뷰션 아카데미)에 참여하게 됐는데, 깃 리포지토리에서 커밋 시 .git 폴더 안의 내용들이 어떻게 변하는지 알아보는 과제를 받았다.
과제 수행 과정에서 알게 된 점들을 기록해두려 한다.
git init
git init은 깃 리포지토리를 초기화(initialize)하는 명령이다
어떻게? .git이란 숨김 폴더를 하나 만든다:



Finder에서 숨김 폴더를 보려면 Cmd + Shift + . (토글)
.git/hooks 안에는 여러 깃 훅 예시 파일들이 들어있다

.git/info/exclude 파일은 .gitignore 파일과 동일한 패턴 매칭 문법을 사용해 git에서 특정 파일들을 추적하지 않도록 할 수 있다

.git/refs/heads에는 브랜치 정보가 담기게 되는데, 첫번째 커밋 전에는 브랜치 정보가 없으므로 빈 디렉토리에서 시작한다
.git/config 파일에는 리포지토리 단위의 깃 설정과 remote 정보 같은 내용들이 담기게 된다
이때 전역 git 설정과 리포지토리의 .git/config의 값이 충돌할 경우, 리포지토리의 설정값이 우선순위를 가진다. (user.name, user.email 등이 겹친다면 리포지토리의 설정이 적용된다)

.git/HEAD 파일은 현재 checkout된 위치를 나타내는데, 두 가지 상태를 가질 수 있다:
- 일반적인 상태 (현재 브랜치를 가리키는 symbolic reference)
- detatched HEAD
TODO: HEAD 설명 더 채우기

.git/FETCH_HEAD 파일에는 마지막 git fetch 결과가 담기는데, 처음에는 remote가 없기 때문에 빈 파일이다

.git/description 파일은 GitWeb 같은 웹 인터페이스에서 저장소 설명을 보여주기 위한 파일이라고 하는데, GitHub, GitLab, Gitea에서는 사용되지 않는다고 하니 이런게 있다는 것 정도만 알아두면 될 것 같다


git add
간단한 파일을 하나 만들고 git add 명령을 실행하면, git commit 명령을 수행하기 전에도 .git/objects 에 새로운 blob 객체가 생기는 것을 확인할 수 있다:


➜ git ls-files -s
100644 9f4d96d5b00d98959ea9960f069585ce42b1349a 0 hello.md
➜ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349a
Hello Gitblob 이란 Binary Large OBjects의 약어로, 파일 내용을 zlib 압축 알고리즘으로 압축한 객체다.
압축을 풀고 읽으면 해당 파일의 모든 내용이 담겨있는 것을 확인해볼 수 있다
+ 디렉토리 안에 들어있는 파일의 수가 너무 많아지면 파일 시스템의 성능이 저하될 수 있기 때문에, 오브젝트의 파일 이름 중 앞 2글자는 디렉토리 이름으로 사용하고 나머지 38글자를 파일명으로 사용한다
git commit
커밋을 하고 나면 .git/objects에 commit, tree 두가지 객체가 더 생성되는 것을 확인할 수 있다
commit은 tree를 가리키고, tree는 파일 이름과 blob을 매핑하는 구조다:



6808bd.. 는 commit 객체, e11db9.. 는 tree 객체
➜ git cat-file -p 6808b6d1928c9f9b5aeac8cc22ff55153fa62a41
tree e11db9408991281fa8fc0b0bd455ecaffbf74cc2
author doorcs <itsdoorcs@gmail.com> 1777275722 +0900
committer doorcs <itsdoorcs@gmail.com> 1777275722 +0900
hello.md 커밋
➜ git cat-file -p e11db9408991281fa8fc0b0bd455ecaffbf74cc2
100644 blob 9f4d96d5b00d98959ea9960f069585ce42b1349a hello.mdgit branch
깃에서 브랜치는 어떻게 관리될까?
브랜치 정보는 .git/refs/heads 안에서 파일로 관리된다.
파일 내용도 별다를 게 없는데, 특정 커밋 해시값이 저장되어 있는 텍스트 파일이다


main 브랜치로부터 develop 브랜치를 새로 만들게 되면 .git/refs/heads 안에 develop이라는 이름의 파일이 생성되는데, main 파일과 동일한 커밋 해시값이 들어있는 것을 확인할 수 있다



blob 중복 관리
빈 파일을 두 개 만들고 git add 명령을 실행하면 blob 객체가 하나만 생성되는 것을 확인할 수 있다.



깃은 git add 명령을 통해 staging area에 올라오는 모든 파일에 대한 blob 파일을 만드는데, 이때 파일에 들어있는 내용을 기준으로 추적하기 때문에 내용이 겹칠 경우 동일한 blob을 재사용한다
빈 파일 두개를 커밋한 다음 commit이 가리키는 tree 의 내용을 살펴보면empty1.md, empty2.md 두 파일이 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 라는 동일한 blob 과 매핑되어 있는 것을 확인해볼 수 있다:
➜ git cat-file -p 0c04eb9141cec2836a4bb1293abb3db0179fb604
tree 137acbf1803395bc2d236f19af50db56a02a4eca
parent 6808b6d1928c9f9b5aeac8cc22ff55153fa62a41
author doorcs <itsdoorcs@gmail.com> 1777277752 +0900
committer doorcs <itsdoorcs@gmail.com> 1777277752 +0900
empty1, empty2 커밋
➜ git cat-file -p 137acbf1803395bc2d236f19af50db56a02a4eca
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 empty1.md
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 empty2.md
100644 blob 9f4d96d5b00d98959ea9960f069585ce42b1349a hello.mdgit merge
현재 브랜치별 커밋 상태는 다음과 같다:
- main 브랜치
- 6808b6d1928c9f9b5aeac8cc22ff55153fa62a41 (hello.md 커밋)
- develop 브랜치
- 6808b6d1928c9f9b5aeac8cc22ff55153fa62a41 (hello.md 커밋)
- 0c04eb9141cec2836a4bb1293abb3db0179fb604 (empty1, empty2 커밋)
main 브랜치로 돌아와서 develop 브랜치와 겹치지 않는 새로운 파일을 커밋하게 되면 브랜치와 커밋 히스토리가 갈라지게 된다:

이 경우 두 브랜치의 작업 내용들을 합치려면 merge commit이 필요하다
➜ git checkout main
Switched to branch 'main'
➜ git merge develop
Merge made by the 'ort' strategy.
empty1.md | 0
empty2.md | 0
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 empty1.md
create mode 100644 empty2.md지금은 merge conflict가 없어서 간단하게 merge 가능
merge commit 이후 git log 명령을 실행하면 커밋이 시간 순서에 맞게 잘 합쳐진것을 확인해볼 수 있다:

일반적인 commit 객체는 parent 커밋을 한 개 가지지만,
merge commit 객체는 parent 커밋을 두 개 가진다:
➜ git cat-file -p 95ee2c0c402f0a0a4bc81987e812eada0c0fedbf
tree e737137c198438d3f50927bbfdfdfb6951499049
parent cf2e9f4ab0d764c2f94534779eb16a524eecf289
parent 0c04eb9141cec2836a4bb1293abb3db0179fb604
author doorcs <itsdoorcs@gmail.com> 1777280109 +0900
committer doorcs <itsdoorcs@gmail.com> 1777280109 +0900
Merge branch 'develop'갈라졌던 깃 브랜치가 다시 합쳐지는 과정을 시각화해보면 다음과 같다:
* 95ee2c0 (HEAD -> main) Merge branch 'develop'
|\
| * 0c04eb9 (develop) empty1, empty2 커밋
* | cf2e9f4 newblob 커밋
|/
* 6808b6d hello.md 커밋git log --oneline --graph
정리
깃을 구성하는 핵심 객체들 ( .git/objects )
- COMMIT
- <Tree> + <Parent> + <Author> + <Committer> + <Message> 구조 (항목 간 줄바꿈)
tree객체를 가리키고 있음parent commit객체도 가리키고 있음 (최초 1회 커밋은 parent commit이 없다)merge commit객체의 경우 parent commit 정보를 두 개 이상 가지고 있기도 함- +
author,committer,commit message정보도 가지고 있음- 이때 author 정보와 committer 정보는 달라질 수도 있다
- TREE
- <Mode> <Type> <SHA-1 hash> <Name> 구조의 리스트 (여러줄일 수 있다)
- tree 객체의 줄 수 == 해당 tree 의 직속 하위 요소 수 (디렉토리, 파일 모두 포함)
Mode- 100644: 일반 파일
- 100755: 실행 파일 (Executable)
- 040000: tree (디렉토리 안에 디렉토리가 있는 경우를 표현)
Type- blob (파일)
- tree (디렉토리)
SHA-1: 해시값Name: 파일명 또는 디렉토리명
- BLOB (Binary Large OBjects)
- <Header> <Content> 구조 (한 줄)
Header- 아래 구조 고정:
- blob <Size>\0
Content- zlib 압축 알고리즘으로 압축된 파일 원본 데이터
- git commit 시점이 아닌
git add 시점에 생성된다 - 파일 내용 전체를 담고 있음
- 파일명은 내용에 포함되지 않는다
- file rename commit은 blob 수정이 아니라 tree 수정!!
- <Header> <Content> 구조 (한 줄)
커밋, 트리, 블롭은 모두 40자리 해시 값으로 표현된다
이때 한 디렉토리에 너무 많은 파일이 들어가면 파일시스템의 성능에 영향을 줄 수 있기 때문에,
앞 2자리는 디렉토리 이름으로 사용하고 나머지 38자를 파일명으로 사용한다!