Skip to content

iq-dev-lab/git-in-depth

Repository files navigation

🌳 Git In-Depth

"git commit 한 줄을 치는 것과, .git/objects/ 안에 blob/tree/commit 객체가 어떤 순서로 만들어지는지 아는 것은 다르다"


"git commit -m을 매일 쓰지만 .git/objects/에 무엇이 들어있는지 본 적 없다 — 와 — Git이 사실 SHA-1로 주소화된 분산 KV 스토어라는 것, branch가 그저 40바이트 텍스트 파일이라는 것, merge가 LCA(Lowest Common Ancestor) 기준 3-way 비교라는 것을 아는 것의 차이를 만드는 레포"

git commit 한 줄이 어떻게 blob → tree → commit 객체 생성 + ref 업데이트라는 4단계 작업으로 분해되는지, git merge가 base / ours / theirs 세 트리를 어떻게 결합하는지, force push 후 reflog로 잃어버린 커밋을 어떻게 복구하는지까지 왜 이렇게 동작하는가 라는 질문으로 Git 내부를 plumbing 명령어와 .git/ 디렉토리 직접 해부로 끝까지 파헤칩니다


GitHub Git Plumbing Docs License


🎯 이 레포에 대하여

Git에 관한 자료는 넘쳐납니다. 하지만 대부분은 "어떤 명령어를 쓰나" 에서 멈춥니다.

일반 자료 이 레포
"git commit -m으로 커밋을 만듭니다" git commit → blob 객체 생성(hash-object -w) → index 업데이트(update-index) → tree 객체 생성(write-tree) → commit 객체 생성(commit-tree) → ref 업데이트(update-ref)까지 4단계로 분해, plumbing 명령어로 직접 재현
"Branch는 포인터입니다" Branch가 .git/refs/heads/<name>에 저장된 40바이트 ASCII 텍스트 파일이라는 점, git branch foo가 사실 update-ref refs/heads/foo HEAD 한 줄과 동치임을 직접 확인
"Merge는 두 브랜치를 합칩니다" LCA(Lowest Common Ancestor) 알고리즘으로 base를 찾고, 각 파일에 대해 (ours == base, theirs != base) → take theirs 같은 결정 트리를 적용하는 3-way merge 알고리즘, recursive에서 ort(Git 2.34+)로 바뀌며 무엇이 빨라졌는지
"Rebase는 히스토리를 깔끔하게 합니다" Rebase가 새 커밋을 만드는 이유(commit은 immutable), interactive rebase의 .git/rebase-merge/git-rebase-todo 파일 구조, pick/squash/fixup/drop 각각이 객체 그래프에 일으키는 변화
"Reflog로 복구할 수 있습니다" .git/logs/HEAD.git/logs/refs/heads/<branch> 파일이 매 ref 변경마다 한 줄씩 추가되는 구조, gc.reflogExpire(기본 90일) 만료 후 git fsck --lost-found로 unreachable 객체를 다시 찾는 방법
"LFS로 큰 파일을 관리합니다" LFS의 본질은 pointer 객체(작은 텍스트) + 외부 스토리지 + Smudge/Clean Filter, .git/objects/에는 SHA를 가리키는 텍스트만 남고 실제 파일은 LFS 서버에 올라간다는 점, partial clone(--filter=blob:none)이 동일 문제를 다른 방식으로 해결
명령어 사용법 나열 Plumbing 명령어(cat-file, hash-object, update-ref, write-tree, commit-tree, ls-files, rev-list)로 .git/ 직접 해부 + 객체 그래프 변화 시각화

🚀 빠른 시작

각 챕터의 첫 문서부터 바로 학습을 시작하세요!

Ch1 Ch2 Ch3 Ch4 Ch5 Ch6 Ch7


📚 전체 학습 지도

💡 각 섹션을 클릭하면 상세 문서 목록이 펼쳐집니다


🔹 Chapter 1: Git Object Model — Content-Addressable Storage

핵심 질문: .git/objects/에는 무엇이 어떤 포맷으로 저장되는가? Git의 4가지 객체(blob, tree, commit, tag)는 어떻게 구조화되어 있고, SHA-1 해시는 정확히 무엇을 입력으로 받는가? Loose object와 pack file은 언제 어떻게 전환되는가?

.git 디렉토리 해부부터 Merkle Tree와 GC까지 (8개 문서)
문서 다루는 내용
01. .git 디렉토리 완전 해부 HEAD, config, refs/, objects/, index, hooks/, info/, logs/, packed-refs 각각의 역할과 파일 포맷, tree .git으로 새 레포 초기 상태와 커밋 후 상태 비교, 어떤 파일이 수동 편집 가능하고 어떤 파일은 바이너리인지 구분
02. 4가지 객체의 구조 — blob, tree, commit, tag 각 객체의 직렬화 포맷(<type> <size>\0<content>), git cat-file -t/-p로 객체 종류와 내용 확인, tree 객체가 mode + name + SHA를 어떻게 인코딩하는지(NUL 구분자), commit 객체의 line-based 헤더 포맷
03. SHA-1 해시 생성 알고리즘 git hash-object<type> <size>\0<content>를 SHA-1에 통과시키는 과정, 직접 printf 'blob 6\0hello\n' | sha1sum으로 동일한 해시가 나오는지 확인, SHA-1 충돌 가능성과 SHA-256 마이그레이션(Git 2.29+ 실험적 지원)
04. Content-Addressable Storage 패턴 Git이 사실 KV 스토어(key=SHA, value=객체)라는 관점, 동일 내용이면 자동 dedup되는 원리(같은 SHA → 같은 파일), 이 설계가 왜 무결성과 효율을 동시에 달성하는지, IPFS / DDB 같은 다른 CAS 시스템과의 비교
05. Loose Object vs Pack File Loose object(.git/objects/ab/cdef...)의 zlib 압축 구조와 fanout 디렉토리 명명 규칙, git gc/git repack이 loose를 pack으로 묶는 시점, .idx 파일의 fanout table로 O(log n) 객체 검색이 가능한 이유
06. Delta Compression 알고리즘 Pack file 내부에서 비슷한 객체를 base + delta로 저장하는 xdelta 알고리즘, base object 선택 휴리스틱(파일명 / 크기 / 타입 유사도), git verify-pack -v로 delta chain 길이 확인, depth가 너무 길면 checkout이 느려지는 이유
07. Reachability와 Garbage Collection Reachability 정의(refs와 reflog로부터 도달 가능한 객체), git fsck가 dangling/unreachable 객체를 어떻게 찾는지, git gc의 자동 트리거 조건(gc.auto), prune 만료 정책(gc.pruneExpire) — "왜 force push 후에도 한동안은 복구 가능한가"
08. Merkle Tree 구조와 무결성 보장 Commit이 tree SHA를 참조하고 tree가 blob/sub-tree SHA를 참조하는 Merkle 구조, 어느 한 byte라도 바뀌면 모든 상위 SHA가 바뀌는 cascade 효과, 이 설계가 어떻게 분산 환경에서 무결성을 자동 보장하는지(Bitcoin / IPFS 와의 공통점)

