git의 내부 동작에 관해서 알아보자. 전부를 알 필요는 없고 중요한 몇 가지를 알아보도록 하자.
실습을 위해 react github인 https://github.com/facebook/react
에 들어가보도록 하자.
clone하여 tag에 대한 실습을 해보도록 하자.
git clone https://github.com/facebook/react.git
cd ./react
ls .git
명령어를 입력하면 숨겨진 .git
디렉터리에 있는 file들이 보일 것이다.
ls .git
branches config description HEAD hooks index info logs objects packed-refs refs
이들 중에 중요한 file과 directory를 모아보면 다음과 같다.
1. object
2. config
3. HEAD
4. index
5. refs
하나하나 알아보도록 하자. 먼저 config
에 대해서 알아보자.
config는 설정을 위한 file이며, local 저장소 단위의 config 설정을 위해 존재하는 file이다. 즉, 해당 config는 local에만 영향을 주지 전역적으로 영향을 주지 않는다.
cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = https://github.com/facebook/react.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
vscode-merge-base = origin/main
git config docs를 가면 여러 가지 설정 값들이 있다. 이를 참고하여 설정해주면 된다.
git config
명령어를 통해서 config 설정을 해줄 수 있는데, --local
을 붙이면 해당 local repo에만 영향을 받는다. 따라서, .git/config
의 값이 수정된다.
git config --local user.name "chicken little"
git config --local user.email "chicken@bbq.com"
다음은 user.name
과 user.email
을 추가한 것이다.
정말 설정되었는 지 확인해보도록 하자.
cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = https://github.com/facebook/react.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
vscode-merge-base = origin/main
[user]
name = chicken little
email = chicken@bbq.com
잘 설정된 것을 볼 수 있다. 만약 --local
을 붙이지 않으면 전역으로 config 설정이 된다는 것을 유념하자.
refs는 reference로 각 branch가 가리키는 head
reference와 remote
에 대한 reference, tag
에 대한 commit reference가 있다.
ls .git/refs/
heads remotes tags
먼저 heads
에 들어가보자.
ls .git/refs/heads/
main
main
branch 밖에 없으므로 main
file만 있다. 해당 file을 읽어보도록 하자.
cat .git/refs/heads/main
028c8e6cf5ce2a87147a7e03e503ce94c7a7a0cf
해당 commit hash가 무엇인지 알아보도록 하자.
git log --oneline
28c8e6cf5 (HEAD -> main, origin/main, origin/HEAD) Add Transition Types (#32105)
18eaf51bd5 Support eslint 8+ flat plugin syntax out of the box for eslint-plugin-react-compiler (#32120)
829401dc17 [Flight] Transport custom error names in dev mode (#32116)
...
28c8e6cf5
이 main
branch가 referencing하는 commit hash값이라는 것을 알 수 있다.
그럼 branch
를 하나 만들어서 해당 branch의 .git/refs/heads/
에는 무엇이 있을 지 보도록 하자.
git switch -c temp
echo "temp" >> ./README.md
git add ./README.md
git commit -m "temp commit1"
이제 .git/refs/heads/
를 확인해보도록 하자.
ls .git/refs/heads/
main temp
main
말고도 temp
branch가 생긴 것을 볼 수 있다.
다음으로 temp
branch가 가리키는 commit hash를 보도록 하자.
cat .git/refs/heads/temp
92e550beede531fdfe7b6f106febcf89134136d6
git log --oneline
92e550beed (HEAD -> temp) temp commit1
028c8e6cf5 (origin/main, origin/HEAD) Add Transition Types (#32105)
현재 HEAD의 값과 일치하는 것을 볼 수 있다.
여기서 재미난 점은 HEAD
도 .git
directory에서 볼 수 있다는 것이다.
cat .git/HEAD
ref: refs/heads/temp
HEAD
file이 refs/heads/temp
를 지칭하는 것을 볼 수 있다. 해당 값이 바로 92e550beede531fdfe7b6f106febcf89134136d6
라는 것이다. 따라서 HEAD
는 현재 branch에서 보고 있는 commit hash값을 레퍼런싱하고 있다는 사실을 알 수 있다.
git checkout
을 통해서 다른 commit으로 간 다음에 .git/refs/heads/temp
값의 변화를 볼 수 있다.
git log --oneline
92e550beed (HEAD -> temp) temp commit1
028c8e6cf5 (origin/main, origin/HEAD) Add Transition Types (#32105)
18eaf51bd5 Support eslint 8+ flat plugin syntax out of the box for eslint-plugin-react-compiler (#32120)
829401dc17 [Flight] Transport custom error names in dev mode (#32116)
fd2d279984 [eslint-plugin-react-hooks] Inline meta fields (#32115)
...
fd2d279984
commit으로 이동하고 .git/HEAD
값을 확인해보도록 하자. 즉, head를 detach시키는 것이다.
git checkout fd2d279984
HEAD is now at fd2d279984 [eslint-plugin-react-hooks] Inline meta fields (#32115)
cat .git/HEAD
fd2d2799840d9066a752bb32bbbb07c93f64a891
현재의 HEAD가 가리키는 commit으로 fd2d2799840d9066a752bb32bbbb07c93f64a891
가 나왔다. 이는 우리가 checkout했던 값이다.
반면에 .git/refs/heads/temp
에 가면 맨 앞에 있는 commit을 가리키는 것을 볼 수 있다.
cat .git/refs/heads/temp
92e550beede531fdfe7b6f106febcf89134136d6
이를 통해서 알 수 있는 사실은 다음과 같다. .git/refs/heads/branch
에 있는 값은 해당 branch의 가장 앞에 있는 commit을 말하는 것이고, .git/HEAD
는 현재 branch가 가리키는 HEAD를 말하는 것이다. 그래서 detached HEAD가 되면 .git/HEAD
는 해당 commit값을 가리킨다는 것이다.
HEAD temp
| |
v v
commit1 --> commit2 --> ... -> commitN (latest)
정리하면 다음과 같이 된 것이다.
git의 핵심 directory이다. file backup, content, commit도 여기에 저장된다.
ls .git/objects
5e 77 92 b3 ba dd info pack
16진수로 된 directory와 info
, pack
들로 이루어져 졌다.
ls .git/objects/5e/
6d2153667deb1a5fae2d672aeee651819aee3f
6d2153667deb1a5fae2d672aeee651819aee3f
라는 file이 있는데, 이 file은 사람이 읽을 수 있는 형식으로 되어있지 않다. 이는 system이 읽는 파일로 commit file, commit 정보, file content들을 git은 snapshot으로 저장한다. 즉, 저장소의 모든 데이터인 전체의 snapshot을 저장하는 것이다.
이 file안에는 4가지 종류의 git 객체가 들어갈 수 있다.
1. commit
2. tree
3. blob
4. annotated tag
이 4개를 해싱 함수를 통해서 해시값으로 관리하는 것이다.
4가지 종류의 git 객체를 분석하기 전에 git은 key와 value를 저장하는 database 역할을 한다는 것이다. git은 content를 저장하면 고유한 key를 반환하는데, 이 key로 저장한 content를 취득할 수 있다. 이 key가 바로 hash값인 것이다.
다음의 예시를 보자
1f7a7a... -> app.js (version1)
83baae... -> app.js (version2)
1f7a7a
, 83baae
와 같은 hash값이 하나의 key이고 value가 바로 app.js
라는 파일이다. 단 app.js
파일은 version에 따라 다르며 version1은 1f7a7a
으로 호출이 가능하고, version2는 83baae
으로 호출이 가능하다.
이렇게 git은 key-value database로서 4가지 종류의 객체를 저장하고, 불러오고 사용할 수 있다.
git hash-object
라는 재미난 명령어가 있는데, 이 명령어를 통해서 특정 value에 대해서 SHA-1 hash값을 만들어낼 수 있다. 기본적으로 file을 해싱하지만 --stdin
을 사용하면 문자도 해싱이 가능하다. 참고로 해당 명령어는 평소에는 아예 안쓰인다.
echo "hello" | git hash-object --stdin
ce013625030ba8dba906f756967f9e9ca394464a
해시값이 나왔지만, 이 해시값으로 얻을 수 있는 것은 없다. 해시값에 맞는 value를 저장하도록 만드는 방법은 -w
옵션을 추가하는 것이다.
echo "hello" | git hash-object --stdin -w
ce013625030ba8dba906f756967f9e9ca394464a
-w
명령어를 사용하면 objects
에 해시값으로 변환된 hello
를 저장한다. 단, 이때 해시값의 앞 두 글자만 따와서 directory로 만들고 저장한다.
objects
에서 해당 해시값이 있는 지 확인해보도록 하자.
ls .git/objects/
5e 77 92 b3 ba ce dd info pack
ls .git/objects/ce/
013625030ba8dba906f756967f9e9ca394464a
ce
에 013625030ba8dba906f756967f9e9ca394464a
가 있는 것을 볼 수 있다. 이는 우리가 저장한 hello
의 해시값인 ce013625030ba8dba906f756967f9e9ca394464a
을 말한다.
그럼 해싱값을 사용하여 어떻게 value를 얻어 올 수 있는가?? 그건 git cat-file
을 이용하면 된다.
git cat-file -p <object-hash>
우리의 해싱값을 넣어보도록 하자.
git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello
사실 전체를 넣을 필요가 없다.
git cat-file -p ce0136
hello
어느정도만 넣어도 이렇게 나온다. 단, 식별이 가능한 길이가 되어야 한다.
git에서는 이렇게 file 정보를 hashing하여 objects
에 저장했다가 cat-file
처럼 hash값을 통해 file 정보를 가져오고 읽는 것이다.
blob
은 파일의 내용을 담고 있는 객체이다. blob은 여러 file들의 content들을 저장할 수 있는데, 이 때 각 file들의 이름은 저장하지 않는다. 단지 file의 content만 담는 것이다.
우리가 hello
를 해시값으로 만들었을 때, objects
의 ce
에 013625030ba8dba906f756967f9e9ca394464a
가 생긴 것을 봤다. 013625030ba8dba906f756967f9e9ca394464a
도 원본 데이터를 담고 있는 하나의 blob이 되는 것이다.
ls .git/objects/
5e 77 92 b3 ba ce dd info pack
ls .git/objects/ce/
013625030ba8dba906f756967f9e9ca394464a
5e
, 77
, 92
, `b3
, ba
, ce
, dd
안에 있는 모든 해시이름의 파일들은 하나의 blob으로 content들의 정보를 담고 있다.
명심해야할 것은 blob
은 file의 content를 저장하지, file의 이름은 저장하지 않는다는 것이다.
그런데, 하위 디렉터리 구조가 복잡하게 연결된 파일들에 대해서는 file content뿐만 아니라, file들의 구조, 관계도 매우 중요하다. 이걸 담당하는 것이 바로 tree 객체이다.
tree는 git 객체로 directory 내용을 저장한다. 그래서 tree는 blob을 가리키는 포인터와 tree를 가리키는 포인터 둘 다 가지고 있다. 그래서 다음과 같이 쓸 수 있다.
root/
app.js
images/
pic.png
README.md
styles/
app.css
다음과 같이 root/
directory에 images/
, styles/
directory가 있고 file로 app.js
, README.md
가 있다. tree로 표현하면 다음과 같다.
object 종류 | 해시 | content |
---|---|---|
blob | 1f7a7a47... | app.js |
tree | 982871aa... | images/ |
blob | be321a77... | README |
tree | 80ff1ae33... | styles/ |
tree안에서 또 다른 tree를 참조하거나, blob을 참조하는 것이다. 또한, tree는 blob과 달리 file이름을 기억한다는 특징이 있다.
|root tree|
|
-------------------------------------------------
| | | |
v v v v
|app.js blob| |README.md blob| |images tree| |styls tree|
| |
v v
|pic.png blob| |app.css blob|
tree를 볼 수 있는 방법은 다음과 같다.
git cat-file -p main^{tree}
다음의 결과가 나온다.
040000 tree d0b0e04eb6108e5cd4c4a2c87a7c68f80772bbb1 .codesandbox
100644 blob 48d2b3d27e85ab32b1a9cff47ef95ebd2700b3b0 .editorconfig
100644 blob c30542a3f7e2c9aa9632def09051b3c709f54ffc .eslintignore
...
040000 tree eb386ac16763fded28806e04e3f852da6caaec2d packages
100644 blob 16ff05dce9ef23527bd6081adad482f5b30eaf1d react.code-workspace
040000 tree ff8b09dcda2f2eb00bd45cd85123cb2f43915459 scripts
100644 blob 48eca75afbba876489faa4d7a988b1510e21f96d yarn.lock
tree
에서 다른 tree에 대한 참조 해시값과 bloc 해시값을 가지고 있고, 각 tree와 blob들의 파일 명을 가지고 있다.
commit을 생성하면 git은 commit 객체를 생성해서 저장한다. commit도 objects
에 저장되는데, 다음과 같은 형식으로 저장된다.
------------------------
| commit(fa4907...) |
|----------------------|
| tree | c38719da...|
| parent | a323ffa....|
| author | Sirius |
|committer| Sirius |
------------------------
| this is my commit msg|
------------------------
parent
는 이전의 commit을 말한다. 하나의 역 linked list 구조를 가지는 것이다. tree
는 해당 commit 당시의 전체적인 project 구조를 표현하는데, 해당 commit에서 수정하지 않은 file, directory라도 tree
에 전부있다. 단, 수정이 있었던 file이나 directory에 대해서는 hash값이 달라져있고, 수정이 없던 file, directory는 모두 이전 commit들의 해시와 동일하다.
즉, tree
는 해당 commit 시점의 전체 project 구조를 전부 기억해놓는다는 것이다.
Head
|
v
------------------------ ------------------------
| commit(fa4907...) | | commit(fa4907...) |
|----------------------| |----------------------|
| tree | c38719da...| | tree | c38719da...|
| parent | none |<-----------| parent | fa4907... |
| author | Sirius | | author | Sirius |
|committer| Sirius | |committer| Sirius |
------------------------ ------------------------
| initial commit | | initial commit |
------------------------ ------------------------
이렇게 parent를 통해 이전 commit을 참조하고 있으니, 이는 git history를 이룰 수 있도록 해준다. tree는 해당 application 구조를 나타내고, blob은 content를 나타낸다.
실습을 위해 demo
directory를 만들자 github까지는 필요 없다.
mkdir demo
cd ./demo
echo "# demo" >> README.md
git init
git add README.md
git commit -m "main commit1"
git branch -M main
commit을 보도록 하자.
git log --oneline
280fa98 (HEAD -> main, origin/main) main commit1
280fa98
commit 해시값이 생성된 것을 볼 수 있다. 이제 해당 해시값을 git cat-file
을 통해 읽어보도록 하자.
git cat-file -t 280fa98
commit
commit
이라는 객체가 나온다. -p
옵션을 사용하면 좀 더 많은 정보를 이쁘게 찍어준다.
git cat-file -p 280fa98
tree 5dadd1311caf60dbb1c640bf8060a87309319eee
author colt <colt@colt.com> 1737541640 +0900
committer colt <colt@colt.com> 1737541640 +0900
main commit1
tree
와 metadata가 들어있는 것을 볼 수 있다. tree의 해시값인 5dadd1311caf60dbb1c640bf8060a87309319eee
을 풀어헤치면 다음의 결과가 나온다.
git cat-file -p 5dadd1311caf60dbb1c640bf8060a87309319eee
100644 blob fc72a5c1094e203eefcd1c710f060957ebbbaac4 README.md
README.md라는 한 개의 blob
으로 이루어진 project 구조가 나온다.
blob의 해시값을 풀어헤치면 다음이 나온다.
git cat-file -p fc72a5c1094e203eefcd1c710f060957ebbbaac4
# demo
이렇게 commit은 tree
를 포함하고 tree
안에 project 구조를 나타내는 tree
와 data를 저장하는 blob
으로 구성되어 있다.
다음으로 두번째 commit을 만들어보도록 하자.
echo "meow meow" >> ./cats.txt
git add ./cats.txt
git commit -m "main commit2"
commit history를 보도록 하자.
git log --oneline
e293a28 (HEAD -> main) main commit2
280fa98 (origin/main) main commit1
HEAD
가 e293a28
commit을 가리키는 것을 볼 수 있다. 해당 commit을 열어보자.
git cat-file -p e293a28
tree 50fe5aecf7aee19bc227bf755fe88c529facbe42
parent 280fa982dc9338703d055f25e6fabbd7c0402762
author colt <colt@colt.com> 1737542164 +0900
committer colt <colt@colt.com> 1737542164 +0900
main commit2
parent
가 있는 것을 볼 수 있다. 280fa982dc9338703d055f25e6fabbd7c0402762
는 바로 이전 commit인 main commit1
을 가리키는 것이다.
tree를 확인해보도록 하자.
git cat-file -p 50fe5aecf7aee19bc227bf755fe88c529facbe42
100644 blob fc72a5c1094e203eefcd1c710f060957ebbbaac4 README.md
100644 blob 6f6b544d1a3605ed85b3645784e2773e3564fc25 cats.txt
README.md
는 이번 commit에서 만든 blob이 아니다. 즉, 수정되지 않았던 값이라는 것이다. 그래서 해시값을 잘보면 이전과 동일하다. cats.txt
blob은 이번에 새로 생겨났으므로 6f6b544d1a3605ed85b3645784e2773e3564fc25
이 추가된 것이다.
그렇다면 README.md
값을 바꾸고 commit을 만들면 어떻게 될까??
echo "main commit3" >> ./README.md
git add ./README.md
git commit -m "main commit3"
3번째 commit이 만들어졌고, README.md
를 수정했다. 생성된 commit 객체를 보도록 하자.
git log --oneline
9847503 (HEAD -> main) main commit3
e293a28 main commit2
280fa98 (origin/main) main commit1
9847503
을 사용하면 된다.
git cat-file -p 9847503
tree 95a0f9d96e0ac19e8d186eea6cc98423ce69f14b
parent e293a282777340684fd68228791f89810a5adfd6
author colt <colt@colt.com> 1737542803 +0900
committer colt <colt@colt.com> 1737542803 +0900
main commit3
parent
인 e293a282777340684fd68228791f89810a5adfd6
은 이전 commit인 main commit2
의 값으로 e293a28
을 지칭한다. 다음으로 tree
해시 값인 95a0f9d96e0ac19e8d186eea6cc98423ce69f14b
을 조사하여 어떻게 바뀌었는 지 확인해보도록 하자.
it cat-file -p 95a0f9d96e0ac19e8d186eea6cc98423ce69f14b
100644 blob dc8d45288c3a347647f3c3a8360be0be0ebec923 README.md
100644 blob 6f6b544d1a3605ed85b3645784e2773e3564fc25 cats.txt
cats.txt
는 수정된 것이 없으므로 6f6b544d1a3605ed85b3645784e2773e3564fc25
와 같이 동일한 해시값을 가진다. 그런데, README.md
는 수정되었으므로 이전의 해시값인 fc72a5c1094e203eefcd1c710f060957ebbbaac4
와 다른 해시값인 dc8d45288c3a347647f3c3a8360be0be0ebec923
을 가진다.
정리하면 git은 commit
, blob
, tree
, tag
를 하나의 object들로 표현하고 이들을 key-value로 저장한다. key로 해시를 사용하여 value에 대한 레퍼런싱을 구현한 것이다. commit
은 하나의 object로 parent
로 이전 commit을 레퍼런싱하여 commit들 간의 history, 즉 관계를 표현한다. 또한 commit
안에 있는 tree
object는 해당 commit 당시의 project의 구조를 표현하는데, tree
와 blob
에 대한 hash값을 가지고 project 구조를 표현한다. blob
은 file의 내용을 저장하는 단위로, 모든 tree
의 끝은 blob
으로 구성되어 있다.