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년
recursive→ort기본 알고리즘 변경. 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.
설계 목표:
- 분산: 모든 clone이 완전한 리포지토리. 중앙 서버 불필요.
- 빠름: 커널 크기의 리포지토리에서도 초 단위 작동.
- 무결성: 데이터 손상 즉시 감지.
- 동시 개발 지원: 브랜치/머지가 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는:
- 입력 내용에 헤더 추가.
- SHA-1 계산.
- zlib 압축.
.git/objects/3b/18e5.../에 저장.- 해시 출력.
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
이 명령이 하는 일:
- file.txt 읽기.
- Blob 해시 계산 (
blob <size>\0<content>SHA-1). .git/objects/해시 경로에 이미 있는지?- 있음: skip.
- 없음: zlib 압축해서 저장.
- Index 업데이트 (blob 해시 기록).
6.2 커밋 생성
git commit -m "Add feature"
- Index의 파일들로 tree 객체들 생성 (재귀적으로 디렉토리마다).
- 최상위 tree 해시.
- Commit 객체 생성: tree + parent + author + message.
- Commit 해시 계산.
HEAD가 가리키는 ref 업데이트.
6.3 Checkout
git checkout main
refs/heads/main의 해시 읽기.- 그 commit의 tree 읽기.
- Tree 순회하며 working directory 갱신.
- Index도 tree와 일치하도록 업데이트.
- 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: A → B → C
branch: A → B → C → D → E
git merge branch하면 main을 E로 그냥 이동. 머지 커밋 없음. "Fast forward".
7.3 3-way Merge Commit
main: A → B → C → D
branch: A → B → E → F
공통 조상 B. 머지 결과:
main: A → B → C → D → M
↑
branch: A → B → E → F → ┘
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: A → B → C
branch: A → B → D → E
git rebase main:
main: A → B → C
branch: A → B → C → D' → E'
D'와 E'는 새 commit. 같은 변경이지만 다른 parent → 다른 해시.
8.2 실제 동작
내부적으로 Git이 하는 일:
main의 최신 commit을 임시 HEAD로.branch의 commits를 순서대로D,E나열.- 각 commit을
cherry-pick:- Ancestor 계산 (이전 commit).
- 3-way merge 수행.
- 새 commit 생성 (새 parent).
- 모든 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 루틴
"아, 실수했다" 순간:
- 당황하지 말 것. Reflog가 살려줄 가능성이 높다.
git reflog실행.- 복구하려는 지점 해시 확인.
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-x와 docs만 체크아웃. 거대 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로) │
│ │
핵심:
- Refs 교환.
- "have" / "want" 협상.
- 서버가 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 (얇지만 깊음).
온라인:
- https://git-scm.com/docs — 공식 reference.
- https://learngitbranching.js.org — 인터랙티브 튜토리얼.
- "A Hacker's Guide to Git" — Wildly Inaccurate 블로그.
영상:
- "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.
HEADpoints 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
recursivetoort. 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?
- Partial commits: stage only a subset.
- Performance: file change detection via inode/mtime/size is cheap.
- 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: A → B → C
branch: A → B → D → E
After git rebase main on branch:
main: A → B → C
branch: A → B → C → D' → E'
D', E' are new commits — same changes, different parent, different hashes.
8.2 How It Works
- Start at main's tip as temporary HEAD.
- List branch-only commits in order.
- Cherry-pick each: compute ancestor, three-way merge, create new commit with new parent.
- 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
- Refs exchange.
- "have"/"want" negotiation.
- 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.