🔹 Chapter 2: References & HEAD

핵심 질문: Branch와 tag의 본질은 무엇인가? HEAD는 어떻게 "현재 위치"를 표현하는가? packed-refs는 언제 만들어지고 왜 필요한가?

refs 구조부터 Detached HEAD 안전 활용까지 (5개 문서)
문서 다루는 내용
01. refs 디렉토리 구조 refs/heads/(로컬 브랜치), refs/tags/(태그), refs/remotes/<remote>/(원격 브랜치 추적) 각 디렉토리의 의미, 각 ref가 단일 SHA만 들어있는 텍스트 파일이라는 점, cat .git/refs/heads/main으로 직접 확인
02. HEAD의 본질 — Symbolic vs Direct Reference HEAD가 보통 ref: refs/heads/main 형태의 심볼릭 참조(branch 이름)이지만, detached 상태에서는 SHA를 직접 가리키는 direct reference로 바뀌는 점, git symbolic-ref HEAD/git rev-parse HEAD 차이, cat .git/HEAD 직접 확인
03. packed-refs 파일과 ref 압축 수천 개 ref가 쌓이면 inode 부담을 덜기 위해 git pack-refsrefs/heads/의 파일들을 단일 packed-refs로 묶는 메커니즘, loose ref와 packed-refs가 동시 존재할 때의 우선순위, gc 시 자동 packing 조건
04. 특수 참조 — ORIG_HEAD, FETCH_HEAD, MERGE_HEAD, CHERRY_PICK_HEAD 각 특수 ref가 언제 생성되고 언제 삭제되는지(예: merge 시작 시 MERGE_HEAD 생성, 완료 시 삭제), git reset --hard ORIG_HEAD로 직전 작업 취소가 가능한 이유, git fetch 후 FETCH_HEAD 활용 패턴
05. Detached HEAD 상태 분석 Detached HEAD가 만들어지는 시나리오(git checkout <SHA>, git checkout <tag>), 이 상태에서 만든 커밋이 어떤 ref에도 연결되지 않아 GC 대상이 되는 위험, 안전한 활용 패턴(이등분 검색 git bisect, 임시 실험), 탈출 방법(git switch -c <name>)

🔹 Chapter 3: Index와 Working Directory

핵심 질문: .git/index는 정확히 무엇을 저장하는가? git status는 어떤 3가지를 비교해서 결과를 만드는가? git add는 내부적으로 어떤 plumbing 명령어로 분해되는가?

