"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/ 디렉토리 직접 해부로 끝까지 파헤칩니다
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/ 직접 해부 + 객체 그래프 변화 시각화 |
각 챕터의 첫 문서부터 바로 학습을 시작하세요!
💡 각 섹션을 클릭하면 상세 문서 목록이 펼쳐집니다
핵심 질문:
.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 와의 공통점) |
핵심 질문: 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-refs가 refs/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>) |
핵심 질문:
.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.txt를 git 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>로 어느 패턴 어느 파일에서 매칭됐는지 추적 |
핵심 질문: 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 속도 차이 |
핵심 질문: 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 foo ≡ git update-ref refs/heads/foo HEAD, git branch -D foo ≡ rm .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>.remote와 branch.<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로 회피되는 경우, 안전한 명명 규칙 |
핵심 질문: 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의 본질 | target이 source의 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) 의미 |
핵심 질문: 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 활용)의 정확한 동작 |
핵심 질문:
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) |
핵심 질문: 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 rebase와 git cherry-pick A^..B가 동일 결과를 만드는 시나리오, 그래도 두 명령이 따로 있는 이유(reflog 표시 / merge handling 차이) |
핵심 질문:
+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를 한 번에 옮길 때 활용 |
핵심 질문: 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일 안에 복구해야 하나"의 정확한 답 |
핵심 질문: 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 충돌 |
핵심 질문: 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 통합 |
핵심 질문: 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 속도 개선 |
핵심 질문: 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 설계 |
핵심 질문: 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 일반화)
- Pro Git Book (2nd Edition) — Scott Chacon & Ben Straub — 특히 Chapter 10. Git Internals
- Git Reference Manual — git-scm.com
- Git Source Code (GitHub) —
Documentation/technical/디렉토리에 wire protocol / pack format 명세 - Building Git — James Coglan — Git을 Ruby로 처음부터 직접 구현하며 internals 학습
- Git from the inside out — Mary Rose Cook — 객체 그래프 시각화로 internals 설명
- Think Like (a) Git — DAG 관점에서 Git 사고법
- git-internals-pdf — Scott Chacon — 초기 internals 문서
⭐️ 도움이 되셨다면 Star를 눌러주세요!
Made with ❤️ by IQ Dev Lab
"git commit 한 줄을 치는 것과, .git/objects/ 안에 blob/tree/commit 객체가 어떤 순서로 만들어지는지 아는 것은 다르다"