내부 구조

정보 저장 위치

  • 스테이지와 레포지토리 내의 정보는 다음 두 가지 형태로 저장된다.
    • 인덱스(index): 스테이지의 정보 목록을 저장하는 하나의 파일
    • 객체(object): 스테이지 및 레포지토리의 파일과 디렉토리 내용을 저장하는 복수의 파일들

그림: 정보 저장 위치

인덱스

  • 인덱스는 .git 디렉토리 아래에 index라는 이름으로 저장되는 바이너리 파일이다.
  • 저장할 파일이 add 명령으로 스테이지에 들어가면
    • 우선 파일의 내용을 블롭 객체라는 형태로 저장하고
    • 해당 파일에 대한 포인터 정보를 인덱스 엔트리(index entry)라는 형식으로 인덱스 파일에 저장한다.
  • 커밋된 이후에도 인덱스 파일내의 엔트리 정보는 사라지지 않는다.

인덱스 엔트리

  • 인덱스 엔트리는 다음과 같은 정보를 포함한다.
    • 워크트리에 있는 실제 파일에 대한 정보
    • 객체로 저장된 파일내용에 대한 포인터(해시값) 정보

객체

  • 객체 파일은 파일의 내용 자체 또는 디렉토리의 구조(파일/서브디렉토리 목록) 등을 저장하는 ‘.git/objects’ 디렉토리 아래의 바이너리 파일이다.
  • 객체 파일의 종류에는 블롭 객체, 트리 객체, 커밋 객체가 있다.
  • 파일이 add 명령으로 스테이지에 들어가면 파일의 내용은 블롭(blob) 객체 파일에, 디렉토리의 내용은 트리(tree) 객체 파일에 저장된다.
  • 파일이 commit 명령으로 레포지토리에 기록되면
    • 해당 커밋에 포함되는 디렉토리들의 정보를 저장하는 트리 객체들을 만들고
    • 이 트리 객체들과 블롭 객체들을 가리키는 포인터(해시) 정보를 포함하는 커밋(commit) 객체가 만들어진다.

객체 파일의 해시값

  • 모든 객체는 이름(아이디)으로 sha1 해시값 40글자를 가짐
  • git에서 객체를 가리키는 포인터 정보라고 함은 이 해시값을 말한다.
  • 객체의 종류에 따라 해시 계산 방법이 다름
    • 블롭 객체의 해시값은 파일의 내용만(파일 이름도 사용하지 않음!) 사용하여 계산하므로 파일 내용이 같으면 해시값도 같음
    • 트리 객체의 해시값은 해당 디렉토리에 있는 파일/서브디렉토리 이름/속성 정보를 사용하여 계산하므로 이 내용이 같으면 해시값도 같음
    • 커밋 객체의 해시값은 작성자 정보, 작성 시간 정보 등을 통합하여 계산하므로 커밋 내용이 같아도 해시값이 달라질 수 있음

객체 파일의 저장 위치

  • 객체는 .git/objects 디렉토리 아래에 서브디렉토리로 나뉘어서 파일형태로 저장
  • 디렉토리를 분산하기 위해 40글자 sha1 해시값 중 첫 2글자를 디렉토리 이름으로 하고 나머지 38글자를 파일이름으로 저장

객체의 저장 형식

  • zlib으로 압축된 바이너리 파일
  • 객체를 hex 에디터로 보면 앞부분이 zlib DEFLATE 형식(0x78 0x9C)으로 시작함

객체 관련 명령

  • 저장소 전체의 객체 목록을 보는 법

    git cat-file --batch-check --batch-all-objects --unordered
  • 객체 하나의 유형을 보는 법

    git cat-file -t 해시값
  • 객체 하나의 파싱 정보를 보는 법

    git cat-file -p 해시값
  • 객체의 내용 정보를 원 바이트열 그대로 보고 싶을 때는 zlib 압축을 풀 수 있는 도구가 있어야 한다.

    • 예를 들어 “qpdf” 애플리케이션을 설치하면 zlib-flate -uncompress 명령으로 객체 내용을 볼 수 있다.
    • 단, 이 경우에는 바이너리 바이트열을 그대로 보여주므로 콘솔에서 문자로 잘 표시되지 않을 수 있다.

실습 1

  • 레포지토리 생성
cd $HOME/lab
rm -rf test_internal_01
git init test_internal_01
cd test_internal_01

Initialized empty Git repository in ~/lab/test_internal_01/.git/

tree .git
.git
├── config
├── description
├── HEAD
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   ├── sendemail-validate.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