index 바이너리 포맷부터 .gitignore 매칭 알고리즘까지 (6개 문서)
문서 다루는 내용
01. .git/index 바이너리 파일 포맷 Index 헤더(DIRC magic + version + entry count), 각 entry의 ctime/mtime/mode/uid/gid/size/SHA/flags/path 필드, git ls-files --stage로 가독화, xxd .git/index | head 직접 hexdump 분석
02. 3 Tree 모델 — HEAD Tree vs Index vs Working Directory 세 영역을 독립적으로 비교하는 Git의 모델, git diff(working↔index), git diff --cached(index↔HEAD), git diff HEAD(working↔HEAD)로 어느 두 영역을 비교하는지 명확히 구분
03. git status가 비교하는 3가지 HEAD↔Index(staged 변경), Index↔Working(unstaged 변경), untracked 파일 탐색이라는 3가지 비교 결과를 합산하는 방식, git status --porcelain의 2글자 코드(M./.M/A./??) 해석법
04. git add의 본질 — blob 생성 + index 업데이트 git add foo.txtgit hash-object -w foo.txt + git update-index --add --cacheinfo 100644 <SHA> foo.txt로 분해해 직접 재현, 이 분해를 알면 git add -p(부분 추가)가 어떻게 동작하는지 이해됨
05. assume-unchanged vs skip-worktree 두 플래그의 의도 차이(전자: 성능 최적화 힌트, 후자: 의도적 제외 선언), git update-index --[no-]assume-unchanged/--[no-]skip-worktree로 토글, sparse checkout이 skip-worktree를 활용하는 방식, "왜 .gitignore로 안 되고 이걸 써야 하나"
06. .gitignore 매칭 알고리즘과 패턴 우선순위 디렉토리별 .gitignore가 누적 적용되며 하위 디렉토리가 상위를 override할 수 있는 우선순위 규칙, !pattern으로 negate, **/*/?/[abc] 와일드카드 의미, git check-ignore -v <path>로 어느 패턴 어느 파일에서 매칭됐는지 추적

🔹 Chapter 4: Commit & DAG History

핵심 질문: Commit 객체는 정확히 무엇을 참조하는가? Git의 history는 왜 Tree가 아니라 DAG인가? Commit-Graph 파일은 무엇을 가속하는가?

Commit 객체 구조부터 Commit-Graph까지 (5개 문서)
문서 다루는 내용
01. Commit 객체 내부 tree <SHA>, parent <SHA>(merge면 여러 줄), author <name> <email> <ts> <tz>, committer ..., 빈 줄, 메시지로 구성된 line-based 포맷, git cat-file -p HEAD로 직접 확인, author와 committer가 다른 경우(rebase / cherry-pick / patch 적용)
02. DAG (Directed Acyclic Graph) 구조 부모 → 자식이 아닌 자식 → 부모 방향의 DAG, 한 commit이 여러 자식을 가질 수도(branch) 여러 부모를 가질 수도(merge) 있는 구조, git log --graph --oneline --all 출력 해석법
03. Linear vs Non-linear History Fast-forward만 허용하는 linear history와 merge commit이 분기를 만드는 non-linear history의 시각적/탐색적 차이, --no-ff 강제 merge commit 정책, "PR 머지 전략"(merge / squash / rebase)이 history 모양에 미치는 영향
04. Reachability 분석 — git rev-list와 ancestor 탐색 git rev-list A ^B로 "A에는 있고 B에는 없는 커밋" 탐색, git merge-base A B로 LCA 찾기, git log --ancestry-path로 두 커밋 사이 경로 탐색, 이 명령어들이 fetch / push 협상에서 사용되는 방식
05. Commit-Graph 파일 (Git 2.18+) .git/objects/info/commit-graph 파일이 부모/세대 번호를 미리 계산해두는 구조, git commit-graph write로 생성, generation number(v=2)가 ancestor 비교를 O(N)에서 O(1)에 가깝게 만드는 원리, 대규모 모노레포에서 git log --graph 속도 차이

🔹 Chapter 5: Branch 메커니즘

핵심 질문: Branch는 어디에 무엇으로 저장되는가? git switch/checkout은 HEAD와 working directory를 어떤 순서로 바꾸는가? Tracking branch는 어디에 설정되는가?

40바이트 텍스트 파일부터 Tracking branch 메커니즘까지 (5개 문서)
문서 다루는 내용
01. Branch는 그저 40바이트 텍스트 파일 cat .git/refs/heads/main으로 단일 SHA만 들어있는 것을 확인, echo <SHA> > .git/refs/heads/foo로 branch 수동 생성(권장하지는 않음), 이 단순한 설계가 어떻게 협업의 모든 복잡성을 처리하는지
02. git branch가 내부적으로 하는 일 git branch foogit update-ref refs/heads/foo HEAD, git branch -D foorm .git/refs/heads/foo + reflog 정리, --copy/--move가 reflog를 어떻게 처리하는지, plumbing으로 직접 재현
03. Branch 이동 — switch/checkout이 HEAD를 바꾸는 과정 git switch foo가 (1) HEAD 심볼릭 참조 갱신 → (2) index를 새 commit의 tree로 갱신 → (3) working directory 파일 갱신 순서로 진행, working directory에 변경사항이 있을 때 충돌 처리, git switch -c(생성 + 이동)
04. Tracking Branch 메커니즘 branch.<name>.remotebranch.<name>.merge 두 config가 upstream 관계를 정의, git branch --set-upstream-to/git push -u가 무엇을 쓰는지, git status의 "ahead/behind N commits" 계산 원리
05. Branch 명명 규칙과 충돌 Ref가 파일시스템 경로로 저장되므로 feature/foo(파일)와 feature/foo/bar(파일을 디렉토리로 사용 시도) 충돌, refs/heads/feature/foo 파일이 있으면 refs/heads/feature/foo/bar 생성 불가, packed-refs로 회피되는 경우, 안전한 명명 규칙

🔹 Chapter 6: Merge Internals

핵심 질문: 3-way merge 알고리즘은 정확히 어떤 결정을 내리는가? recursive에서 ort(Git 2.34+)로 바뀌며 무엇이 빨라졌는가? Conflict는 어떻게 감지되고 marker는 어떤 알고리즘으로 만들어지는가?

3-way merge 알고리즘부터 Merge Driver 커스터마이징까지 (8개 문서)
문서 다루는 내용
01. 3-way Merge 알고리즘 base / ours / theirs 세 트리를 결합하는 결정 트리: (ours == base, theirs != base) → take theirs, (ours != base, theirs == base) → take ours, (ours == theirs) → no conflict, (ours != base, theirs != base, ours != theirs) → CONFLICT, 파일 단위가 아닌 hunk 단위 적용
02. Common Ancestor 찾기 — LCA 알고리즘 DAG에서 두 commit의 Lowest Common Ancestor를 찾는 BFS 기반 알고리즘, criss-cross merge에서 LCA가 여러 개인 경우, git merge-base --all로 모든 후보 확인, recursive 전략이 "재귀적으로 LCA의 LCA를 merge"하는 방식
03. Fast-forward의 본질 targetsource의 ancestor일 때 새 commit 없이 ref만 앞으로 옮기는 동작, --ff-only로 강제, --no-ff로 항상 merge commit 만들기, "PR을 squash merge하면 항상 새 commit"인 이유
04. Merge Strategies 비교 — resolve, recursive, ort, octopus, subtree 각 전략이 적합한 시나리오(2-way / 일반 / 다중 LCA / 3개 이상 브랜치 / 서브트리 통합), -s 옵션 사용법, -X ours/-X theirs(해결 우선순위)와 -s ours(상대 트리 무시)의 결정적 차이
05. recursive → ort 전환 (Git 2.34+) ort(Ostensibly Recursive's Twin)가 working directory를 거치지 않고 in-memory tree만 다루도록 재설계된 점, 거대 모노레포에서 100x 속도 향상 사례, merge.tool/merge.conflictstyle은 그대로 동작
06. Conflict Detection과 충돌 마커 생성 hunk 단위 비교 후 양쪽이 동일 영역을 다르게 수정하면 conflict로 표시, <<<<<<< HEAD / ======= / >>>>>>> branch 마커가 ours / theirs를 어떻게 표현하는지, merge.conflictStyle=diff3/zdiff3로 base 영역까지 표시
07. Rerere — REuse REcorded REsolution rerere.enabled=true로 활성화, .git/rr-cache/에 (conflict hash → resolution) 매핑 저장, 동일 conflict가 다시 나타나면 자동 해결, long-lived feature branch에서 반복 rebase 시 효과, 위험성과 해제 방법
08. Merge Driver 커스터마이징 (.gitattributes) .gitattributes*.json merge=jsondiff 같은 매핑, .git/config[merge "jsondiff"] driver = ... 설정, lock 파일/생성된 파일에 merge=ours driver를 적용해 자동 해결, 직접 driver 작성 시 입력 인자(%O %A %B %L %P) 의미

🔹 Chapter 7: Rebase Internals

핵심 질문: Rebase는 왜 새 commit을 만드는가? Interactive rebase의 todo 파일은 어떻게 구성되는가? --onto는 정확히 무엇을 잘라 어디에 붙이는가?

새 commit 생성 이유부터 --onto 분해까지 (6개 문서)
문서 다루는 내용
01. Rebase가 새 commit을 만드는 이유 Commit이 immutable한 이유(SHA 자체가 내용 + 부모로 결정), parent를 바꾸면 SHA가 바뀌고 결과적으로 다른 commit이 됨, 기존 commit은 reflog에 남고 GC 대상이 되는 흐름, "rebase가 history를 다시 쓴다"의 정확한 의미
02. Patch 적용 방식 vs Merge 방식 Rebase는 각 commit의 diff를 patch로 추출 → 새 base에 순차 적용, merge는 두 tip을 LCA 기준으로 결합, 동일 충돌이라도 rebase는 commit마다 따로 해결해야 하고 merge는 한 번에 해결한다는 차이
03. Interactive Rebase의 todo 파일 구조 git rebase -i가 만드는 .git/rebase-merge/git-rebase-todo 파일 구조(pick <SHA> <subject> 라인 목록), 편집기에서 저장 → 종료 후 Git이 위에서 아래로 실행, git rebase --edit-todo로 도중 재편집
04. pick / reword / edit / squash / fixup / drop의 객체 변화 각 액션이 객체 그래프에 일으키는 변화: pick(새 commit 생성), reword(메시지만 변경, 새 SHA), squash(앞 commit과 합쳐 새 commit + 메시지 결합), fixup(squash와 같지만 메시지 무시), drop(skip), edit(중단 후 amend)
05. git rebase --onto 분해 "branch에서 upstream까지의 commit들을 잘라내고 newbase 위에 붙인다"는 의미를 그래프로 시각화, 잘못된 base에서 시작한 feature를 올바른 base로 이동하는 시나리오, hot-fix를 잘못된 brach에서 만든 후 옮기기
06. Rebase 충돌 해결 — continue / skip / abort 충돌 시 .git/rebase-merge/ 디렉토리 상태(stopped-sha, done, git-rebase-todo), --continue(현재 patch 적용 후 다음으로), --skip(이 commit 건너뛰기), --abort(원래 상태로 복구 — ORIG_HEAD 활용)의 정확한 동작

🔹 Chapter 8: Reset, Restore, Revert 분해

핵심 질문: git reset의 3가지 모드는 정확히 어느 영역을 바꾸는가? git restore는 왜 새로 도입됐는가? git revert는 어떻게 안전하게 "되돌림"을 구현하는가?

reset 3모드부터 Merge revert 함정까지 (5개 문서)
문서 다루는 내용
01. git reset 3가지 모드 — soft / mixed / hard --soft: HEAD만 이동, --mixed(기본): HEAD + index 갱신, --hard: HEAD + index + working directory 모두 갱신, 표로 정리한 영향 범위, "reset은 ref를 옮기는 명령"이라는 본질
02. git restore가 새로 도입된 이유 (Git 2.23+) git checkout이 brach 이동과 파일 복원을 동시 담당해 모호했던 문제, git switch(브랜치) + git restore(파일)로 책임 분리, --source/--staged/--worktree 옵션으로 어느 영역에 어디서 복원할지 명확히 지정
03. git revert의 본질 — 역방향 패치를 새 commit으로 Revert는 history를 지우지 않고 "반대 방향 변경"을 새 commit으로 추가, 공유된 brach에서 안전한 이유(history 변경 없음), conflict 해결이 필요한 경우와 그 의미
04. Merge commit revert (-m 1이 필요한 이유) Merge commit은 부모가 2개라 "어느 부모와의 차이를 되돌릴지" 명시 필요(-m 1은 첫 부모 기준), revert한 merge를 다시 merge하면 이미 reverted된 변경이 다시 들어오지 않는 함정, 안전한 재적용 패턴(git revert <revert-commit>)
05. Reset이 위험한 진짜 이유 — reflog 만료와 GC --hard로 잃어버린 commit이 reflog에 남으므로 즉시는 복구 가능하지만, gc.reflogExpire(기본 90일) 만료 후 GC가 unreachable 객체를 제거하면 영구 손실, 안전한 작업 흐름(reset 전 backup branch)

🔹 Chapter 9: Stash & Cherry-pick

핵심 질문: Stash는 어떤 객체로 저장되는가? Cherry-pick과 Rebase는 본질적으로 같은 작업인가?

특수한 merge commit으로서의 Stash부터 Cherry-pick 알고리즘까지 (5개 문서)
문서 다루는 내용
01. Stash의 본질 — 특수한 merge commit git stash가 만드는 commit이 (parent: HEAD, parent: index 상태, parent: untracked 상태(있으면))인 multi-parent commit이라는 점, git cat-file -p stash@{0}로 직접 확인
02. .git/refs/stash와 stash 스택 .git/refs/stash가 가장 최근 stash를 가리키는 ref, .git/logs/refs/stash(reflog)로 스택 형태 관리, stash@{0}/stash@{1} 표기법, git stash list/git stash drop 동작, "stash도 reflog 만료 대상"이라는 위험
03. Stash 충돌 해결 메커니즘 git stash pop이 내부적으로 stash commit과 현재 HEAD 간 merge를 수행, 충돌 시 stash가 자동 drop되지 않고 그대로 남는 안전 동작, --index 옵션으로 staged 상태까지 복원
04. Cherry-pick 알고리즘 — 부모와의 diff를 새 base에 적용 git cherry-pick <commit>이 (1) 해당 commit과 그 부모의 diff를 추출 → (2) HEAD에 patch 적용 → (3) 메시지 그대로 새 commit 생성, conflict 처리, -x로 원본 SHA 메시지에 기록
05. Cherry-pick vs Rebase — 본질적으로 같은 작업 Rebase가 사실은 "여러 commit에 대한 cherry-pick의 반복"이라는 관점, git rebasegit cherry-pick A^..B가 동일 결과를 만드는 시나리오, 그래도 두 명령이 따로 있는 이유(reflog 표시 / merge handling 차이)

🔹 Chapter 10: Refspec & Remote Protocol

핵심 질문: +refs/heads/*:refs/remotes/origin/*는 정확히 무엇을 의미하는가? Push/Fetch는 어떤 protocol negotiation을 거치는가? --force-with-lease--force와 무엇이 다른가?

Refspec 문법부터 Atomic Push까지 (6개 문서)
문서 다루는 내용
01. Refspec 문법 완전 분석 <src>:<dst>(push: 로컬 → 원격, fetch: 원격 → 로컬), 앞의 +는 fast-forward 아니어도 강제 갱신, * 와일드카드로 다중 ref 매핑, .git/config[remote "origin"] fetch = ... 라인 직접 분석
02. Smart Protocol vs Dumb Protocol Dumb HTTP(서버가 정적 파일만 서빙, 클라이언트가 추론), Smart(서버가 git-upload-pack/git-receive-pack 실행), HTTPS / SSH / git:// 각 transport별 인증 방식과 capability 차이
03. Push 메커니즘 — Capability negotiation → Pack 전송 → Ref 업데이트 (1) capability 광고, (2) 클라이언트가 갱신할 ref 목록 송신, (3) 서버에 없는 객체만 pack으로 전송, (4) 서버가 ref 업데이트 + hook 실행, GIT_TRACE=1 GIT_TRACE_PACKET=1로 wire protocol 직접 관찰
04. Fetch 메커니즘과 Pack Negotiation "want / have" negotiation으로 클라이언트에 없는 commit만 식별, 서버가 해당 commit의 reachable 객체만 묶어 pack 전송, shallow clone(--depth N) 시 negotiation 차이, partial clone과의 비교
05. --force vs --force-with-lease vs --force-if-includes --force: 무조건 ref 덮어씀(다른 사람 push를 silently 덮어쓸 위험), --force-with-lease: 원격 ref가 내가 fetch한 시점과 같을 때만 force(다른 사람 push 보호), --force-if-includes (Git 2.30+): 내 reflog에 원격 ref가 포함될 때만 force
06. Atomic Push와 다중 ref 업데이트 --atomic 옵션으로 여러 ref 갱신을 모두 성공 or 모두 실패로 묶음, 일부 ref만 업데이트되는 inconsistent 상태 방지, monorepo에서 여러 branch를 한 번에 옮길 때 활용

🔹 Chapter 11: Reflog & 복구 메커니즘

핵심 질문: Reflog는 어디에 어떤 포맷으로 저장되는가? 잃어버린 commit을 어떻게 다시 찾는가? GC는 어느 시점에 어떤 객체를 지우는가?

logs 디렉토리 구조부터 GC 시점까지 (5개 문서)
문서 다루는 내용
01. .git/logs/HEAD와 .git/logs/refs/heads/ 파일 구조 매 ref 변경 시 한 줄씩 추가되는 line-based 포맷(<old SHA> <new SHA> <author> <ts> <tz>\t<reason>), cat .git/logs/HEAD 직접 분석, branch별 reflog와 HEAD reflog의 차이
02. Reflog 항목 포맷과 만료 정책 gc.reflogExpire(기본 90일, reachable), gc.reflogExpireUnreachable(기본 30일, unreachable), gc.reflogExpire=never로 무기한 보존, 복구 가능 기간을 결정하는 변수들
03. git fsck --lost-found로 unreachable 객체 찾기 git fsck가 dangling commit/blob/tree를 출력, --lost-found.git/lost-found/에 객체 복사, dangling commit의 SHA를 새 branch로 만들어 복구하는 절차
04. 복구 시나리오 — force push, rebase 실수, hard reset (1) Force push 후 원격 commit 복구(다른 clone에서 pull 직전 상태 활용), (2) Interactive rebase 중 실수로 drop한 commit을 reflog에서 찾기, (3) Hard reset으로 사라진 working directory 변경 복구(stage된 것만 가능)
05. GC와 reflog 만료의 관계 git gc가 (1) 만료된 reflog 항목 제거 → (2) unreachable 객체 prune이라는 순서로 동작, gc.pruneExpire(기본 2주)로 즉시 prune되지 않는 grace period, "왜 30일/90일 안에 복구해야 하나"의 정확한 답

🔹 Chapter 12: Hooks & 자동화

핵심 질문: Client-side hook과 server-side hook은 무엇이 다른가? Hook은 어떤 환경 변수를 받고 어떤 stdin을 받는가? Husky 같은 도구는 내부적으로 어떻게 동작하는가?

Client/Server Hook부터 Husky 동작 원리까지 (4개 문서)
문서 다루는 내용
01. Client-side Hooks 13종 실행 시점과 환경변수 pre-commit/prepare-commit-msg/commit-msg/post-commit/pre-rebase/post-checkout/post-merge/pre-push 등 각 hook의 호출 시점, exit code 0/non-0이 commit 진행 여부에 미치는 영향, 인자와 stdin 형식
02. Server-side Hooks와 표준 입력 pre-receive/update/post-receive가 push 처리 파이프라인에서 호출되는 시점, stdin으로 <old-SHA> <new-SHA> <ref-name> 라인 목록을 받음, update hook은 ref 단위로 인자 받는 차이
03. Hook으로 만드는 자동화 commit-msg로 메시지 컨벤션(feat:/fix:) 검증, pre-push로 테스트 자동 실행, pre-receive로 protected branch 정책 강제, hook 우회(--no-verify)를 막는 방법(server-side에서만 검증)
04. Husky / lint-staged 동작 원리 Husky가 core.hooksPath 또는 .husky/ 디렉토리에 hook 파일을 만들어 npm 의존성으로 설치, lint-staged가 git diff --cached --name-only로 staged 파일만 추려 lint 실행, 모노레포에서 흔한 hooksPath 충돌

🔹 Chapter 13: Submodule, Subtree, Monorepo

핵심 질문: Submodule은 왜 별도 저장소처럼 동작하는가? Subtree와는 무엇이 다른가? git filter-repo로 모노레포 마이그레이션을 어떻게 하는가?

gitlink 객체부터 모노레포 마이그레이션까지 (5개 문서)
문서 다루는 내용
01. Submodule이 별도 저장소인 이유 — gitlink 객체 Tree에서 submodule entry는 mode 160000(gitlink), value는 submodule의 commit SHA(별도 객체로 fetch 필요), git ls-tree HEAD로 직접 확인, parent 레포가 sub의 specific SHA를 pin한다는 의미
02. .gitmodules와 .git/modules// 구조 .gitmodules(committed config: path / url / branch), .git/config의 active config, .git/modules/<name>/(실제 sub의 .git 디렉토리), submodule init/update 시 어떤 파일이 어떻게 생성되는지
03. Subtree merge strategy git subtree add --prefix=<dir>로 다른 레포의 history를 현재 레포의 한 디렉토리로 merge, -s subtree -X subtree=<prefix> 전략의 동작, git subtree split로 디렉토리만 추출해 새 history 만들기
04. Submodule vs Subtree 트레이드오프 Submodule(별도 SHA 추적, 사용자가 submodule update 필요, 보안 격리) vs Subtree(history 통합, 일반 clone으로 모두 받음, 변경 push 분리 어려움), 팀 규모/협업 모델별 권장
05. git filter-repo로 모노레포 마이그레이션 filter-branch가 deprecate된 이유(느림 / 함정 많음), git filter-repo로 path-based 추출(--path src/), commit author 재작성, 분리한 history를 새 레포로 push, refs/replace를 활용한 history 통합

🔹 Chapter 14: 대용량 처리 — LFS, Pack, Partial Clone

핵심 질문: Pack file의 binary 포맷은 무엇인가? LFS는 어떤 방식으로 큰 파일을 외부에 저장하는가? Partial clone과 Sparse checkout은 어떤 문제를 해결하는가?

Pack file 포맷부터 Sparse Index까지 (5개 문서)
문서 다루는 내용
01. Pack File 포맷 분석 PACK magic + version + object count 헤더, 각 object entry의 type(3-bit) + size(가변) + content(zlib 또는 delta), .idx 파일의 fanout table / sorted SHA / CRC32 / offset 구조, git verify-pack -v로 entry 분석
02. Git LFS 구조 — Pointer 객체 + 외부 스토리지 + Smudge/Clean Filter .gitattributes*.psd filter=lfs diff=lfs merge=lfs -text, commit 시 clean filter가 큰 파일을 pointer 텍스트(oid sha256:... size ...)로 치환해 저장, checkout 시 smudge filter가 LFS 서버에서 다운로드해 복원
03. LFS Protocol — Batch API와 transfer agents LFS 서버의 /objects/batch API에 (operation: download/upload, objects: [...]) 요청, 응답으로 받은 presigned URL로 직접 업/다운로드, basic / standalone / custom transfer agent 차이
04. Partial Clone — --filter=blob:none 내부 동작 (Git 2.19+) git clone --filter=blob:none이 commit과 tree만 받고 blob은 필요 시 lazy fetch하는 메커니즘, promisor remote 개념, git rev-list --missing=allow-promisor 같은 안전장치, git config remote.origin.promisor true 의미
05. Sparse Checkout과 Sparse Index (Git 2.32+) git sparse-checkout init --cone으로 working directory에 일부 디렉토리만 체크아웃, sparse index가 index 항목 수를 모노레포 전체 → 관심 디렉토리만으로 축소해 git status/git add 속도 개선

🔹 Chapter 15: 워크플로우 전략

핵심 질문: Git Flow / GitHub Flow / Trunk-Based Development는 각각 어떤 조직에 맞는가? Stacked PR은 무엇을 해결하는가?

워크플로우 선택 기준부터 회사 규모별 설계까지 (5개 문서)
문서 다루는 내용
01. Centralized vs Feature Branch vs Forking 워크플로우 (1) 모두가 main에 직접 push(소규모 / 신뢰), (2) feature branch로 분리 후 PR(중간), (3) fork → PR(OSS / 외부 기여) 각 모델의 push 권한 설계와 protection rule
02. Git Flow vs GitHub Flow vs Trunk-Based Development Git Flow(develop / release / hotfix branch — 무거움, 릴리즈 주기 김), GitHub Flow(main + feature only — 가벼움, CI 필수), Trunk-Based(short-lived branch + feature flag — 빠른 통합, 강력한 테스트 필요) 비교
03. Release Train 모델과 Backport 전략 정해진 주기(2주 / 6주)에 release branch를 fork해 stabilize, hotfix를 main에 먼저 적용 후 cherry-pick으로 release branch에 backport, git cherry-pick -x로 출처 SHA 기록
04. Stacked PR / Stacked Diff 워크플로우 큰 변경을 작은 commit 시퀀스로 쪼개 각각 PR을 만드는 패턴, Graphite/Sapling이 base PR이 merge되면 stack의 다음 PR을 자동 rebase, GitHub의 base branch 기능만으로 수동 운용하는 방법
05. 회사 규모/문화별 워크플로우 설계 스타트업(GitHub Flow + 적은 protection), 중견(GitHub Flow + CODEOWNERS + required reviews), 빅테크(Trunk-Based + monorepo + bot 자동 merge), OSS(Forking + DCO/CLA), 각 규모에서의 hooks와 CI 설계

🔹 Chapter 16: 트러블슈팅 시나리오

핵심 질문: Push가 거부되는 모든 경우는? .git이 손상됐을 때 무엇부터 확인하는가? 잘못 merge한 거대 PR은 어떻게 안전하게 되돌리는가?

Push Rejected부터 거대 PR Revert까지 (7개 문서)
문서 다루는 내용
01. Push Rejected 모든 경우 non-fast-forward(원격이 더 앞섬 → pull 후 retry), pre-receive hook reject(메시지 컨벤션 / 파일 크기 / 브랜치 이름 정책), protected branch(force push / direct push 금지), tag already exists의 차이별 진단
02. 손상된 .git 복구 — fsck 활용 git fsck --full로 dangling/missing 객체 식별, missing object를 다른 clone에서 복사, packed-refs 손상 시 loose ref로 재구성, 마지막 수단으로 --mirror clone에서 복구
03. 대용량 파일 잘못 commit → 히스토리에서 제거 git filter-repo --path <file> --invert-paths로 파일 제거(추천), 결과를 force push, 모든 협업자에게 reclone 안내, GitHub의 secret scanning 트리거된 경우 추가 절차(secret 회전)
04. 히스토리 재작성 후 협업 — force-with-lease와 통보 프로토콜 Force push 전 팀에 사전 공지 → --force-with-lease로 안전 push → 모두에게 git fetch && git reset --hard origin/<branch> 안내, 진행 중 PR이 있는 사람의 처리(rebase 또는 cherry-pick)
05. Detached HEAD 안전 탈출 작업 결과를 잃지 않으려면 git switch -c <new-branch>로 즉시 branch 생성, 이미 다른 branch로 이동했다면 reflog에서 HEAD@{N} 찾아 cherry-pick, bisect 중이면 git bisect reset
06. Rebase 도중 50개 연속 충돌 지옥 탈출 git rerere로 반복 conflict 자동 해결, git rebase --abort 후 작은 단위로 분할 rebase(git rebase --onto), 일부 commit을 squash로 묶어 충돌 지점 줄이기, 최후의 수단으로 cherry-pick + 새 branch
07. 잘못 merge한 거대 PR 되돌리기 git revert -m 1 <merge-SHA>로 안전하게 revert, 이후 같은 변경을 다시 merge하면 reverted 상태가 유지되어 변경이 다시 들어오지 않는 함정, 재적용 시 git revert <revert-commit>로 revert를 다시 revert해야 함

🏗️ 실험 환경

모든 실험은 Git 2.40+ + bash + 표준 유틸리티만 있으면 로컬에서 재현할 수 있습니다.

# Git 버전 확인 — 2.40 이상 권장 (sparse index, ort merge 활용)
git --version

# 실험용 빈 레포 생성 + 초기 상태 확인
mkdir lab && cd lab
git init
tree .git
# .git
# ├── HEAD                  ← ref: refs/heads/main 한 줄
# ├── config
# ├── description
# ├── hooks/                ← .sample 예시 hook들
# ├── info/exclude
# ├── objects/
# │   ├── info/
# │   └── pack/
# └── refs/
#     ├── heads/            ← 비어있음 (아직 commit 없음)
#     └── tags/

# 실험용 공통 명령어 세트

# 1. Plumbing으로 commit 직접 만들기 (porcelain 없이)
echo "hello" > hello.txt
blob_sha=$(git hash-object -w hello.txt)
git update-index --add --cacheinfo 100644 $blob_sha hello.txt
tree_sha=$(git write-tree)
commit_sha=$(echo "first" | git commit-tree $tree_sha)
git update-ref refs/heads/main $commit_sha
git log --oneline   # 방금 만든 commit이 보임

# 2. Loose object 직접 풀어보기 (zlib 압축됨)
python3 -c "import zlib,sys; \
  print(zlib.decompress(open('.git/objects/${blob_sha:0:2}/${blob_sha:2}','rb').read()))"
# b'blob 6\x00hello\n'   ← <type> <size>\0<content>

# 3. 객체 종류와 내용 확인
git cat-file -t $commit_sha    # commit
git cat-file -p $commit_sha    # tree ... / author ... / message
git cat-file -p HEAD^{tree}    # tree 내용 (mode + name + SHA)

# 4. Index 바이너리 hexdump
xxd .git/index | head -20
git ls-files --stage           # 가독화된 index 내용

# 5. Reflog로 모든 ref 변경 이력 추적
git reflog show HEAD
cat .git/logs/HEAD             # 원본 line-based 포맷

# 6. Pack file 분석
git gc                         # loose → pack 압축
ls .git/objects/pack/          # .pack + .idx
git verify-pack -v .git/objects/pack/pack-*.idx | head -20

# 7. Wire protocol 직접 관찰 (push/fetch 시)
GIT_TRACE=1 GIT_TRACE_PACKET=1 git fetch origin 2>&1 | head -50

# 8. ignore 매칭 디버깅
git check-ignore -v <path>     # 어느 .gitignore 어느 라인이 매칭했는지

# 9. fsck로 무결성 확인
git fsck --full --unreachable

# 10. 객체 그래프 시각화
git log --graph --oneline --all --decorate

📖 각 문서 구성 방식

모든 문서는 동일한 구조로 작성됩니다.

섹션 설명
🎯 핵심 질문 이 문서를 읽고 나면 답할 수 있는 질문
🔍 왜 이 메커니즘이 필요한가 실무에서 마주치는 문제 상황과 이 메커니즘의 연결
😱 흔한 오해 또는 실수 Before — 내부를 모를 때의 접근과 그 결과
올바른 이해와 사용 After — 원리를 알고 난 후의 접근
🔬 내부 동작 원리 .git/ 직접 해부 + Plumbing 명령어로 분해
💻 실험으로 확인하기 bash로 재현 가능한 시나리오 (cat-file, hash-object, xxd, ls-files 등)
📊 객체 그래프 시각화 Before → After 그래프 변화 (mermaid gitGraph 또는 ASCII)
⚖️ 트레이드오프 이 설계의 장단점, 언제 다른 접근을 택할 것인가
📌 핵심 정리 한 화면 요약
🤔 생각해볼 문제 개념을 더 깊이 이해하기 위한 질문 + 해설

🗺️ 추천 학습 경로

🟢 "force push로 동료의 commit을 날렸다" — 긴급 복구 (1~2일)
Day 1  Ch11-01  .git/logs/HEAD 구조 → reflog의 본질 이해
       Ch11-03  fsck --lost-found → unreachable 객체 찾기
       Ch11-04  복구 시나리오 → force push 후 복구 절차
Day 2  Ch11-05  GC와 reflog 만료 → 시간 안에 복구해야 하는 이유
       Ch16-04  history 재작성 후 협업 → 재발 방지 프로토콜
🟡 "Git internals를 처음으로 제대로 이해하고 싶다" — 핵심 집중 (1주)
Day 1  Ch1-01  .git 디렉토리 해부 → 전체 구조 파악
       Ch1-02  4가지 객체 → blob/tree/commit/tag 직접 보기
Day 2  Ch1-03  SHA-1 해시 → hash-object로 직접 계산
       Ch1-04  Content-Addressable Storage 패턴
Day 3  Ch3-01  index 바이너리 포맷 → ls-files로 가독화
       Ch3-04  git add 분해 → plumbing으로 재현
Day 4  Ch4-01  Commit 객체 → cat-file -p HEAD
       Ch5-01  Branch는 40바이트 → cat .git/refs/heads/main
Day 5  Ch6-01  3-way Merge 알고리즘 → 결정 트리 외우기
       Ch6-02  LCA 알고리즘 → merge-base 활용
Day 6  Ch7-01  Rebase가 새 commit 만드는 이유
       Ch7-04  todo 액션의 객체 변화
Day 7  Ch11-04 복구 시나리오 → reflog 활용 종합
🟠 "Git 면접 internals 레벨로 답하고 싶다" — 면접 준비 (2주)
1주차
 Day 1  Ch1 전체(8문서) — Object Model 완전 이해
 Day 2  Ch2 전체(5문서) + Ch3 전체(6문서) — refs와 index
 Day 3  Ch4 전체(5문서) + Ch5 전체(5문서) — DAG와 branch
 Day 4  Ch6 전체(8문서) — Merge internals (가장 자주 묻는 영역)
 Day 5  Ch7 전체(6문서) — Rebase internals (immutable 강조)
 Day 6  Ch8 전체(5문서) — reset/restore/revert 차이
 Day 7  Ch11 전체(5문서) — reflog와 GC

2주차
 Day 8  Ch9 전체(5문서) + Ch12 전체(4문서)
 Day 9  Ch10 전체(6문서) — wire protocol (advanced)
 Day 10 Ch14 전체(5문서) — pack file / LFS / partial clone
 Day 11 Ch13 전체(5문서) — submodule / monorepo
 Day 12 Ch15 전체(5문서) — workflow trade-off
 Day 13 Ch16 전체(7문서) — troubleshooting 시나리오
 Day 14 모의 면접 질문 100개 풀이 (각 챕터 "생각해볼 문제" 활용)
🔴 "Git 소스코드 레벨까지 완전 정복" — 전체 정복 (10주)
1주차  Chapter 1 (Object Model, 8문서) — 모든 후속 챕터의 기반
        → loose object 직접 풀기, hash-object 알고리즘 검증, pack format 분석

2주차  Chapter 2 + Chapter 3 (refs + index, 11문서)
        → packed-refs 동작 확인, .git/index hexdump 분석

3주차  Chapter 4 + Chapter 5 (DAG + branch, 10문서)
        → commit-graph 빌드 후 log --graph 속도 측정, branch 충돌 재현

4주차  Chapter 6 (Merge, 8문서)
        → recursive vs ort 벤치마크, rerere 캐시 직접 확인

5주차  Chapter 7 + Chapter 8 (Rebase + Reset, 11문서)
        → rebase-merge 디렉토리 추적, --soft/--mixed/--hard 영역 변화 표

6주차  Chapter 9 + Chapter 12 (Stash + Hooks, 9문서)
        → stash가 multi-parent commit임을 cat-file로 증명, husky 동작 분해

7주차  Chapter 10 (Remote Protocol, 6문서)
        → GIT_TRACE_PACKET=1로 wire protocol 패킷 캡처

8주차  Chapter 11 (Reflog, 5문서)
        → 일부러 force push → fsck --lost-found로 복구 실습

9주차  Chapter 13 + Chapter 14 (Submodule + Large files, 10문서)
        → filter-repo로 모노레포 마이그레이션, partial clone 실습

10주차 Chapter 15 + Chapter 16 (Workflow + Troubleshooting, 12문서)
        → 회사 규모별 워크플로우 설계 문서 작성, 트러블슈팅 시나리오 재현

🔗 연관 레포지토리

레포 주요 내용 연관 챕터
jvm-deep-dive JVM 런타임 시스템 내부 분석 (메모리 / GC / JIT) Ch1(Object Model — 객체 그래프 reachability와 GC가 거의 동일한 구조)
linux-for-backend-deep-dive 파일시스템 / inode / 권한 / 파이프 OS 레벨 Ch1-05(loose object의 디스크 표현), Ch1-08(Merkle tree와 inode), Ch12(hooks와 fork/exec)
spring-core-deep-dive DI / AOP / BeanPostProcessor 내부 동작 Ch12(Hooks가 Spring의 BeanPostProcessor와 비슷한 확장점 메커니즘)
architecture-patterns CAS / Merkle Tree / Event Sourcing 등 패턴 Ch1-04(Content-Addressable Storage), Ch1-08(Merkle Tree), Ch4-02(append-only DAG와 Event Sourcing)

💡 선행 학습 권장 순서: linux-for-backend-deep-dive(파일시스템 기본) → git-in-depth → architecture-patterns(CAS/Merkle 일반화)


🙏 Reference


⭐️ 도움이 되셨다면 Star를 눌러주세요!

Made with ❤️ by IQ Dev Lab


"git commit 한 줄을 치는 것과, .git/objects/ 안에 blob/tree/commit 객체가 어떤 순서로 만들어지는지 아는 것은 다르다"

About

단순 명령어 암기를 넘어, Git의 내부 동작 원리부터 복잡한 실전 시나리오까지

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors