Skip to content

Split View: Git Internals Deep Dive — Object Model, Packfile, Merge 알고리즘, Reflog, 프로토콜 완전 정복 (2025)

|

Git Internals Deep Dive — Object Model, Packfile, Merge 알고리즘, Reflog, 프로토콜 완전 정복 (2025)

TL;DR

  • Git은 content-addressable 파일시스템이다. 버전 관리는 그 위에 구축된 레이어에 불과. 모든 데이터(파일, 디렉토리, 커밋)는 SHA-1 해시로 주소 지정.
  • 4가지 객체 타입: blob(파일 내용), tree(디렉토리), commit(스냅샷 + 메타), tag(서명된 참조).
  • Loose object vs Packfile: 처음엔 각 객체가 파일 하나(.git/objects/ab/cd...), 시간이 지나면 packfile로 압축하여 델타 저장.
  • Refs: 브랜치/태그는 단순히 commit 해시를 가리키는 텍스트 파일. HEAD는 현재 체크아웃된 것.
  • Index: 스테이징 에어리어. 다음 커밋의 tree를 미리 구성하는 바이너리 파일.
  • Merge: 2021년 recursiveort 기본 알고리즘 변경. 500배 빠른 경우도.
  • Rebase: 커밋을 하나씩 cherry-pick해 새 base 위에 재배치. 커밋 해시 변경 주의.
  • Reflog: HEAD와 refs의 모든 변경 이력. "잃어버린 커밋"을 복구하는 비밀.
  • Packfile: 델타 압축 + 인덱스(.idx) + 비트맵(.bitmap). Git push/fetch를 KB 수준으로 줄임.
  • Protocol: Smart HTTP (most common), SSH, Git protocol. 클라이언트가 "내가 가진 것"을 선언 → 서버가 필요한 것만 전송.

1. Git의 철학 — "파일시스템, 버전 관리 아님"

1.1 Linus Torvalds의 2주

2005년, BitKeeper(Linux 커널이 쓰던 VCS)가 무료 라이선스를 철회했다. Linus는 분노했고 2주 안에 대체제를 만들어냈다. Git.

설계 목표:

  1. 분산: 모든 clone이 완전한 리포지토리. 중앙 서버 불필요.
  2. 빠름: 커널 크기의 리포지토리에서도 초 단위 작동.
  3. 무결성: 데이터 손상 즉시 감지.
  4. 동시 개발 지원: 브랜치/머지가 first-class.

Linus의 관점: 기존 VCS들은 틀린 추상화를 썼다. 파일 간 diff를 저장하는 대신 스냅샷을 저장해야 한다. Git의 핵심 아이디어는 이것이다.

1.2 Content-Addressable Filesystem

Git의 본질은 키-값 저장소:

SHA-1 해시 → 객체 내용

저장: "이 내용을 저장해줘" → Git이 SHA-1 해시를 돌려줌. 조회: "이 해시에 해당하는 내용을 줘" → Git이 객체를 돌려줌.

이것이 전부다. 나머지(브랜치, 머지, 히스토리)는 이 위에 구축된 레이어다.

1.3 간단한 실험

mkdir /tmp/gitrepo && cd /tmp/gitrepo
git init

echo "Hello, Git" | git hash-object -w --stdin
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad

hash-object -w는:

  1. 입력 내용에 헤더 추가.
  2. SHA-1 계산.
  3. zlib 압축.
  4. .git/objects/3b/18e5.../ 에 저장.
  5. 해시 출력.
git cat-file -p 3b18e512
# Hello, Git

"이 해시의 내용을 보여줘" → 내용 반환. 버전 관리 없이 내용 기반 저장소가 작동.


2. 객체 모델

2.1 4가지 객체 타입

Blob: 파일 내용 (메타 없음). 같은 내용 두 파일은 같은 blob.

Tree: 디렉토리 목록. "이 이름 → 이 모드 → 이 blob/tree".

Commit: 스냅샷 (tree 포인터) + 메타 (부모, 저자, 메시지).

Tag: annotated 태그. Commit을 가리키는 서명/메시지 포함 객체.

2.2 Blob 상세

Blob은 순수한 내용:

blob <byte_length>\0<content>

예: "Hello, Git" (11바이트, 개행 포함):

blob 11\0Hello, Git\n

이것을 SHA-1 → 식별자. zlib으로 압축해 저장.

같은 내용 = 같은 blob. 10,000 파일이 같은 "Hello World"를 가지면 blob 하나만 저장.

2.3 Tree 상세

Tree는 디렉토리:

tree <length>\0
100644 blob abc123... README.md
040000 tree def456... src
100755 blob ghi789... build.sh

각 엔트리:

  • 모드: 100644(일반 파일), 100755(실행), 040000(디렉토리), 120000(심볼릭 링크), 160000(서브모듈).
  • 타입: blob 또는 tree.
  • SHA-1: 20 바이트 바이너리.
  • 이름: null로 끝.
git cat-file -p HEAD^{tree}
# 100644 blob a906cb2... README.md
# 040000 tree 68aba62... src
# 100755 blob 9daeafb... build.sh

Tree는 파일시스템 스냅샷을 재귀적으로 표현한다. 현재 디렉토리는 tree 하나, 하위 디렉토리는 tree의 tree.

2.4 Commit 상세

Commit은 tree를 가리키는 포인터 + 메타데이터:

commit <length>\0
tree 3c4e9cd789...
parent 5a9d8b41...
author Linus Torvalds <torvalds@linux.org> 1234567890 -0700
committer Linus Torvalds <torvalds@linux.org> 1234567890 -0700

Initial commit

필드:

  • tree: 이 커밋의 전체 파일시스템 스냅샷.
  • parent: 부모 커밋(들). 첫 커밋은 없음. 머지 커밋은 2개 이상.
  • author: 변경을 만든 사람 + 시간.
  • committer: 커밋을 적용한 사람 + 시간. 보통 author와 같음. rebase/cherry-pick 시 다름.
  • 메시지: 빈 줄 이후 텍스트.
git cat-file -p HEAD
# tree 3c4e9cd789...
# parent 5a9d8b41...
# author John Doe <john@example.com> 1700000000 +0000
# committer John Doe <john@example.com> 1700000000 +0000
#
# Add feature X

2.5 Tag 상세

Annotated tag (git tag -a):

tag <length>\0
object 3c4e9cd789...
type commit
tag v1.0.0
tagger John Doe <john@example.com> 1700000000 +0000

Release v1.0.0

-----BEGIN PGP SIGNATURE-----
...
-----END PGP SIGNATURE-----

Lightweight tag (git tag)는 그냥 ref (아래 설명).

2.6 해시 체인

네 타입이 연결된 그래프:

commit → tree → blob
              → blob
              → tree → blob
commit → parent commit → parent commit → ...

모든 연결이 해시. 한 바이트만 바뀌어도 해시가 완전히 달라진다 → 모든 조상 커밋의 해시도 연쇄적으로 변경.

이것이 Git의 무결성 보장이다. 한 부분을 변조하면 이후 모든 해시가 틀리므로 즉시 감지.

2.7 SHA-1 vs SHA-256

Git은 원래 SHA-1을 썼다. 2017년 Google이 SHA-1 충돌 공격(SHAttered)을 증명한 후 SHA-256 지원 추가.

  • 2020+: git init --object-format=sha256 옵션.
  • SHA-1과 SHA-256은 상호 호환 불가. 별도 저장소.
  • 대부분 프로젝트는 여전히 SHA-1. 마이그레이션 복잡.

2.8 Loose Object의 저장

새로 만든 객체는 loose object로 저장:

.git/objects/
├── 3b/
│   └── 18e512dba79e4c8300dd08aeb37f8e728b8dad   (2자 디렉토리, 나머지 38자 파일)
├── 5a/
│   └── 9d8b41...
└── ...

파일 내용: <type> <size>\0<content> 를 zlib으로 압축. 해시는 압축 전 내용의 SHA-1.

왜 첫 2자로 디렉토리를 나누는가? 많은 파일시스템이 한 디렉토리의 파일 수가 수만을 넘으면 느려지기 때문. 256 버킷으로 분산.


3. Refs — 의미 있는 이름

3.1 Ref = 해시의 별칭

브랜치, 태그, HEAD는 단순히 해시를 가리키는 파일이다.

cat .git/refs/heads/main
# 3c4e9cd789abc...

그게 전부다. "main 브랜치"는 이 파일 하나. 아무 커밋 해시로 내용을 바꾸면 브랜치가 그 커밋을 가리킨다.

3.2 HEAD

현재 체크아웃된 ref:

cat .git/HEAD
# ref: refs/heads/main

Detached HEAD는 branch 대신 직접 해시:

cat .git/HEAD
# 3c4e9cd789abc...

git checkout <commit_hash>가 이 상태를 만든다.

3.3 Tag refs

cat .git/refs/tags/v1.0.0
# 3c4e9cd789abc...

Lightweight tag: commit을 바로 가리킴. Annotated tag: tag object를 가리킴, 그 object가 commit을 가리킴.

3.4 Remote refs

ls .git/refs/remotes/origin/
# main  develop  feature-x
cat .git/refs/remotes/origin/main
# 3c4e9cd789abc...

Remote에서 마지막으로 fetch한 상태. 로컬 브랜치와 완전히 독립.

3.5 packed-refs

수만 개 ref가 있으면 파일이 너무 많음. Git은 주기적으로 ref들을 하나의 파일로 packing:

cat .git/packed-refs
# 3c4e9cd789abc refs/heads/main
# 5a9d8b41... refs/tags/v1.0.0
# ...

개별 파일은 "unpacked" 상태. Git이 ref 조회 시 둘 다 확인(unpacked 우선).

3.6 Symbolic Ref

Ref가 다른 ref를 가리킬 수 있다. HEAD가 대표적:

HEAD → refs/heads/main → <commit_hash>

git symbolic-ref HEAD로 직접 볼 수 있다.


4. Index — 스테이징 에어리어

4.1 역할

Index는 다음 커밋의 tree를 미리 구성하는 캐시다. git add가 업데이트하는 대상.

작업 디렉토리  (git add)Index  (git commit)Commit

4.2 구조

.git/index바이너리 파일:

header (magic + version + count)
entries:
  - ctime, mtime
  - dev, ino (inode)
  - mode
  - uid, gid
  - file size
  - SHA-1 (blob)
  - flags
  - path name
checksum

각 항목은 이미 git add된 파일을 가리킨다.

git ls-files --stage
# 100644 a906cb2a4a904a15... 0 README.md
# 100644 3b18e512dba79e4c... 0 src/main.c

4.3 왜 index가 있는가

이유 1: 부분 커밋. git add 일부 파일만 → 선택적 커밋.

이유 2: 성능. 파일 변경 감지가 inode/size만 비교로 빠름. 실제 diff 비싸지 않음.

이유 3: 머지 중간 상태. 충돌 시 index에 여러 단계(stage)로 저장:

100644 <ancestor_hash>  1 foo.c    # 공통 조상
100644 <ours_hash>      2 foo.c    # 우리 쪽
100644 <theirs_hash>    3 foo.c    # 저쪽

4.4 "3 트리" 모델

Git의 세 가지 상태:

HEAD              Index            Working Tree
(last commit)    (staged)        (unstaged)
   ↓                ↓                 ↓
commit objects   index file      filesystem

git status는 두 쌍을 비교:

  • HEAD vs Index: "Changes to be committed".
  • Index vs Working Tree: "Changes not staged".

5. Packfiles — 효율적 저장

5.1 Loose Object의 한계

10,000 파일에 10번씩 수정하면 loose object 100,000개. 각각이 파일이라 inode 낭비 + 디스크 공간 낭비.

더 큰 문제: 델타 압축 불가. 100MB 파일의 10바이트 수정이 100MB 새 blob 생성.

5.2 Packfile의 해결

Packfile은 델타 압축된 객체 묶음:

.git/objects/pack/
├── pack-abc123.idx     # 인덱스 (offset lookup)
├── pack-abc123.pack    # 실제 데이터
├── pack-abc123.bitmap  # reachability bitmap (선택)

5.3 Pack 포맷

Pack file:
  헤더: "PACK" + version + object count
  객체들:
    - 타입 (blob/tree/commit/tag/delta)
    - 압축된 데이터
    - OFS_DELTA 또는 REF_DELTA 면 base 참조
  체크섬 (20 바이트)

5.4 델타 압축

객체 A가 객체 B와 비슷하면:

Pack의 B:    전체 내용 (zlib 압축)
Pack의 A:    "B를 참조해서, 다음 edit instructions 적용하라"

Edit instructions:

  • COPY: "B의 offset X부터 N 바이트".
  • INSERT: "이 새 N 바이트 삽입".

예: "Hello, World" → "Hello, Git" 변경:

A = COPY(0, 7) + INSERT("Git") + COPY(12, 1)
  = "Hello, " + "Git" + "\n"

대부분의 파일 변경은 작은 edit → 델타가 훨씬 작다.

5.5 Base Object 선택

어느 객체를 base로 쓸지? Git의 휴리스틱:

  • 같은 파일명의 이전 버전.
  • 크기가 비슷한 객체.
  • 접두사 매칭.

git repack -f 옵션으로 강제 재구성 가능.

5.6 Delta Chain

델타가 또 다른 델타의 base가 될 수 있다:

V1 (full)V2 (delta of V1)V3 (delta of V2)...

검색 시 체인을 거슬러 올라가며 복원. 너무 긴 체인은 성능 저하 → pack.depth (기본 50)로 제한.

5.7 Pack Index (.idx)

.pack 파일 자체는 순차. 특정 해시를 찾으려면 .idx 사용:

Pack index:
  fanout table (256 entries, 해시 첫 바이트별 오프셋)
  sorted sha1 list
  CRC32 list
  offset list

빠른 lookup: fanout → binary search. O(log n).

5.8 Reachability Bitmap

Git 2.0+. 각 커밋에 도달 가능한 객체들의 bitmap을 저장:

Commit abc123:
  bitmap: 0b101100110011...  (각 비트가 pack의 객체)

git fetch가 "이 커밋이 필요하지만 그 부모들은 이미 있어"를 빠르게 계산. GitHub의 git clone이 빨라진 비결.


6. Object Storage 동작

6.1 객체 생성

git add file.txt

이 명령이 하는 일:

  1. file.txt 읽기.
  2. Blob 해시 계산 (blob <size>\0<content> SHA-1).
  3. .git/objects/ 해시 경로에 이미 있는지?
    • 있음: skip.
    • 없음: zlib 압축해서 저장.
  4. Index 업데이트 (blob 해시 기록).

6.2 커밋 생성

git commit -m "Add feature"
  1. Index의 파일들로 tree 객체들 생성 (재귀적으로 디렉토리마다).
  2. 최상위 tree 해시.
  3. Commit 객체 생성: tree + parent + author + message.
  4. Commit 해시 계산.
  5. HEAD가 가리키는 ref 업데이트.

6.3 Checkout

git checkout main
  1. refs/heads/main의 해시 읽기.
  2. 그 commit의 tree 읽기.
  3. Tree 순회하며 working directory 갱신.
  4. Index도 tree와 일치하도록 업데이트.
  5. HEAD를 main으로 갱신.

6.4 GC (Garbage Collection)

git gc
  • Loose object → packfile로 압축.
  • Unreachable 객체 (어떤 ref/reflog에서도 도달 불가) 삭제.
  • 기본 2주 이전 dangling 객체 제거.

자동 GC는 .git/objects에 loose object가 6700+ 되면 트리거.

6.5 Prune

Reachable object만 유지하고 나머지 영구 삭제:

git prune --expire=now

주의: reflog의 entries도 유지 이유가 된다. Reflog가 clean이어야 실제 삭제.


7. 머지 알고리즘

7.1 3-way Merge 기본

두 브랜치를 합칠 때:

  • Common Ancestor: 두 브랜치의 공통 조상 커밋.
  • Ours: 현재 브랜치의 마지막 커밋.
  • Theirs: 합칠 브랜치의 마지막 커밋.

Git은 각 파일에 대해:

Ancestor (base)
    ↓        ↓
   Ours   Theirs

Case 1: Ours만 변경 → Ours 사용. Case 2: Theirs만 변경 → Theirs 사용. Case 3: 둘 다 같은 변경 → 그 변경 사용. Case 4: 둘 다 다른 변경 → 충돌.

7.2 Fast-forward

가장 단순한 케이스:

main:    ABC
branch:  ABCDE

git merge branch하면 main을 E로 그냥 이동. 머지 커밋 없음. "Fast forward".

7.3 3-way Merge Commit

main:    ABCD
branch:  ABEF

공통 조상 B. 머지 결과:

main:    ABCDM
branch:  ABEF → ┘

M은 머지 커밋: 두 개 부모를 가진다.

7.4 Recursive → Ort

2021년까지 Git의 기본 merge 알고리즘은 recursive. 공통 조상이 여러 개인 경우(criss-cross merge), 조상들을 재귀적으로 merge.

문제: 대형 레포에서 극도로 느릴 수 있음. "Too many changes" 케이스에서 디스크 사용량 폭발.

Ort (Ours/Recursive/Theirs): Elijah Newren이 다시 쓴 알고리즘.

  • 500x 빠른 경우도.
  • Rename detection 개선.
  • Subtree merge 지원.
  • 메모리 효율 개선.

2021년 git 2.33부터 기본. 대부분 개발자는 변경을 눈치채지 못함 — merge가 그냥 빨라졌다.

7.5 Rename Detection

Before: foo.c
After:  bar.c (내용 비슷)

Git은 이를 rename으로 인식한다 (내용 유사도가 50%+ 이면 기본). 중요한 이유: rename 된 파일에 다른 브랜치에서 편집이 있었다면 merge가 이전 이름에 적용할 게 아니라 새 이름에 적용해야.

Ort는 rename detection 더 정확 + 빠름.

7.6 충돌 해결

git merge branch
# Auto-merging foo.c
# CONFLICT (content): Merge conflict in foo.c

foo.c 내용:

<<<<<<< HEAD
int x = 1;
=======
int x = 2;
>>>>>>> branch

개발자가 편집 후:

git add foo.c
git commit  # merge commit 생성

Index의 다중 stage entries가 사용된다.

7.7 Strategy 옵션

  • -X ours: 충돌 시 항상 Ours 선택.
  • -X theirs: 항상 Theirs.
  • -X ignore-all-space: 공백 무시.
  • -X rename-threshold=80: rename 감지 임계.

전략 자체 (-s):

  • ort: 기본, 2-way + criss-cross 모두.
  • recursive: Legacy, 같은 역할.
  • resolve: 간단, 역사적.
  • octopus: 여러 브랜치 한 번에 (충돌 없는 경우만).
  • ours: Theirs를 완전히 무시 (이력만 합침).

8. Rebase 메카닉

8.1 아이디어

main:    ABC
branch:  ABDE

git rebase main:

main:    ABC
branch:  ABCD' → E'

D'E'새 commit. 같은 변경이지만 다른 parent → 다른 해시.

8.2 실제 동작

내부적으로 Git이 하는 일:

  1. main의 최신 commit을 임시 HEAD로.
  2. branch의 commits를 순서대로 D, E 나열.
  3. 각 commit을 cherry-pick:
    • Ancestor 계산 (이전 commit).
    • 3-way merge 수행.
    • 새 commit 생성 (새 parent).
  4. 모든 commit이 성공하면 branch를 새 HEAD로 업데이트.

8.3 Interactive Rebase

git rebase -i HEAD~5

텍스트 에디터:

pick abc123 Commit 1
pick def456 Commit 2
squash ghi789 Commit 3
reword jkl012 Commit 4
drop mno345 Commit 5

Actions:

  • pick: 그대로 유지.
  • reword: 커밋 메시지 변경.
  • edit: 중간에 멈추고 수정 가능.
  • squash: 이전 커밋에 합침.
  • fixup: squash + 이전 메시지 유지.
  • drop: 삭제.

이력을 다시 쓴다 (history rewriting). 새 해시 생성.

8.4 Rebase의 위험

Public 브랜치에 rebase하지 말 것:

#  (local)
git rebase main

# 푸시
git push --force   # <-- 위험

다른 사람이 옛 해시 기반으로 일하고 있으면 그 사람의 작업이 꼬인다. Public 브랜치에는 merge 사용.

8.5 Force-with-lease

--force의 안전한 버전:

git push --force-with-lease

"원격이 내가 마지막으로 본 상태와 같을 때만 force push". 다른 사람이 push한 게 있으면 거부.


9. Reflog — 안전망

9.1 Reflog란

HEAD와 각 ref의 변경 이력을 기록하는 로컬 로그. git reset --hard, git rebase 같은 "위험한" 작업 후에도 복구 가능.

git reflog
# abc123 (HEAD -> main) HEAD@{0}: commit: Add feature
# def456 HEAD@{1}: commit: Fix bug
# ghi789 HEAD@{2}: rebase finished: returning to refs/heads/main
# jkl012 HEAD@{3}: rebase: onto main
# ...

9.2 복구 예제

시나리오: git reset --hard로 실수로 최근 커밋 날림.

git reflog
# abc123 HEAD@{1}: commit: 잃어버린 커밋!
# def456 HEAD@{0}: reset: moving to HEAD~1

git reset --hard abc123
# 복구됨

Git이 reset했지만 객체는 아직 .git/objects/에 있다. Reflog에 해시가 기록돼 있으므로 찾을 수 있다.

9.3 저장 위치

cat .git/logs/HEAD

각 ref별로 별도 로그:

.git/logs/HEAD
.git/logs/refs/heads/main
.git/logs/refs/heads/feature

9.4 만료

Reflog는 영원하지 않다. 기본 설정:

  • reachable 객체: 90일 후 reflog entry 제거.
  • unreachable: 30일 후.

git gc 시 정리.

9.5 Recovery 루틴

"아, 실수했다" 순간:

  1. 당황하지 말 것. Reflog가 살려줄 가능성이 높다.
  2. git reflog 실행.
  3. 복구하려는 지점 해시 확인.
  4. git reset --hard <hash> 또는 git cherry-pick <hash>.

30일 안에만 기억하면 거의 모든 실수 복구 가능.


10. Partial Clone과 Sparse Checkout

10.1 큰 리포의 문제

Linux 커널: 3GB. Chromium: 50GB. 완전 clone은 시간/공간 낭비.

10.2 Shallow Clone

git clone --depth=1 https://github.com/...

최근 commit 1개만. 이력 없음. CI/CD에 자주 사용.

단점: 전체 이력 필요할 때 git pull 불가.

10.3 Partial Clone

Git 2.19+. 객체의 일부만 받기:

git clone --filter=blob:none https://github.com/...
  • 모든 commit + tree 받음.
  • Blob은 lazy하게 필요할 때 받음.
  • git log는 빠름.
  • git checkout은 느릴 수 있음 (blob 다운로드).

10.4 Sparse Checkout

워킹 디렉토리에 일부 디렉토리만 펼치기:

git sparse-checkout init
git sparse-checkout set src/feature-x docs

src/feature-xdocs만 체크아웃. 거대 monorepo에서 필수. Facebook, Google 같은 기업이 활용.

10.5 조합

git clone --filter=blob:none --sparse https://...
git sparse-checkout init
git sparse-checkout set src/mymodule
  • 필요한 blob만.
  • 필요한 디렉토리만.
  • 50GB 리포가 1GB 이하로.

10.6 Git LFS (Large File Storage)

또 다른 접근: 큰 파일을 별도 서버에 저장, git에는 포인터만.

.git/hooks에 LFS 필터 설치
→ 1GB 파일 대신 60바이트 포인터 저장
→ 실제 파일은 LFS 서버에서 다운로드

이미지, 영상, 빌드 결과물에 적합. 정상 diff 불가는 단점.


11. Git 프로토콜

11.1 Clone/Fetch 흐름

Client                                   Server
  │                                       │
"내 refs는 [A, B, C]야"  ────────────►│
  │                                       │
  │◄──────────── "내 refs는 [D, E, F]야"  │                                       │
"D, E, F가 필요해. 난 A, B가 있어" ──►│
  │                                       │
  │ ◄──── packfile (D, E, F에 필요한      │
  │         객체만, A/B 기반 delta로)  │                                       │

핵심:

  1. Refs 교환.
  2. "have" / "want" 협상.
  3. 서버가 packfile 생성 → 전송.

11.2 Smart HTTP

가장 흔한 방식 (GitHub, GitLab).

GET  /repo.git/info/refs?service=git-upload-pack
POST /repo.git/git-upload-pack

HTTP 위에 Git 고유 protocol. Firewall 친화적 (port 443).

11.3 SSH

ssh git@github.com "git-upload-pack 'user/repo.git'"

SSH 세션 위에 같은 protocol. 더 빠른 경우가 있다 (HTTP 오버헤드 없음).

11.4 Git Protocol (port 9418)

원본 git protocol. 인증 없음(익명 clone만). 거의 사용 안 됨.

11.5 Protocol V2 (2018+)

개선된 버전. capabilities^{} 협상, 더 효율적:

  • ls-refs 필터링: 원하는 ref만 요청.
  • 압축 개선.
  • Stateless: 각 요청이 독립.

GitHub/GitLab에서 기본. Git 2.18+.

11.6 Packfile 전송 최적화

서버는 reachability bitmap을 사용해 필요한 객체를 빠르게 계산.

Client: "wants [D]"
Server: D + parents + trees + blobs 를 bitmap로 계산
Server: 이미 Client가 가진 것은 제외
Server: Delta 압축해서 pack 생성

GitHub 같은 대형 호스팅은 pre-computed packfile + 사용자별 차이만 실시간 압축.


12. 디버깅과 탐색 도구

12.1 git cat-file

객체 내용 확인:

git cat-file -t abc123    # type
git cat-file -s abc123    # size
git cat-file -p abc123    # pretty-print
git cat-file --batch-all-objects --unordered  # 모든 객체 나열

12.2 git rev-list

Reachable commit 순회:

git rev-list HEAD         # HEAD에서 도달 가능한 모든 commit
git rev-list --count HEAD # commit 개수
git rev-list --objects HEAD  # commit + tree + blob 해시들

12.3 git verify-pack

Packfile 내용 분석:

git verify-pack -v .git/objects/pack/pack-abc123.idx
# 각 객체의 type, size, 압축 크기, 델타 체인 등

12.4 git fsck

무결성 검사:

git fsck --full
# dangling blob (레퍼런스 없는 blob)
# dangling commit (도달 불가)
# missing object (파손!)

12.5 git show-ref

모든 ref:

git show-ref
# abc123 refs/heads/main
# def456 refs/heads/feature
# ghi789 refs/tags/v1.0.0
# ...

12.6 git count-objects

저장소 통계:

git count-objects -v
# count: 3               ← loose objects
# size: 12               ← KB
# in-pack: 5000          ← packed objects
# packs: 1
# size-pack: 5000        ← KB

13. 흔한 실전 시나리오

13.1 "실수로 force push 했어요"

git reflog                 # 원래 상태 해시 찾기
git push --force-with-lease origin main:<원래 해시>

Reflog가 기록한 옛 상태로 돌아가기.

13.2 "brick wall 충돌"

대형 머지에서 수백 개 충돌:

git merge --abort          # 일단 취소

# 더 작은 단위로 나누기
git checkout feature
git rebase -i main         # 커밋을 정리
git checkout main
git merge feature          # 이제 충돌이 적음

13.3 "커밋을 둘로 나누고 싶어요"

git rebase -i HEAD~3
# 해당 커밋을 'edit'으로 표시

# rebase가 그 커밋에서 멈춤
git reset HEAD^            # 변경을 unstage
git add file1
git commit -m "첫 번째 부분"
git add file2
git commit -m "두 번째 부분"

git rebase --continue

13.4 "민감 정보를 실수로 커밋했어요"

# 최근 커밋이면
git reset --soft HEAD~1
# 파일 제거, 재커밋

# 오래전 커밋이면
git filter-branch --index-filter \
  'git rm --cached --ignore-unmatch secrets.txt' \
  HEAD
# 또는 BFG Repo-Cleaner 사용 (훨씬 빠름)

주의: force push 필요, 모든 clone이 영향받음. 비밀번호는 이미 노출됐다고 보고 즉시 교체.

13.5 "이 파일이 언제 추가됐지?"

git log --all --full-history --source -- <file>
git log --diff-filter=A -- <file>    # 추가 커밋만
git log -p -- <file>                  # 각 변경의 diff
git blame <file>                      # 줄별 최종 변경자

14. Git 2024-2025의 변화

14.1 git worktree 개선

여러 브랜치를 동시에 체크아웃:

git worktree add ../feature-x feature-x
# ../feature-x 디렉토리에서 feature-x 브랜치 작업

여러 폴더에서 동시에 작업 가능. Context switching 없이 여러 PR 검토.

14.2 Reftable

많은 ref가 있는 리포에서 refs/ 디렉토리가 느려진다. Reftable은 단일 파일 포맷:

  • O(log n) lookup.
  • 효율적 업데이트.
  • 2024년 실험적, 2025년 안정화 중.

14.3 Scalar

Microsoft의 대형 리포 최적화 도구. Git 2.38+에 통합:

scalar clone https://github.com/microsoft/windows.git

Partial clone + sparse checkout + background maintenance 자동 설정.

14.4 SSH Signing

GPG 대신 SSH 키로 커밋 서명:

git config gpg.format ssh
git config user.signingkey ~/.ssh/id_ed25519.pub
git commit -S -m "Signed"

GPG 세팅의 복잡함 없이 서명 가능. GitHub도 2022+ 지원.


15. 학습 리소스

:

  • "Pro Git" — Scott Chacon (공식 무료 책). https://git-scm.com/book
  • "Git in Practice" — Mike McQuaid.
  • "Git Internals" — Peepcode (얇지만 깊음).

온라인:

영상:

  • "Git From the Bits Up" — Tim Berglund.
  • Linus Torvalds 2007 Google Tech Talk (역사적 가치).

실습:

  • .git/ 디렉토리 탐험.
  • git cat-file로 객체 직접 읽기.
  • 토이 Git 구현해보기.

논문:

  • Jean-Philippe Aumasson et al. "SHAttered" (SHA-1 충돌).
  • Git mailing list 아카이브 (새 기능 논의).

16. 요약 — 한 장 정리

┌─────────────────────────────────────────────────────┐
Git Internals Cheat Sheet├─────────────────────────────────────────────────────┤
│ 본질:Content-addressable filesystem                      │
SHA-1 해시 → 객체                                    │
│                                                       │
│ 객체 타입:│   blob    : 파일 내용                                   │
│   tree    : 디렉토리                                    │
│   commit  : 스냅샷 + 메타                               │
│   tag     : annotated 태그                             │
│                                                       │
│ 저장:Loose: .git/objects/ab/cd... (개별 파일)Pack:  .git/objects/pack/*.pack (델타 압축)         │
│   Index: .idx (빠른 lookup)                           │
│   Bitmap: reachability 가속                           │
│                                                       │
│ Refs:                                                  │
│   refs/heads/<name> = 브랜치                           │
│   refs/tags/<name>  = 태그                             │
│   refs/remotes/origin/<name> = remote 상태            │
│   HEAD              = 현재                             │
│   packed-refs       = 최적화                           │
│                                                       │
│ Index:                                                 │
│   .git/index 바이너리                                  │
│   staging area                                         │
│   3트리 모델: HEAD / Index / Working                   │
│                                                       │
│ Merge:                                                 │
│   3-way: ancestor + ours + theirs                    │
│   Fast-forward: 선형 이력                              │
│   Ort algorithm (2021+)                               │
│   Rename detection                                    │
│                                                       │
│ Rebase:                                                │
│   Cherry-pick each commit                             │
│   새 해시 생성                                          │
│   interactive로 편집                                   │
│   Public 브랜치에 쓰지 말 것                           │
│                                                       │
│ Reflog:                                                │
│   모든 ref 변경 기록                                    │
│   .git/logs/                                          │
│   30-90일 유지                                         │
│   실수 복구의 안전망                                     │
│                                                       │
│ 대형 리포:                                              │
│   --depth=1 (shallow)                                 │
│   --filter=blob:none (partial)                        │
│   sparse-checkout                                     │
│   LFS (바이너리)                                       │
│                                                       │
│ 프로토콜:                                                │
│   Smart HTTP (기본)                                   │
│   SSH                                                 │
│   Protocol V2 (2018+)                                 │
│                                                       │
│ 도구:                                                  │
│   cat-file, rev-list, fsck                            │
│   verify-pack, count-objects                          │
│   reflog, show-ref                                    │
└─────────────────────────────────────────────────────┘

17. 퀴즈

Q1. Git의 4가지 객체 타입은 무엇이며 어떻게 연결되는가?

A. blob(파일 내용), tree(디렉토리 목록), commit(스냅샷 + 메타), tag(annotated 태그). 연결 구조: commit이 최상위 tree를 가리키고, tree가 하위 blob과 tree들을 가리킨다. Commit은 또한 parent commit(들)을 가리켜 이력 DAG를 형성. 모든 연결은 SHA-1 해시다. 핵심: tree는 재귀적 — 디렉토리의 디렉토리는 tree 안에 tree 엔트리로 표현. 같은 내용의 blob은 단 하나만 저장된다(중복 제거). 한 byte만 바꿔도 해시 체인 전체가 연쇄적으로 변경 → 무결성 보장.

Q2. Packfile의 델타 압축은 어떻게 작동하는가?

A. Packfile은 비슷한 객체들을 base + edit instructions 형태로 저장한다. Base 객체는 그대로 (zlib 압축), 그와 비슷한 객체는 "base의 offset X부터 N 바이트 copy" + "이 새 M 바이트 insert" 같은 지시어로 표현. 결과: 100MB 파일의 10 바이트 수정이 10 바이트 delta로 저장(원본 100MB 대신). Base 선택은 휴리스틱(같은 파일명 이전 버전, 크기 유사도 등). 체인 길이는 기본 50으로 제한(너무 길면 복원 느려짐). 이 때문에 git clone이 KB 단위로 동작 가능. Loose object만 썼다면 git clone이 GB 단위였을 것.

Q3. Index(스테이징 에어리어)가 왜 존재하는가?

A. 세 가지 이유: (1) 부분 커밋git add file1로 선택적 커밋 가능. 변경된 여러 파일 중 일부만 커밋하고 싶을 때 필수. (2) 성능 — Index는 각 파일의 inode, mtime, 크기를 캐시해서 git status가 실제 파일 diff를 돌리지 않고도 변경을 감지. 수만 파일 리포에서 밀리초 단위 응답. (3) 머지 중간 상태 — 충돌 시 index에 여러 stage(ancestor/ours/theirs)를 저장해 git checkout --theirs file 같은 명령을 가능하게 한다. "3-tree 모델"(HEAD, Index, Working)에서 Index는 "다음 커밋이 될 상태"를 명시적으로 만드는 레이어.

Q4. Rebase가 public 브랜치에서 위험한 이유는?

A. Rebase는 커밋의 해시를 변경한다. 같은 변경 내용이라도 parent가 바뀌면 SHA-1이 완전히 달라짐 → 실질적으로 새 커밋. 다른 개발자가 옛 해시 기반으로 이미 작업하고 있었다면 그 사람의 브랜치가 "사라진" 커밋에 의존하는 상황이 된다. git pull이 충돌 또는 중복 커밋 생성. --force push로 밀어버리면 옛 해시가 remote에서 지워져 상황 더 악화. 해결 원칙: "한 명만 보는 브랜치"에서만 rebase. 공유 브랜치는 merge로 합쳐야 이력이 보존. --force-with-lease는 "원격이 내가 본 상태 그대로일 때만" 조건을 걸어 최소한의 안전망.

Q5. Reflog가 실수 복구의 안전망인 이유는?

A. Reflog는 HEAD와 모든 ref의 변경 이력을 로컬에 기록한다. git reset --hard, git rebase, git checkout, git commit 등 모든 ref 변경이 타임스탬프와 함께 .git/logs/ 아래에 저장. 중요한 것: "객체는 바로 삭제되지 않는다"git reset으로 커밋이 현재 ref에서 사라져도 reflog에 해시가 남아있으면 객체는 그대로 .git/objects/ 에 존재. 따라서 git reflog로 옛 해시 찾아 git reset --hard <hash>로 복구 가능. 기본 30-90일 보관(reachable/unreachable). 이 때문에 Git에서 "완전히 잃어버렸다"는 경우가 거의 없다. 'git gc`가 실행되기 전까진 살아있다.

Q6. Partial clone (--filter=blob:none)과 Shallow clone의 차이는?

A. Shallow clone (--depth=N)은 최근 N개 commit만 받는다. 이력이 잘려있어 git log가 제한적이고 일부 git pull이 작동 안 함. 과거 커밋을 못 봄. CI/CD에 주로 사용. Partial clone (--filter=blob:none)은 모든 commit과 tree는 받지만 blob은 lazy하게 받는다. git log 완전히 가능, 이력 탐색 OK. git checkout이나 git show로 특정 파일 내용이 필요할 때만 서버에서 blob 다운로드. 대형 repo를 경량으로 다루는 더 유연한 방법. Sparse checkout과 조합하면 50GB 리포가 1GB 이하로 작동. Microsoft Windows 리포가 이 방식으로 운영된다.

Q7. Ort 알고리즘이 recursive를 대체한 이유는?

A. 성능과 정확성 두 가지. Recursive는 criss-cross merge(공통 조상이 여러 개인 상황)에서 조상들을 재귀적으로 merge하는데, 이 과정이 대형 레포에서 지수적으로 느려질 수 있었다. 극단적 케이스에서 merge가 시간 초과되거나 메모리 폭발. Ort(Ours/Recursive/Theirs)는 Elijah Newren이 2021년 다시 작성한 알고리즘으로: (1) 500배 빠른 경우도 있고, (2) rename detection이 정확(파일 이동 후 편집 처리), (3) 메모리 효율 대폭 개선. Git 2.33(2021)부터 기본. 대부분 개발자는 변경을 눈치채지 못했다 — merge가 그냥 빨라졌다. "사용자가 눈치채지 못하는 좋은 변화"의 전형적 예.


이 글이 도움이 됐다면 다음 포스트도 확인해 보세요:

  • "Binary Serialization Protobuf/Thrift/Avro/FlatBuffers" — Git의 바이너리 포맷과 비교.
  • "Docker BuildKit & Image Layers Deep Dive" — 또 다른 content-addressable 시스템.
  • "RocksDB & LSM-Tree Deep Dive" — append-only + background compaction 철학.
  • "Consistent Hashing & Virtual Nodes" — content addressing의 분산 버전.

Git Internals Deep Dive — Object Model, Packfile, Merge Algorithms, Reflog, Protocol (2025)

TL;DR

  • Git is a content-addressable filesystem. Version control is a layer built on top. Every piece of data (files, directories, commits) is addressed by SHA-1 hash.
  • Four object types: blob (file content), tree (directory), commit (snapshot + metadata), tag (signed reference).
  • Loose object vs Packfile: each object starts as its own file (.git/objects/ab/cd...), later packed and delta-compressed.
  • Refs: branches/tags are just text files pointing at commit hashes. HEAD points to whatever is currently checked out.
  • Index: staging area. A binary file that pre-builds the next commit's tree.
  • Merge: in 2021 the default changed from recursive to ort. Up to 500x faster in some cases.
  • Rebase: cherry-picks commits one by one onto a new base. Changes commit hashes.
  • Reflog: complete history of HEAD and refs. The secret to recovering "lost commits."
  • Packfile: delta compression + index (.idx) + bitmap (.bitmap). Makes push/fetch KB-scale.
  • Protocol: Smart HTTP (most common), SSH, Git protocol. Client declares "what I have" and server sends only what's needed.

1. Git's Philosophy — "A Filesystem, Not Version Control"

In 2005, BitKeeper (then used by the Linux kernel) revoked its free license. Linus built Git in two weeks. Design goals: distributed, fast, data-integrity, first-class branching/merging.

Linus's insight: existing VCS picked the wrong abstraction. Instead of storing diffs between files, Git stores snapshots.

Content-Addressable Filesystem

At its core, Git is a key-value store:

SHA-1 hash → object content

Store: "save this content" → returns a SHA-1. Retrieve: "give me the content for this hash" → returns the object. Everything else (branches, merges, history) is built on top.

mkdir /tmp/gitrepo && cd /tmp/gitrepo
git init
echo "Hello, Git" | git hash-object -w --stdin
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad

git cat-file -p 3b18e512
# Hello, Git

hash-object -w prepends a header, computes SHA-1, zlib-compresses, and stores under .git/objects/3b/18e5.../.


2. The Object Model

2.1 Four Types

  • Blob: pure file content (no metadata). Same content = same blob.
  • Tree: directory listing. Maps name → mode → blob/tree hash.
  • Commit: snapshot (tree pointer) + metadata (parent, author, message).
  • Tag: annotated tag. Object pointing at a commit with a signature/message.

2.2 Blob

Format: blob <byte_length>\0<content>, then SHA-1, then zlib. 10,000 files with identical "Hello World" contents share one blob.

2.3 Tree

tree <length>\0
100644 blob abc123... README.md
040000 tree def456... src
100755 blob ghi789... build.sh

Each entry: mode (100644 regular, 100755 executable, 040000 tree, 120000 symlink, 160000 submodule), type, SHA-1 (20 bytes binary), null-terminated name.

git cat-file -p HEAD^{tree}

Trees compose recursively — directory of directories is tree containing trees.

2.4 Commit

commit <length>\0
tree 3c4e9cd789...
parent 5a9d8b41...
author Linus Torvalds <torvalds@linux.org> 1234567890 -0700
committer Linus Torvalds <torvalds@linux.org> 1234567890 -0700

Initial commit

Fields: tree (full snapshot), parent(s) (initial has none, merges have 2+), author (who made change), committer (who applied — differs on rebase/cherry-pick), message.

2.5 Tag

Annotated tag (git tag -a) is a real object with object, type, tag, tagger, message, and optional PGP signature. Lightweight tags are just refs.

2.6 Hash Chain

commit → tree → blob
              → blob
              → tree → blob
commit → parent commit → parent commit → ...

Every edge is a hash. Change one byte and every ancestor hash shifts — Git's integrity guarantee.

2.7 SHA-1 vs SHA-256

Git originally used SHA-1. After Google proved SHAttered (SHA-1 collision) in 2017, SHA-256 support was added (git init --object-format=sha256). The two are not interoperable. Most projects still use SHA-1.

2.8 Loose Object Storage

.git/objects/
├── 3b/
│   └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
├── 5a/
│   └── 9d8b41...

First 2 hex chars form directory, remaining 38 form filename — 256 buckets to avoid filesystem slowdown with huge directories.


3. Refs — Meaningful Names

3.1 Ref = Alias for a Hash

cat .git/refs/heads/main
# 3c4e9cd789abc...

That's it. A branch is one text file containing a commit hash.

3.2 HEAD

cat .git/HEAD
# ref: refs/heads/main

Detached HEAD holds a commit hash directly.

3.3 Tag and Remote Refs

Lightweight tags point at commits; annotated tags point at tag objects. Remote refs live under .git/refs/remotes/origin/ and hold the last fetched state, fully independent from local branches.

3.4 packed-refs

With many refs, Git packs them into a single .git/packed-refs file. Git consults both unpacked files and packed-refs (unpacked wins).

3.5 Symbolic Ref

A ref may point at another ref (HEAD being the canonical example). See with git symbolic-ref HEAD.


4. Index — The Staging Area

4.1 Role

Working directory  (git add)Index  (git commit)Commit

The index pre-builds the next commit's tree.

4.2 Structure

.git/index is binary: header + entries (ctime, mtime, dev, ino, mode, uid, gid, size, SHA-1, flags, path) + checksum.

git ls-files --stage
# 100644 a906cb2a4a904a15... 0 README.md
# 100644 3b18e512dba79e4c... 0 src/main.c

4.3 Why an Index?

  1. Partial commits: stage only a subset.
  2. Performance: file change detection via inode/mtime/size is cheap.
  3. Merge state: on conflict, the index stores multiple stages (1 ancestor, 2 ours, 3 theirs) per path.

4.4 The Three-Tree Model

HEAD              Index            Working Tree
(last commit)    (staged)        (unstaged)

git status compares HEAD vs Index ("Changes to be committed") and Index vs Working Tree ("Changes not staged").


5. Packfiles — Efficient Storage

5.1 Loose Object Limits

10,000 files with 10 modifications = 100,000 loose objects (inode waste). Worse: no delta compression — a 10-byte edit of a 100MB file produces a new 100MB blob.

5.2 Packfile Layout

.git/objects/pack/
├── pack-abc123.idx     # index for offset lookup
├── pack-abc123.pack    # actual data
├── pack-abc123.bitmap  # reachability bitmap (optional)

5.3 Pack Format

Header (PACK + version + count), objects (type + compressed data; deltas carry OFS_DELTA or REF_DELTA base ref), 20-byte checksum.

5.4 Delta Compression

If object A is similar to B:

Pack's B: full content (zlib-compressed)
Pack's A: "reference B, then apply these edit instructions"

Edit instructions: COPY (offset X, N bytes from base) or INSERT (N new bytes).

Example: "Hello, World" to "Hello, Git":

A = COPY(0, 7) + INSERT("Git") + COPY(12, 1)

5.5 Base Object Selection

Heuristics: same filename previous version, similar size, prefix match. git repack -f forces rebuild.

5.6 Delta Chain

V1 (full) ← V2 (delta of V1) ← V3 (delta of V2) ← ...

Walk backwards to reconstruct. Chains are capped (default pack.depth=50).

5.7 Pack Index (.idx)

Fanout table (256 entries keyed on first byte) + sorted SHA-1 list + CRC32 + offsets. Lookup is O(log n) via binary search.

5.8 Reachability Bitmap

Git 2.0+. Each selected commit stores a bitmap of reachable objects. git fetch computes "need X, already have Y" instantly. Why GitHub git clone is fast.


6. Object Storage Operations

6.1 Create

git add file.txt: read → compute blob hash → check .git/objects/ → write if absent → update index.

6.2 Commit

git commit -m "...": build tree objects from index (recursive per dir) → create commit object with tree, parent, author, message → update HEAD's ref.

6.3 Checkout

git checkout main: read refs/heads/main → read its commit tree → materialize tree into working dir → sync index → update HEAD.

6.4 GC and Prune

git gc

Compacts loose objects into packfiles and deletes unreachable objects older than 2 weeks. Auto-triggers at ~6700 loose objects. git prune --expire=now purges everything unreachable (reflog entries keep objects alive).


7. Merge Algorithms

7.1 Three-way Merge

  • Common Ancestor: shared ancestor commit.
  • Ours: current branch tip.
  • Theirs: branch being merged.

Per file: only Ours changed → Ours; only Theirs → Theirs; both same change → that change; both diverge → conflict.

7.2 Fast-forward

When main is a strict ancestor of branch, main is simply advanced. No merge commit.

7.3 Three-way Merge Commit

When histories diverge, Git creates a merge commit M with two parents.

7.4 Recursive to Ort

Until 2021, Git's default was recursive. Criss-cross merges (multiple common ancestors) recursed into merging ancestors — sometimes pathologically slow.

Ort (Ours/Recursive/Theirs), rewritten by Elijah Newren: up to 500x faster, better rename detection, improved subtree merges, much lower memory. Default from Git 2.33 (2021). Most developers never noticed — merges just got faster.

7.5 Rename Detection

Renamed file (50%+ content similarity) is tracked as a rename, so edits on another branch apply to the new name, not the old. Ort makes this faster and more accurate.

7.6 Conflict Resolution

<<<<<<< HEAD
int x = 1;
=======
int x = 2;
>>>>>>> branch

Edit, git add, git commit. Multi-stage index entries drive this.

7.7 Strategy Options

  • -X ours / -X theirs: auto-pick on conflict.
  • -X ignore-all-space: ignore whitespace.
  • -X rename-threshold=80: tune rename detection.

Strategies (-s): ort (default), recursive (legacy), resolve, octopus (multi-branch, conflict-free only), ours (discard Theirs content).


8. Rebase Mechanics

8.1 Idea

main:    ABC
branch:  ABDE

After git rebase main on branch:

main:    ABC
branch:  ABCD' → E'

D', E' are new commits — same changes, different parent, different hashes.

8.2 How It Works

  1. Start at main's tip as temporary HEAD.
  2. List branch-only commits in order.
  3. Cherry-pick each: compute ancestor, three-way merge, create new commit with new parent.
  4. Move the branch ref to the new tip.

8.3 Interactive Rebase

git rebase -i HEAD~5

Actions: pick, reword, edit, squash, fixup, drop. History rewrite — new hashes.

8.4 Danger on Public Branches

Rebase changes hashes. Anyone working on old hashes will have divergent history after git push --force. Rebase only your own branches; merge shared ones.

8.5 Force-with-lease

git push --force-with-lease

Refuses the push if the remote advanced since you last fetched — prevents clobbering teammates' work.


9. Reflog — The Safety Net

9.1 What It Is

Local log of every HEAD/ref change. Even after git reset --hard or git rebase, you can recover.

git reflog
# abc123 (HEAD -> main) HEAD@{0}: commit: Add feature
# def456 HEAD@{1}: commit: Fix bug
# ghi789 HEAD@{2}: rebase finished

9.2 Recovery Example

Accidentally dropped a commit with reset:

git reflog
# abc123 HEAD@{1}: commit: the lost one
# def456 HEAD@{0}: reset: moving to HEAD~1

git reset --hard abc123

Objects remain in .git/objects/ as long as reflog holds the hash.

9.3 Storage and Expiry

.git/logs/HEAD and .git/logs/refs/.... Default expiry: 90 days for reachable, 30 days for unreachable. git gc cleans up.

9.4 Recovery Routine

Don't panic → git reflog → find hash → git reset --hard <hash> or git cherry-pick <hash>. Within 30 days, almost any mistake is recoverable.


10. Partial Clone and Sparse Checkout

10.1 Big Repos

Linux kernel: 3GB. Chromium: 50GB. Full clones waste time and space.

10.2 Shallow Clone

git clone --depth=1 https://github.com/...

Latest commit only. Common in CI. Limits some git pull workflows.

10.3 Partial Clone (Git 2.19+)

git clone --filter=blob:none https://github.com/...

All commits + trees; blobs fetched lazily on demand.

10.4 Sparse Checkout

git sparse-checkout init
git sparse-checkout set src/feature-x docs

Only specified directories materialize. Essential for monorepos.

10.5 Combined

git clone --filter=blob:none --sparse https://...
git sparse-checkout init
git sparse-checkout set src/mymodule

50GB repo can operate in under 1GB.

10.6 Git LFS

Stores large files on a separate server; Git tracks only a pointer (~60 bytes). Good for media/binaries, but no real diff.


11. Git Protocols

11.1 Clone/Fetch Flow

  1. Refs exchange.
  2. "have"/"want" negotiation.
  3. Server builds and sends a packfile.

11.2 Smart HTTP

Most common (GitHub, GitLab):

GET  /repo.git/info/refs?service=git-upload-pack
POST /repo.git/git-upload-pack

Firewall-friendly over port 443.

11.3 SSH

ssh git@github.com "git-upload-pack 'user/repo.git'"

Same protocol over SSH — often faster (no HTTP overhead).

11.4 Git Protocol (port 9418)

Original, anonymous-only, rarely used.

11.5 Protocol V2 (2018+)

Capability negotiation, ls-refs filtering, better compression, stateless. Default in GitHub/GitLab, Git 2.18+.

11.6 Transfer Optimization

Server uses reachability bitmaps to compute needed objects instantly, skipping what the client already has, then delta-compresses into a pack. Large hosts pre-compute packs plus per-user deltas.


12. Debugging and Inspection Tools

git cat-file -t abc123    # type
git cat-file -s abc123    # size
git cat-file -p abc123    # pretty-print
git cat-file --batch-all-objects --unordered

git rev-list HEAD
git rev-list --count HEAD
git rev-list --objects HEAD

git verify-pack -v .git/objects/pack/pack-abc123.idx
git fsck --full
git show-ref
git count-objects -v

13. Common Real-World Scenarios

13.1 Accidental Force Push

git reflog
git push --force-with-lease origin main:<old-hash>

13.2 Brick-wall Conflict

git merge --abort
git checkout feature
git rebase -i main
git checkout main
git merge feature

13.3 Split a Commit

git rebase -i HEAD~3
# mark target commit 'edit'
git reset HEAD^
git add file1 && git commit -m "first half"
git add file2 && git commit -m "second half"
git rebase --continue

13.4 Committed a Secret

git reset --soft HEAD~1   # if recent
# or, for older commits:
git filter-branch --index-filter \
  'git rm --cached --ignore-unmatch secrets.txt' HEAD
# or use BFG Repo-Cleaner

Needs force push; assume the secret is already leaked and rotate it immediately.

13.5 When Was This File Added?

git log --all --full-history --source -- <file>
git log --diff-filter=A -- <file>
git log -p -- <file>
git blame <file>

14. Changes in 2024-2025 Git

14.1 Worktree Improvements

git worktree add ../feature-x feature-x

Multiple branches checked out in parallel — no context-switch pain.

14.2 Reftable

New single-file ref storage with O(log n) lookup; experimental in 2024, stabilizing in 2025.

14.3 Scalar

Microsoft's big-repo wrapper (integrated into Git 2.38+): partial clone + sparse checkout + background maintenance configured automatically.

14.4 SSH Signing

git config gpg.format ssh
git config user.signingkey ~/.ssh/id_ed25519.pub
git commit -S -m "Signed"

Sign commits with SSH keys — no GPG setup. GitHub supports since 2022.


15. Summary Cheat Sheet

Essence: Content-addressable filesystem, SHA-1 hash → object
Objects: blob (content), tree (dir), commit (snapshot+meta), tag
Storage: loose (.git/objects/ab/cd...) / pack (*.pack+*.idx+*.bitmap)
Refs:    refs/heads, refs/tags, refs/remotes, HEAD, packed-refs
Index:   .git/index binary, three-tree model (HEAD/Index/Working)
Merge:   3-way, fast-forward, ort (2021+), rename detection
Rebase:  cherry-pick new commits, new hashes, not on public branches
Reflog:  local history of ref changes, 30-90 day retention
Big repo: --depth=1, --filter=blob:none, sparse-checkout, LFS
Protocol: Smart HTTP / SSH / Protocol V2
Tools:   cat-file, rev-list, fsck, verify-pack, count-objects, reflog

16. Quiz

Q1. What are the four Git object types and how do they link?

A. blob (file contents), tree (directory listing), commit (snapshot + metadata), tag (annotated tag). A commit points at a top-level tree; trees point at blobs and sub-trees. Commits also point at parent commit(s), forming a DAG. Every edge is a SHA-1 hash. Trees are recursive. Identical blobs are stored once (deduplication). Any byte change cascades through every ancestor hash — the integrity guarantee.

Q2. How does packfile delta compression work?

A. Similar objects are stored as base + edit instructions. The base is full (zlib-compressed); similar objects say "copy offset X for N bytes from base" and "insert these M new bytes." A 10-byte edit of a 100MB file costs ~10 bytes instead of a new 100MB blob. Base selection is heuristic (same filename previous version, size similarity). Chain depth is capped (default 50) to bound reconstruction cost. This is why git clone is KB-scale.

Q3. Why does the index exist?

A. Three reasons: (1) Partial commits — stage only selected files. (2) Performance — the index caches inode/mtime/size so git status avoids full diffs. (3) Merge state — during conflicts the index holds multiple stages (ancestor/ours/theirs) per path, enabling git checkout --theirs file. In the three-tree model (HEAD/Index/Working), the index is the explicit "what the next commit will be" layer.

Q4. Why is rebase dangerous on public branches?

A. Rebase changes commit hashes. Same changes, different parent, entirely new SHA-1 — effectively new commits. If teammates branched off the old hashes, their work depends on commits that "disappear" after a force push. Shared branches should be merged, not rebased. --force-with-lease narrows the window by refusing push if the remote advanced since you last fetched.

Q5. Why is reflog a safety net for mistakes?

A. Reflog records every HEAD/ref update locally with timestamps under .git/logs/. Objects are not immediately deleted — even after git reset --hard, the blob/commit still lives in .git/objects/ as long as the reflog references it. So git reflog + git reset --hard <hash> recovers almost anything within 30-90 days. Mistakes are nearly always reversible until git gc actually prunes.

Q6. Partial clone vs shallow clone — what's the difference?

A. Shallow (--depth=N) fetches only the last N commits; history is truncated and some git pull paths break. Partial (--filter=blob:none) fetches all commits and trees but fetches blobs lazily — full log history, file contents on demand. Combined with sparse checkout a 50GB repo can operate under 1GB. Microsoft Windows runs on this model.

Q7. Why did ort replace recursive?

A. Performance and correctness. Recursive could explode on criss-cross merges (recursively merging multiple ancestors), slowing or OOM-ing on large repos. Ort (Ours/Recursive/Theirs), rewritten by Elijah Newren, is up to 500x faster, has more accurate rename detection, and uses far less memory. Default from Git 2.33 (2021). Most developers never noticed — merges just got faster.


If this was useful, check out:

  • "Binary Serialization: Protobuf/Thrift/Avro/FlatBuffers" — compare with Git's binary formats.
  • "Docker BuildKit & Image Layers Deep Dive" — another content-addressable system.
  • "RocksDB & LSM-Tree Deep Dive" — append-only + background compaction philosophy.
  • "Consistent Hashing & Virtual Nodes" — distributed content addressing.