git add와 commit을 하면 objects에는 무엇이 생길까?

파란색 배경 위에 git add, commit을 하면 objects에는 무엇이 생길까라는 제목이 적힌 대표 이미지

이전 글에서는 .git 디렉토리 안에 어떤 파일과 폴더가 있는지 살펴보았다. 이번에는 그중에서도 Git의 실제 데이터가 저장되는 .git/objects를 조금 더 직접 살펴보려고 한다.

빈 저장소에서 파일 하나를 만들고, git add, git commit을 실행할 때 objects 디렉토리에 무엇이 생기는지 확인해보자!

git init을 통해 git 저장소를 생성하자

실제 작업 중인 프로젝트에서 실험하면 기존 객체와 섞여서 보기가 어려우니 /tmp 아래에 실험용 레포를 하나 만들자.

mkdir -p /tmp/git-object-lab
cd /tmp/git-object-lab
git init

만약 commit 시 사용자 정보가 없다는 에러가 난다면 실험 저장소 안에서만 아래처럼 설정하자.

git config user.name "Object Lab"
git config user.email "object-lab@example.com"

이제 현재 상태의 objects 디렉토리를 확인해보자.

find .git/objects -type f

처음에는 보통 실제 객체 파일이 없다. info, pack 디렉토리는 있지만, 아직 blob, tree, commit 객체는 만들어지지 않은 상태다.

object 내용을 실제로 확인해보자

Git object 파일은 그냥 텍스트 파일이 아니다. zlib으로 압축되어 있기 때문에 그대로 cat 하면 제대로 읽을 수가 없다. 간단히 확인하려면 Python으로 압축을 풀어볼 수 있는데

python3 - <<'PY'
import zlib

OBJECT_PATH = ".git/objects/OBJECT_DIR/OBJECT_FILE"

with open(OBJECT_PATH, "rb") as file:
    print(zlib.decompress(file.read()))
PY

OBJECT_PATH에는 실제 object 파일 경로를 넣고, 위와 같이 명령어를 입력해주자.

예를 들어 object id가 아래와 같다면:

8ab686eafeb1f44702738c8b0f24f2567c36da6d

Git은 앞 두 글자를 디렉토리 이름으로 쓰고, 나머지를 파일 이름으로 쓴다.

.git/objects/8a/b686eafeb1f44702738c8b0f24f2567c36da6d

물론 Git이 제공하는 명령어로도 확인할 수 있다.

git cat-file -t <object-id>
git cat-file -p <object-id>

직접 zlib을 풀어보면 Git object가 내부적으로 어떤 형태로 저장되는지 확인할 수 있다.

git add를 하면 blob이 생긴다

아래와 같이 파일을 하나 만들어보자

echo "hello git object" > hello.txt

아직 git add를 하지 않았으므로 objects에는 새 객체가 생기지 않은 상태이고

find .git/objects -type f

이 상태에서 git add를 실행해보고

git add hello.txt

다시 objects를 확인해보면

find .git/objects -type f

파일 내용에 해당하는 blob object가 생긴것을 확인할 수 있다. 즉, git add는 단순히 index만 바꾸는 것이 아니라 파일 내용을 Git object database에 저장한다. add 명령어를 통해 index 폴더와 git object 폴더 두 내용이 다 바뀐다고 생각하면 된다.

object id는 git hash-object로도 확인할 수 있다.

git hash-object hello.txt

방금 생긴 object 파일의 경로와 같은 hash인지 비교해보면 명확하게 확인할 수 있다.

git commit을 하면 tree와 commit이 생긴다

이제 commit을 만들어보자.

git commit -m "Add hello file"

다시 objects를 확인해보면

find .git/objects -type f

이번에는 객체가 더 늘어나는 것을 확인할 수 있다.

  • blob: 파일 내용
  • tree: 디렉토리 구조와 파일명, 파일 모드, blob 참조
  • commit: 작성자, 커밋 메시지, root tree, parent commit 정보

최근 commit object는 아래 명령어로 확인할 수 있다.

git rev-parse HEAD
git cat-file -p HEAD

git cat-file -p HEAD를 보면 commit이 tree object를 가리키고 있다는 것을 볼 수 있고 그 tree를 다시 열어보면 hello.txt가 어떤 blob을 가리키는지 확인할 수 있다.

git cat-file -p <tree-object-id>

결국 commit은 파일 전체를 직접 들고 있는 것이 아니라, tree를 가리키고, tree가 blob을 가리키는 구조라는 것을 알 수 있다. 여기에서 트리가 파일 이름과 blob을 매핑하는 역할을 하는 거고, blob은 순수한 데이터를 담고 있는 녀석이라고 생각하면 된다.

git push를 하면 local objects는 거의 바뀌지 않는다

git push는 local에 이미 있는 commit, tree, blob object를 remote로 전송하는 과정이다. 그래서 일반적으로 local .git/objects에 새로운 blob, tree, commit이 추가되는 과정은 아니지만 대신 remote 저장소의 objects를 확인하면 객체가 생긴 것을 볼 수 있다.

find /private/tmp/git-object-remote.git/objects -type f

정리하자면

Git의 변경 이력은 막연히 어딘가에 저장되는 것이 아니다. 블랙박스처럼 Git의 동작을 확인하기 어렵다는 생각을 종종했었는데, 공부를 하다보니, .git/objects 안에 object 형태로 쌓인다는 것을 알 수 있었다.

git add는 파일 내용을 blob으로 저장하고, git commit은 tree와 commit object를 만들어 그 blob들을 연결한다. git push는 이렇게 만들어진 object들을 원격 저장소로 보내는 과정이라고 이해하면 된다!

이 글을 따라해보며 실제 테스트해보자. 그렇다면 git에 대해서 조금 더 잘 이해할 수 있을 것이다!