9 directories, 18 files
echo "file1 line1" > file1.txt
git status -uall
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    file1.txt

nothing added to commit but untracked files present (use "git add" to track)

그림: 비추적 파일 생성
git add file1.txt
tree .git
.git
├── config
├── description
├── HEAD
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   ├── sendemail-validate.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── objects
│   ├── 0b
│   │   └── 11cfca50e35a4865e8505f1a108bd23a3f9401
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

10 directories, 20 files

그림: 파일 스테이징

블롭 객체

블롭 객체의 내용

  • 파일의 내용(바이트 스트림)만 저장
  • 커밋이 아닌 스테이징 시점에 생성되고 커밋되면 트리 객체에서 참조하게 됨
  • 파일명, 모드, 권한, 타임스탬프 등의 메타데이터 포함하지 않음
  • 파일명과 모드는 블롭 객체가 아닌 트리 객체에 저장됨

블롭 객체의 해시값

  • “blob <바이트길이>\0<데이터바이트>” 형식의 바이트열을 sha1 계산

미참조 블롭 (dangling blob)

  • 파일이 스테이징되어 블롭 객체가 생성되었는데 커밋되지 않고 취소가 되면 해당 파일의 블롭 객체는 인덱스 파일이나 다른 객체에게 참조되지 않고 남아있음
  • 이렇게 아무도 참조하지 않는 블롭 객체를 미참조 블롭(dangling blob)이라고 함
  • 미참조 블롭 객체가 많으면 디스크 용량을 많이 차지함
  • 시간이 지나면 자동으로 지워지지만 git gc 명령으로 명시적 삭제 가능

실습 2: 블롭 객체 조사

git cat-file --batch-check --batch-all-objects --unordered
0b11cfca50e35a4865e8505f1a108bd23a3f9401 blob 12
git cat-file -t 0b11cfca50e35a4865e8505f1a108bd23a3f9401
blob
git cat-file -p 0b11cfca50e35a4865e8505f1a108bd23a3f9401
file1 line1
zlib-flate -uncompress < ".git/objects/0b/11cfca50e35a4865e8505f1a108bd23a3f9401"
blob 12file1 line1
printf "file1 line1\n" | git hash-object -t blob --stdin
0b11cfca50e35a4865e8505f1a108bd23a3f9401
printf "blob 12\0file1 line1\n" | sha1sum
0b11cfca50e35a4865e8505f1a108bd23a3f9401  -

tree 객체

  • 트리 객체는 하나의 디렉토리(서브디렉토리 미포함)에 포함된 파일과 서브디렉토리에 대한 해시값 정보
  • 서브디렉토리가 존재하면 그에 해당하는 트리 객체가 추가적으로 존재

트리 객체의 내용

  • 트리 객체의 내용은 엔트리 열로 구성됨

  • 엔트리(entry)

    • 하나의 파일 또는 서브디렉토리를 대표하는 정보
    • 하나의 엔트리 내용은 “<mode> <파일명>\0<해시값>
  • 엔트리 열

    • 엔트리 정보를 이어붙인 “<entry><entry><entry>...” 형식
  • 하나의 트리 객체 내용은 “tree <크기>\0<엔트리 열>” 형식

  • 콘솔에서 정상 표시되지 않는 바이트열을 보고 싶으면 다음 파이썬 코드를 사용한다.

cat $HOME/get_tree_object_content.py
import zlib
import sys
from pathlib import Path


def get_tree_object_content(data: bytes) -> bytes:
    output = bytearray()

    # 기존 헤더 ("tree <size>\x00") 그대로 복사
    header_end = data.find(b"\x00")
    output += data[: header_end + 1]
    i = header_end + 1

    # 각 엔트리 파싱 및 SHA를 .hex()로 변환
    while i < len(data):
        mode_end = data.find(b" ", i)
        name_end = data.find(b"\x00", mode_end)
        mode = data[i:mode_end]
        name = data[mode_end + 1 : name_end]

        sha_start = name_end + 1
        sha_end = sha_start + 20
        sha = data[sha_start:sha_end]
        sha_hex = sha.hex().encode("ascii")

        # 기존 구조 유지
        output += b" " + mode + b" " + name + r"\0".encode() + sha_hex

        i = sha_end

    return bytes(output)


# .git/objects/xx/yyyyyy 에서 Git object 읽기
path = Path(".git", "objects", sys.argv[1][:2], sys.argv[1][2:])
with open(path, "rb") as f:
    compressed = f.read()
    raw = zlib.decompress(compressed)

    result = get_tree_object_content(raw) + b"\n"
    sys.stdout.buffer.write(result)

트리 객체의 해시값

  • tree 객체의 내용에 대해 SHA-1을 계산

커밋 객체

  • 커밋 객체는 커밋 전체의 정보를 저장하는 객체
  • 해당 커밋 시점의 전체 디렉토리 스냅샷을 저장
  • 구성 요소
    • 1개의 전역 트리 객체에 대한 해시값 정보
    • 1개 이상의 부모 커밋 객체에 대한 해시값 정보
    • 저자 정보 (커밋 시간 정보 포함)
    • 커미터 정보 (커밋 시간 정보 포함)
    • 커밋 메시지 정보
  • 시간 정보의 경우 일반적으로는 저자의 커밋 시간과 커미터의 커밋 기간은 동일함
    • 패치 파일을 사용하거나 리베이스, 체리픽 등을 한 경우 달라짐

실습 3: 커밋 객체와 트리 객체 생성

git commit -m c1
[main (root-commit) 4199a82] c1
 1 file changed, 1 insertion(+)
 create mode 100644 file1.txt
git cat-file --batch-check --batch-all-objects --unordered
0b11cfca50e35a4865e8505f1a108bd23a3f9401 blob 12
4199a828ee48b82acef1032616332e4646f50af7 commit 149
d6a665ff13b175d407fb943c946c4022017d4dd0 tree 37
git cat-file -t d6a665ff13b175d407fb943c946c4022017d4dd0
tree
git cat-file -p d6a665ff13b175d407fb943c946c4022017d4dd0
100644 blob 0b11cfca50e35a4865e8505f1a108bd23a3f9401    file1.txt
zlib-flate -uncompress < ".git/objects/d6/a665ff13b175d407fb943c946c4022017d4dd0"
tree 37100644 file1.txt��P�ZHe�P_��:?�
python $HOME/get_tree_object_content.py d6a665ff13b175d407fb943c946c4022017d4dd0
tree 37 100644 file1.txt\00b11cfca50e35a4865e8505f1a108bd23a3f9401
c1=$(git rev-parse HEAD)
echo $c1
4199a828ee48b82acef1032616332e4646f50af7
git cat-file -t $c1
commit
git cat-file -p $c1
tree d6a665ff13b175d407fb943c946c4022017d4dd0
author user <user@company.com> 1755584213 +0900
committer user <user@company.com> 1755584213 +0900

c1

그림: 커밋
mkdir -p sub
echo "file2 line1" > sub/file2.txt
git add .
git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   sub/file2.txt
git cat-file --batch-check --batch-all-objects --unordered
0b11cfca50e35a4865e8505f1a108bd23a3f9401 blob 12
4199a828ee48b82acef1032616332e4646f50af7 commit 149
4a8d995103b41f561e547ce3b32e1983d424da22 blob 12
d6a665ff13b175d407fb943c946c4022017d4dd0 tree 37

그림: 추가 파일 add
git commit -m c2
[main c6c762a] c2
 1 file changed, 1 insertion(+)
 create mode 100644 sub/file2.txt
git cat-file --batch-check --batch-all-objects --unordered
0b11cfca50e35a4865e8505f1a108bd23a3f9401 blob 12
4199a828ee48b82acef1032616332e4646f50af7 commit 149
4a8d995103b41f561e547ce3b32e1983d424da22 blob 12
4c2cf5eb3d8af11e9fe5f56cb6c853e1559d7166 tree 67
c6c762a824788dd896d9de6f71135f482d881a00 commit 197
d6a665ff13b175d407fb943c946c4022017d4dd0 tree 37
dd62677237dce0946aeffef97910ffc4ec32c3e7 tree 37
git cat-file -p $(git rev-parse HEAD)
tree 4c2cf5eb3d8af11e9fe5f56cb6c853e1559d7166
parent 4199a828ee48b82acef1032616332e4646f50af7
author user <user@company.com> 1755584215 +0900
committer user <user@company.com> 1755584215 +0900

c2
git cat-file -p 4c2cf5eb3d8af11e9fe5f56cb6c853e1559d7166
100644 blob 0b11cfca50e35a4865e8505f1a108bd23a3f9401    file1.txt
040000 tree dd62677237dce0946aeffef97910ffc4ec32c3e7    sub
git cat-file -p dd62677237dce0946aeffef97910ffc4ec32c3e7
100644 blob 4a8d995103b41f561e547ce3b32e1983d424da22    file2.txt

그림: 두번째 commit