Split View: Git 내부 동작 원리: Blob, Tree, Commit, 그리고 DAG — Git이 진짜로 작동하는 방법
Git 내부 동작 원리: Blob, Tree, Commit, 그리고 DAG — Git이 진짜로 작동하는 방법
- 들어가며: 왜 Git 내부를 이해해야 하는가
- 1. Git 객체 모델 — 모든 것의 기초
- 2. SHA-1 해싱과 Content-Addressable Storage
- 3. .git 디렉토리 해부
- 4. DAG와 커밋 그래프
- 5. 브랜치와 태그 — 단순한 포인터
- 6. Merge 내부 동작
- 7. Rebase 내부 동작
- 8. Cherry-Pick과 Revert
- 9. Reflog — 안전망
- 10. Pack 파일과 가비지 컬렉션
- 11. Plumbing vs Porcelain 명령어
- 12. 면접 질문 모음 (10문제)
- Q1. Git에서 git add를 하면 내부적으로 무슨 일이 일어나는가?
- Q2. Git 브랜치의 내부 구조를 설명하라.
- Q3. merge와 rebase의 내부적 차이를 설명하라.
- Q4. Detached HEAD란 무엇이고, 왜 위험한가?
- Q5. reflog로 삭제된 커밋을 복구하는 방법을 설명하라.
- Q6. pack 파일은 무엇이고, 왜 필요한가?
- Q7. SHA-1 충돌이 Git에 미치는 영향과 대응은?
- Q8. git clone --depth 1의 내부 동작을 설명하라.
- Q9. 3방향 병합 알고리즘을 설명하라.
- Q10. Plumbing 명령어만으로 커밋을 만드는 과정을 설명하라.
- 13. 실전 퀴즈 (5문제)
- 14. 참고 자료
들어가며: 왜 Git 내부를 이해해야 하는가
대부분의 개발자는 git add, git commit, git push 정도만 사용합니다. 하지만 Git 내부 동작을 이해하면 다음과 같은 이점이 있습니다.
- 디버깅 능력 향상:
detached HEAD, 충돌, 잃어버린 커밋을 정확히 이해하고 복구할 수 있음 - 고급 워크플로우 활용: rebase, cherry-pick, bisect를 자신 있게 사용
- 기술 면접 대비: FAANG급 회사에서 Git 내부 구조 질문이 빈번
- 트러블슈팅: 저장소 손상, 대용량 파일 문제, 느린 clone 해결
이 글에서는 Git의 내부를 바닥부터 해부합니다. 객체 모델, 해싱, 디렉토리 구조, DAG, 브랜치의 실체, merge/rebase 내부 동작, reflog, pack 파일까지 모든 것을 다룹니다.
1. Git 객체 모델 — 모든 것의 기초
Git은 본질적으로 Content-Addressable Storage(내용 주소 지정 저장소)입니다. 파일 내용의 SHA-1 해시를 키로 사용하여 4가지 타입의 객체를 저장합니다.
1.1 Blob (Binary Large Object)
Blob은 파일의 내용만 저장합니다. 파일명이나 경로 정보는 포함하지 않습니다.
# 파일 내용으로부터 blob 생성
echo "Hello, Git!" | git hash-object --stdin
# 출력: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec
# blob 내용 확인
git cat-file -p 0907f4a
# 출력: Hello, Git!
# blob 타입 확인
git cat-file -t 0907f4a
# 출력: blob
핵심 포인트: 동일한 내용의 파일이 100개 있어도 blob은 딱 1개만 저장됩니다. 이것이 Git이 효율적인 이유입니다.
1.2 Tree (트리)
Tree는 디렉토리 구조를 나타냅니다. 파일명, 파일 모드, blob/tree 참조를 포함합니다.
# 최신 커밋의 tree 확인
git cat-file -p HEAD^{tree}
# 출력:
# 100644 blob a1b2c3d... README.md
# 100644 blob d4e5f6a... package.json
# 040000 tree b7c8d9e... src
Tree (root)
├── blob: README.md (100644)
├── blob: package.json (100644)
└── tree: src/
├── blob: index.ts (100644)
└── blob: utils.ts (100644)
파일 모드 의미:
| 모드 | 의미 |
|---|---|
| 100644 | 일반 파일 |
| 100755 | 실행 파일 |
| 120000 | 심볼릭 링크 |
| 040000 | 디렉토리 (tree) |
1.3 Commit (커밋)
Commit 객체는 스냅샷의 메타데이터를 저장합니다.
git cat-file -p HEAD
# 출력:
# tree a1b2c3d4e5f6...
# parent 9f8e7d6c5b4a...
# author Kim <kim@example.com> 1711234567 +0900
# committer Kim <kim@example.com> 1711234567 +0900
#
# feat: add user authentication
Commit 객체의 구성 요소:
- tree: 이 커밋 시점의 전체 프로젝트 스냅샷 (root tree 참조)
- parent: 부모 커밋 SHA-1 (최초 커밋은 parent 없음, merge 커밋은 2개 이상)
- author: 코드를 작성한 사람
- committer: 커밋을 만든 사람 (cherry-pick 시 다를 수 있음)
- message: 커밋 메시지
1.4 Tag (태그)
Annotated tag는 별도의 객체로 저장됩니다.
# annotated tag 생성
git tag -a v1.0.0 -m "Release version 1.0.0"
# tag 객체 확인
git cat-file -p v1.0.0
# 출력:
# object d4e5f6a7b8c9...
# type commit
# tag v1.0.0
# tagger Kim <kim@example.com> 1711234567 +0900
#
# Release version 1.0.0
1.5 객체 관계도
Tag ──▶ Commit ──▶ Tree ──▶ Blob
│ │
▼ ▼
Commit Tree ──▶ Blob
(parent)
2. SHA-1 해싱과 Content-Addressable Storage
2.1 SHA-1이 작동하는 방식
Git은 객체의 내용에 헤더를 붙여 SHA-1 해시를 계산합니다.
# Git이 내부적으로 하는 계산
content="Hello, Git!"
header="blob ${#content}\0"
echo -en "${header}${content}" | sha1sum
# 결과: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec
헤더 형식: 타입 크기\0내용
blob 11\0Hello, Git!
└─┬─┘└┬┘└┬┘└────┬────┘
타입 크기 널 실제 내용
2.2 Content-Addressable의 의미
같은 내용은 항상 같은 해시를 생성합니다. 이것은 다음을 보장합니다.
- 무결성: 데이터가 변조되면 해시가 달라짐
- 중복 제거: 같은 파일은 한 번만 저장
- 효율적 비교: 해시만 비교하면 내용이 같은지 즉시 판단
2.3 SHA-1 충돌과 SHA-256 전환
2017년 Google이 SHA-1 충돌을 실증했습니다(SHAttered 공격). Git은 이에 대응하여 SHA-256으로의 전환을 진행 중입니다.
# SHA-256 저장소 생성 (Git 2.29+)
git init --object-format=sha256
# 현재 저장소의 해시 알고리즘 확인
git rev-parse --show-object-format
| 항목 | SHA-1 | SHA-256 |
|---|---|---|
| 해시 길이 | 40자 | 64자 |
| 보안성 | 충돌 발견됨 | 안전 |
| 호환성 | 모든 Git 버전 | Git 2.29+ |
| 상태 | 기본값 | 실험적 |
3. .git 디렉토리 해부
git init 후 생성되는 .git 디렉토리의 구조를 살펴봅시다.
.git/
├── HEAD # 현재 체크아웃된 브랜치 참조
├── config # 저장소별 설정
├── description # GitWeb용 설명 (거의 안 씀)
├── hooks/ # 클라이언트/서버 훅 스크립트
│ ├── pre-commit.sample
│ ├── commit-msg.sample
│ └── ...
├── info/
│ └── exclude # .gitignore의 로컬 버전
├── objects/ # 모든 Git 객체 저장소
│ ├── 09/
│ │ └── 07f4a3c4740fa3a5c919cb4447fdb1f1a66aec
│ ├── info/
│ └── pack/ # pack 파일들
├── refs/ # 브랜치와 태그 참조
│ ├── heads/ # 로컬 브랜치
│ │ └── main
│ ├── remotes/ # 원격 브랜치
│ │ └── origin/
│ └── tags/ # 태그
└── index # 스테이징 영역 (바이너리)
3.1 HEAD 파일
# HEAD는 현재 브랜치를 가리킴
cat .git/HEAD
# ref: refs/heads/main
# detached HEAD 상태에서는 커밋 해시를 직접 가리킴
git checkout a1b2c3d
cat .git/HEAD
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
3.2 refs 디렉토리
# 브랜치는 단순히 커밋 해시를 담은 파일
cat .git/refs/heads/main
# d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3
# 수동으로 브랜치 생성도 가능!
echo "d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3" > .git/refs/heads/my-branch
3.3 objects 디렉토리
객체는 SHA-1 해시의 처음 2자로 디렉토리를 만들고, 나머지 38자로 파일명을 지정합니다.
# 해시: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec
# 저장 위치: .git/objects/09/07f4a3c4740fa3a5c919cb4447fdb1f1a66aec
# 객체는 zlib으로 압축됨
python3 -c "
import zlib
with open('.git/objects/09/07f4a3c4740fa3a5c919cb4447fdb1f1a66aec', 'rb') as f:
print(zlib.decompress(f.read()))
"
3.4 index 파일 (Staging Area)
# 스테이징 영역 내용 확인
git ls-files --stage
# 100644 a1b2c3d4... 0 README.md
# 100644 d4e5f6a7... 0 src/index.ts
# index는 바이너리 파일
# "DIRC" 시그니처로 시작
hexdump -C .git/index | head -3
4. DAG와 커밋 그래프
4.1 DAG(Directed Acyclic Graph)란
Git의 커밋 히스토리는 방향 비순환 그래프(DAG) 자료구조입니다.
- 방향(Directed): 커밋이 부모를 가리킴 (자식에서 부모 방향)
- 비순환(Acyclic): 순환 참조 불가능 (A가 B의 부모이면서 동시에 자식일 수 없음)
- 그래프(Graph): 노드(커밋)와 엣지(부모 참조)로 구성
A ◀── B ◀── C ◀── D (main)
▲
└── E ◀── F (feature)
4.2 커밋 그래프 시각화
# 그래프 시각화
git log --oneline --graph --all
# 예시 출력:
# * f1a2b3c (HEAD -> main) Merge branch 'feature'
# |\
# | * d4e5f6a (feature) feat: add search
# | * a7b8c9d feat: add filter
# |/
# * 1e2f3a4 initial commit
4.3 부모 포인터
# 첫 번째 부모 (merge 커밋에서)
git log --first-parent --oneline
# 부모 커밋 접근
git cat-file -p HEAD^1 # 첫 번째 부모
git cat-file -p HEAD^2 # 두 번째 부모 (merge 커밋)
git cat-file -p HEAD~3 # 3단계 위 조상
부모 참조 문법:
HEAD~1 = HEAD^ = 첫 번째 부모
HEAD~2 = HEAD^^ = 첫 번째 부모의 첫 번째 부모
HEAD^2 = 두 번째 부모 (merge 커밋에서만)
5. 브랜치와 태그 — 단순한 포인터
5.1 브랜치는 포인터다
Git 브랜치는 특정 커밋을 가리키는 40바이트 파일에 불과합니다.
# 브랜치 생성 = 파일 생성
git branch feature
# 위 명령은 사실상 아래와 같음:
# echo $(git rev-parse HEAD) > .git/refs/heads/feature
# 브랜치 전환 = HEAD 파일 수정
git checkout feature
# 위 명령은 사실상:
# 1. .git/HEAD에 "ref: refs/heads/feature" 기록
# 2. 워킹 트리를 해당 커밋의 tree로 업데이트
# 3. index를 해당 tree에 맞게 업데이트
.git/refs/heads/
├── main → d4e5f6a (commit)
├── feature → a7b8c9d (commit)
└── hotfix → 1e2f3a4 (commit)
.git/HEAD → ref: refs/heads/main
5.2 HEAD의 역할
HEAD는 현재 체크아웃된 위치를 가리키는 포인터입니다.
정상 상태:
HEAD → refs/heads/main → commit d4e5f6a
Detached HEAD 상태:
HEAD → commit a7b8c9d (직접 커밋을 가리킴)
Detached HEAD 주의점: 이 상태에서 커밋하면 브랜치가 없어서 나중에 참조할 수 없습니다. git gc가 실행되면 해당 커밋이 삭제될 수 있습니다.
# detached HEAD 상태에서 작업 저장
git checkout -b recovery-branch
5.3 Lightweight vs Annotated Tag
# Lightweight tag: 단순 참조 (refs/tags/에 커밋 해시만 저장)
git tag v1.0.0-rc1
# Annotated tag: 별도 tag 객체 생성
git tag -a v1.0.0 -m "Release 1.0.0"
# 차이점 확인
git cat-file -t v1.0.0-rc1 # commit
git cat-file -t v1.0.0 # tag
6. Merge 내부 동작
6.1 Fast-Forward Merge
main에서 feature가 분기한 후, main에 추가 커밋이 없는 경우:
Before:
A ◀── B ◀── C (main)
◀── D ◀── E (feature)
After (fast-forward):
A ◀── B ◀── C ◀── D ◀── E (main, feature)
# fast-forward merge
git checkout main
git merge feature
# "Fast-forward" 메시지 출력
# fast-forward를 하지 않고 merge 커밋 생성 강제
git merge --no-ff feature
Fast-forward는 포인터만 이동하므로 새 커밋이 생성되지 않습니다.
6.2 Three-Way Merge (3방향 병합)
두 브랜치 모두에 새 커밋이 있는 경우:
Before:
A ◀── B ◀── C ◀── F (main)
◀── D ◀── E (feature)
After (3-way merge):
A ◀── B ◀── C ◀── F ◀── G (main) [merge commit]
◀── D ◀── E ──────┘
(feature)
3방향 병합 알고리즘:
- 공통 조상(Merge Base) 찾기: B가 공통 조상
- 두 브랜치의 변경사항 계산: B에서 F까지, B에서 E까지
- 변경사항 합치기: 충돌이 없으면 자동 병합, 있으면 수동 해결
# merge base 확인
git merge-base main feature
# 출력: B의 SHA-1 해시
# merge를 위한 3개의 tree 비교
git diff $(git merge-base main feature) main # base vs main
git diff $(git merge-base main feature) feature # base vs feature
6.3 Recursive Strategy
Merge base가 여러 개인 경우 Git은 recursive strategy를 사용합니다.
A ◀── B ◀── E (main)
▲ ◀── F (feature)
└── C ◀── D
(크로스 merge 히스토리)
이 경우 Git은:
- 여러 merge base를 찾음
- merge base끼리 먼저 가상 merge
- 그 결과를 실제 merge base로 사용
# merge strategy 확인
git merge -s recursive feature
git merge -s ort feature # Git 2.34+ 기본값 (Ostensibly Recursive's Twin)
6.4 Octopus Merge
3개 이상의 브랜치를 동시에 병합할 때 사용합니다.
git merge feature1 feature2 feature3
A ◀── B (main)
◀── C (feature1)
◀── D (feature2)
◀── E (feature3)
→
A ◀── B ◀── M (main) [3개의 부모]
◀── C ──────┘
◀── D ──────┘
◀── E ──────┘
주의: 충돌이 발생하면 octopus merge는 실패합니다.
7. Rebase 내부 동작
7.1 Rebase의 본질: 커밋 재생성
Rebase는 기존 커밋을 복사하여 새로운 부모 위에 재생성합니다.
Before:
A ◀── B ◀── C (main)
◀── D ◀── E (feature)
After rebase (git checkout feature && git rebase main):
A ◀── B ◀── C (main)
◀── D' ◀── E' (feature)
중요: D'과 E'는 D, E와 다른 커밋입니다. 내용은 같지만 부모가 다르므로 SHA-1이 다릅니다.
7.2 Rebase 내부 단계
git checkout feature
git rebase main
Git이 내부적으로 수행하는 작업:
- feature 브랜치의 커밋 중 main에 없는 것 찾기 (D, E)
- 이 커밋들의 패치를 임시 저장 (
.git/rebase-apply/또는.git/rebase-merge/) - feature를 main의 최신 커밋(C)으로 reset
- 저장한 패치를 하나씩 적용하여 새 커밋 생성 (D', E')
# rebase 중 임시 파일 확인
ls .git/rebase-merge/
# done # 완료된 커밋
# git-rebase-todo # 남은 커밋
# head-name # 원래 브랜치명
# onto # rebase 대상 커밋
7.3 Interactive Rebase
git rebase -i HEAD~3
# .git/rebase-merge/git-rebase-todo 내용:
pick a1b2c3d feat: add login
pick d4e5f6a feat: add signup
pick 7b8c9d0 fix: typo in login
# 명령어:
# pick = 커밋 사용
# reword = 메시지 수정
# edit = 커밋 수정 후 계속
# squash = 이전 커밋과 합치기 (메시지 합침)
# fixup = 이전 커밋과 합치기 (메시지 버림)
# drop = 커밋 삭제
7.4 Rebase vs Merge 비교
| 항목 | Merge | Rebase |
|---|---|---|
| 히스토리 | 분기 유지 (비선형) | 직선형 |
| 기존 커밋 | 변경 없음 | 새 커밋 생성 |
| 충돌 해결 | 1번만 | 커밋마다 가능 |
| 공유 브랜치 | 안전 | 위험 (force push 필요) |
| merge 커밋 | 생성됨 | 없음 |
황금 규칙: 이미 push한 커밋은 rebase하지 마세요. 다른 사람이 그 커밋을 base로 작업했을 수 있습니다.
8. Cherry-Pick과 Revert
8.1 Cherry-Pick 내부 동작
Cherry-pick은 특정 커밋의 변경사항만 현재 브랜치에 적용합니다.
git cherry-pick d4e5f6a
내부 동작:
- 대상 커밋(d4e5f6a)과 그 부모 간의 diff 계산
- 현재 HEAD에 해당 diff 적용
- 새 커밋 생성 (같은 메시지, 다른 SHA-1)
Before:
A ◀── B ◀── C (main)
◀── D ◀── E (feature)
git checkout main && git cherry-pick E:
A ◀── B ◀── C ◀── E' (main) [E의 변경사항만 복사]
◀── D ◀── E (feature)
# cherry-pick한 커밋의 원본 확인
git log --oneline E'
# author와 committer가 다를 수 있음
8.2 Revert 내부 동작
Revert는 특정 커밋의 역방향 패치를 적용하는 새 커밋을 생성합니다.
git revert d4e5f6a
Before:
A ◀── B ◀── C (main)
git revert B:
A ◀── B ◀── C ◀── B' (main) [B의 변경을 되돌리는 새 커밋]
Revert는 히스토리를 다시 쓰지 않으므로 공유 브랜치에서 안전합니다.
8.3 Merge 커밋 Revert
# merge 커밋을 revert할 때는 어떤 부모를 기준으로 할지 지정
git revert -m 1 MERGE_COMMIT_HASH
# -m 1: 첫 번째 부모(main) 기준으로 되돌림
# -m 2: 두 번째 부모(feature) 기준으로 되돌림
9. Reflog — 안전망
9.1 Reflog란
Reflog는 HEAD와 브랜치 참조의 모든 변경 이력을 기록합니다. 실수로 커밋을 삭제하거나, rebase로 히스토리를 망쳤을 때 복구의 핵심 도구입니다.
# reflog 확인
git reflog
# d4e5f6a HEAD@{0}: commit: feat: add auth
# a1b2c3d HEAD@{1}: checkout: moving from feature to main
# 7b8c9d0 HEAD@{2}: commit: feat: add search
# 1e2f3a4 HEAD@{3}: rebase finished
# ...
# 특정 브랜치의 reflog
git reflog show feature
9.2 Reflog를 이용한 복구 시나리오
시나리오 1: 실수로 hard reset한 경우
# 실수!
git reset --hard HEAD~3
# reflog에서 이전 상태 확인
git reflog
# a1b2c3d HEAD@{1}: 이전 상태
# 복구
git reset --hard a1b2c3d
시나리오 2: rebase 후 원래 상태로 되돌리기
# rebase 전 상태는 reflog에 남아있음
git reflog
# ... HEAD@{5}: rebase (start): checkout main
# rebase 전으로 복구
git reset --hard HEAD@{5}
시나리오 3: 삭제한 브랜치 복구
# 브랜치 삭제
git branch -D feature
# reflog에서 해당 브랜치의 마지막 커밋 찾기
git reflog | grep feature
# 브랜치 재생성
git branch feature a1b2c3d
9.3 Reflog의 만료
# 기본 만료 기간
# 도달 가능한 항목: 90일
# 도달 불가능한 항목: 30일
# 만료 기간 설정
git config gc.reflogExpire "180 days"
git config gc.reflogExpireUnreachable "60 days"
# 수동 만료
git reflog expire --expire=now --all
10. Pack 파일과 가비지 컬렉션
10.1 Loose Objects vs Packed Objects
Git은 처음에 각 객체를 개별 파일(loose object)로 저장합니다. 객체가 많아지면 pack 파일로 압축합니다.
# loose objects 수 확인
find .git/objects -type f | grep -v 'pack\|info' | wc -l
# pack 파일 확인
ls .git/objects/pack/
# pack-a1b2c3d4e5f6.idx (인덱스)
# pack-a1b2c3d4e5f6.pack (데이터)
10.2 Delta Compression
Pack 파일은 delta compression을 사용합니다. 비슷한 파일은 차이점만 저장합니다.
# pack 파일 내용 확인
git verify-pack -v .git/objects/pack/pack-*.idx
# SHA-1 type size size-in-pack offset depth base-SHA-1
# a1b2c3d blob 10240 3521 12 0
# d4e5f6a blob 10245 45 1200 1 a1b2c3d # delta!
위 예시에서 d4e5f6a는 a1b2c3d와의 차이만 45바이트로 저장됩니다.
10.3 git gc (Garbage Collection)
# 가비지 컬렉션 실행
git gc
# 수행하는 작업:
# 1. loose objects를 pack 파일로 압축
# 2. 도달 불가능한 객체 제거
# 3. reflog 정리
# 4. refs를 packed-refs로 압축
# 더 공격적인 GC
git gc --aggressive --prune=now
# GC 통계 확인
git count-objects -v
# count: 0 (loose objects)
# size: 0 (loose objects 크기, KB)
# in-pack: 1234 (pack된 objects)
# packs: 1 (pack 파일 수)
# size-pack: 5678 (pack 파일 크기, KB)
# prune-packable: 0
# garbage: 0
10.4 대용량 파일 문제와 해결
# 저장소에서 가장 큰 파일 찾기
git rev-list --objects --all | \
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
sed -n 's/^blob //p' | sort -rnk2 | head -10
# BFG Repo-Cleaner로 대용량 파일 제거
bfg --strip-blobs-bigger-than 100M
# git filter-repo (권장)
git filter-repo --strip-blobs-bigger-than 100M
11. Plumbing vs Porcelain 명령어
Git 명령어는 두 계층으로 나뉩니다.
11.1 Porcelain (도자기) — 사용자 친화적
일상적으로 사용하는 고수준 명령어:
git add, git commit, git push, git pull
git branch, git checkout, git merge, git rebase
git log, git diff, git status
git stash, git tag, git remote
11.2 Plumbing (배관) — 저수준
Git 내부에서 사용하는 저수준 명령어:
# 객체 조작
git hash-object # 객체 해시 계산 및 저장
git cat-file # 객체 내용/타입/크기 확인
git write-tree # index에서 tree 객체 생성
git commit-tree # tree에서 commit 객체 생성
git update-ref # 참조 업데이트
# Index 조작
git update-index # index에 파일 추가
git ls-files # index 내용 확인
git read-tree # tree를 index로 읽기
# 전송
git pack-objects # pack 파일 생성
git unpack-objects # pack 파일 해제
git send-pack # 객체 전송
git receive-pack # 객체 수신
11.3 Plumbing으로 커밋 만들기
Porcelain 없이 커밋을 만드는 과정:
# 1. blob 생성
echo "Hello World" | git hash-object -w --stdin
# a1b2c3d...
# 2. index에 추가
git update-index --add --cacheinfo 100644 a1b2c3d hello.txt
# 3. tree 생성
git write-tree
# d4e5f6a...
# 4. commit 생성
echo "first commit" | git commit-tree d4e5f6a
# 7b8c9d0...
# 5. 브랜치가 이 커밋을 가리키게 함
git update-ref refs/heads/main 7b8c9d0
12. 면접 질문 모음 (10문제)
Q1. Git에서 git add를 하면 내부적으로 무슨 일이 일어나는가?
모범 답변: 파일 내용이 SHA-1로 해시되어 blob 객체가 .git/objects/에 저장됩니다. 그리고 .git/index (staging area)에 해당 blob의 해시와 파일 경로가 기록됩니다. 이때 파일의 이전 버전은 그대로 남아있고, 새 버전만 추가됩니다.
Q2. Git 브랜치의 내부 구조를 설명하라.
모범 답변: 브랜치는 .git/refs/heads/ 디렉토리에 있는 40바이트 파일입니다. 이 파일에는 브랜치가 가리키는 커밋의 SHA-1 해시만 저장됩니다. 브랜치 전환은 HEAD 파일을 수정하고, 워킹 트리와 index를 해당 커밋의 tree로 업데이트하는 것입니다.
Q3. merge와 rebase의 내부적 차이를 설명하라.
모범 답변: Merge는 두 브랜치의 최신 커밋과 공통 조상을 사용한 3방향 병합으로 새 merge 커밋을 생성합니다. Rebase는 현재 브랜치의 커밋들을 대상 브랜치 위에 하나씩 재생성합니다. Rebase는 새로운 커밋을 만들기 때문에 SHA-1이 변경됩니다.
Q4. Detached HEAD란 무엇이고, 왜 위험한가?
모범 답변: HEAD가 브랜치를 통하지 않고 직접 커밋을 가리키는 상태입니다. 이 상태에서 새 커밋을 만들면 어떤 브랜치도 이 커밋을 참조하지 않아, 다른 브랜치로 체크아웃하면 해당 커밋에 접근할 수 없게 됩니다. git gc가 실행되면 이러한 미참조 커밋은 삭제됩니다.
Q5. reflog로 삭제된 커밋을 복구하는 방법을 설명하라.
모범 답변: git reflog로 HEAD의 변경 이력을 확인하고, 복구하려는 커밋의 해시를 찾습니다. 그 후 git reset --hard HASH 또는 git checkout -b recovery HASH로 복구합니다. reflog는 기본 30일(미참조) ~ 90일(참조) 보관됩니다.
Q6. pack 파일은 무엇이고, 왜 필요한가?
모범 답변: Git은 초기에 각 객체를 개별 파일(loose object)로 저장하는데, 객체가 많아지면 파일 시스템 성능이 저하됩니다. Pack 파일은 여러 객체를 하나의 파일로 압축하며, delta compression을 통해 비슷한 객체 간 차이만 저장합니다. git gc나 git push 시 자동 생성됩니다.
Q7. SHA-1 충돌이 Git에 미치는 영향과 대응은?
모범 답변: SHA-1 충돌 시 서로 다른 내용이 같은 해시를 가져 데이터 무결성이 깨질 수 있습니다. 2017년 SHAttered 공격으로 충돌이 실증되었고, Git은 충돌 감지 로직 추가와 SHA-256 전환을 진행 중입니다. Git 2.29부터 --object-format=sha256 옵션을 지원합니다.
Q8. git clone --depth 1의 내부 동작을 설명하라.
모범 답변: Shallow clone은 히스토리의 일부만 가져옵니다. 서버에서 최신 커밋과 그에 필요한 tree, blob만 pack 파일로 전송합니다. .git/shallow 파일에 shallow boundary 커밋이 기록되어 그 이전 히스토리는 접근할 수 없습니다.
Q9. 3방향 병합 알고리즘을 설명하라.
모범 답변: 두 브랜치의 공통 조상(merge base)을 찾고, base에서 각 브랜치까지의 diff를 계산합니다. 한쪽만 변경한 부분은 자동 적용, 양쪽 모두 같은 부분을 다르게 변경한 경우 충돌로 표시합니다. Git은 git merge-base 명령으로 공통 조상을 찾습니다.
Q10. Plumbing 명령어만으로 커밋을 만드는 과정을 설명하라.
모범 답변: (1) git hash-object -w로 blob 생성, (2) git update-index로 index에 추가, (3) git write-tree로 tree 생성, (4) git commit-tree로 commit 생성, (5) git update-ref로 브랜치가 새 커밋을 가리키게 합니다.
13. 실전 퀴즈 (5문제)
Q1. 다음 중 Git 객체 타입이 아닌 것은? (a) blob (b) tree (c) branch (d) commit (e) tag
정답: (c) branch
브랜치는 Git 객체가 아닙니다. 브랜치는 커밋을 가리키는 참조(reference)로, .git/refs/heads/에 텍스트 파일로 저장됩니다. Git의 4가지 객체 타입은 blob, tree, commit, tag입니다.
Q2. git rebase main과 git merge main의 결과가 동일한 경우는?
정답: Fast-forward가 가능한 경우
현재 브랜치가 main에서 분기한 후 main에 추가 커밋이 없는 경우, merge는 fast-forward되고 rebase도 동일한 결과를 냅니다. 두 경우 모두 같은 선형 히스토리가 됩니다.
Q3. git reset --hard HEAD~1 후 삭제된 커밋을 복구하려면?
정답: git reflog에서 삭제된 커밋의 해시를 찾아 git reset --hard HASH 또는 git branch recovery HASH로 복구합니다. reflog는 HEAD의 모든 변경을 기록하므로, reset 전의 커밋 해시가 남아있습니다.
Q4. 같은 내용의 파일이 다른 이름으로 3개 있을 때, Git은 blob을 몇 개 저장하는가?
정답: 1개
Git은 content-addressable storage이므로 파일 내용의 SHA-1 해시를 키로 사용합니다. 같은 내용은 같은 해시이므로 blob 1개만 저장됩니다. 파일명과 경로 정보는 tree 객체에 저장됩니다.
Q5. merge 커밋의 부모가 3개 이상인 경우는?
정답: Octopus merge
git merge branch1 branch2 branch3처럼 3개 이상의 브랜치를 동시에 병합하면 octopus merge가 수행됩니다. 이때 생성되는 merge 커밋은 3개 이상의 부모를 가집니다. 단, 충돌이 발생하면 octopus merge는 실패합니다.
14. 참고 자료
- Pro Git Book - Git Internals — 공식 문서
- Git from the Bottom Up — John Wiegley
- How Git Works Internally — GitHub Blog
- Git Object Model — 공식 문서
- SHAttered Attack — SHA-1 충돌 실증
- Git SHA-256 Transition — SHA-256 전환 계획
- Merge Strategies in Git — 공식 문서
- Git Rebase Documentation — 공식 문서
- Git Internals - Transfer Protocols
- Unpacking Git Packfiles — Recurse Center
- Git Delta Compression — Matthew McCullough
- Think Like a Git — DAG와 그래프 이론 관점에서 Git 이해
Git Internals: How Git Really Works — Blob, Tree, Commit, and DAG Deep Dive
- Introduction: Why Understand Git Internals
- 1. Git Object Model — The Foundation of Everything
- 2. SHA-1 Hashing and Content-Addressable Storage
- 3. .git Directory Anatomy
- 4. DAG and Commit Graph
- 5. Branches and Tags — Just Pointers
- 6. Merge Internals
- 7. Rebase Internals
- 8. Cherry-Pick and Revert
- 9. Reflog — The Safety Net
- 10. Pack Files and Garbage Collection
- 11. Plumbing vs Porcelain Commands
- 12. Interview Questions (10 Questions)
- Q1. What happens internally when you run git add?
- Q2. Explain the internal structure of a Git branch.
- Q3. Explain the internal differences between merge and rebase.
- Q4. What is detached HEAD and why is it dangerous?
- Q5. How do you recover deleted commits using reflog?
- Q6. What are pack files and why are they needed?
- Q7. What impact do SHA-1 collisions have on Git, and how is it being addressed?
- Q8. Explain the internal workings of git clone --depth 1.
- Q9. Explain the three-way merge algorithm.
- Q10. Describe the process of creating a commit using only plumbing commands.
- 13. Quiz (5 Questions)
- 14. References
Introduction: Why Understand Git Internals
Most developers only use git add, git commit, and git push. However, understanding Git internals provides significant advantages:
- Better debugging: Understand and recover from
detached HEAD, conflicts, and lost commits - Advanced workflows: Use rebase, cherry-pick, and bisect with confidence
- Interview preparation: FAANG-level companies frequently ask about Git internals
- Troubleshooting: Resolve repository corruption, large file issues, and slow clones
This article dissects Git from the ground up: the object model, hashing, directory structure, DAG, the true nature of branches, merge/rebase internals, reflog, and pack files.
1. Git Object Model — The Foundation of Everything
At its core, Git is a Content-Addressable Storage system. It stores four types of objects using the SHA-1 hash of their content as keys.
1.1 Blob (Binary Large Object)
A blob stores only the file content. It does not include the filename or path information.
# Create a blob from content
echo "Hello, Git!" | git hash-object --stdin
# Output: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec
# Inspect blob content
git cat-file -p 0907f4a
# Output: Hello, Git!
# Check blob type
git cat-file -t 0907f4a
# Output: blob
Key point: Even if you have 100 files with identical content, Git stores only one blob. This is what makes Git efficient.
1.2 Tree
A tree represents a directory structure. It contains filenames, file modes, and references to blobs or other trees.
# Inspect the tree of the latest commit
git cat-file -p HEAD^{tree}
# Output:
# 100644 blob a1b2c3d... README.md
# 100644 blob d4e5f6a... package.json
# 040000 tree b7c8d9e... src
Tree (root)
├── blob: README.md (100644)
├── blob: package.json (100644)
└── tree: src/
├── blob: index.ts (100644)
└── blob: utils.ts (100644)
File mode meanings:
| Mode | Meaning |
|---|---|
| 100644 | Regular file |
| 100755 | Executable file |
| 120000 | Symbolic link |
| 040000 | Directory (tree) |
1.3 Commit
A commit object stores metadata about a snapshot.
git cat-file -p HEAD
# Output:
# tree a1b2c3d4e5f6...
# parent 9f8e7d6c5b4a...
# author Kim <kim@example.com> 1711234567 +0900
# committer Kim <kim@example.com> 1711234567 +0900
#
# feat: add user authentication
Commit object components:
- tree: Complete project snapshot at this point (references the root tree)
- parent: Parent commit SHA-1 (initial commit has no parent; merge commits have 2+)
- author: Person who wrote the code
- committer: Person who created the commit (can differ in cherry-pick)
- message: Commit message
1.4 Tag
Annotated tags are stored as separate objects.
# Create annotated tag
git tag -a v1.0.0 -m "Release version 1.0.0"
# Inspect tag object
git cat-file -p v1.0.0
# Output:
# object d4e5f6a7b8c9...
# type commit
# tag v1.0.0
# tagger Kim <kim@example.com> 1711234567 +0900
#
# Release version 1.0.0
1.5 Object Relationship Diagram
Tag ──▶ Commit ──▶ Tree ──▶ Blob
│ │
▼ ▼
Commit Tree ──▶ Blob
(parent)
2. SHA-1 Hashing and Content-Addressable Storage
2.1 How SHA-1 Works in Git
Git computes the SHA-1 hash by prepending a header to the object content.
# What Git does internally
content="Hello, Git!"
header="blob ${#content}\0"
echo -en "${header}${content}" | sha1sum
# Result: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec
Header format: type size\0content
blob 11\0Hello, Git!
└─┬─┘└┬┘└┬┘└────┬────┘
type size null actual content
2.2 What Content-Addressable Means
The same content always produces the same hash. This guarantees:
- Integrity: Tampered data produces a different hash
- Deduplication: Identical files are stored only once
- Efficient comparison: Comparing hashes instantly determines content equality
2.3 SHA-1 Collisions and SHA-256 Migration
In 2017, Google demonstrated a SHA-1 collision (the SHAttered attack). Git is responding by transitioning to SHA-256.
# Create a SHA-256 repository (Git 2.29+)
git init --object-format=sha256
# Check current repository hash algorithm
git rev-parse --show-object-format
| Aspect | SHA-1 | SHA-256 |
|---|---|---|
| Hash length | 40 chars | 64 chars |
| Security | Collision found | Secure |
| Compatibility | All Git versions | Git 2.29+ |
| Status | Default | Experimental |
3. .git Directory Anatomy
Let us examine the .git directory structure created after git init.
.git/
├── HEAD # Reference to currently checked-out branch
├── config # Repository-specific settings
├── description # For GitWeb (rarely used)
├── hooks/ # Client/server hook scripts
│ ├── pre-commit.sample
│ ├── commit-msg.sample
│ └── ...
├── info/
│ └── exclude # Local version of .gitignore
├── objects/ # All Git objects
│ ├── 09/
│ │ └── 07f4a3c4740fa3a5c919cb4447fdb1f1a66aec
│ ├── info/
│ └── pack/ # Pack files
├── refs/ # Branch and tag references
│ ├── heads/ # Local branches
│ │ └── main
│ ├── remotes/ # Remote branches
│ │ └── origin/
│ └── tags/ # Tags
└── index # Staging area (binary)
3.1 HEAD File
# HEAD points to the current branch
cat .git/HEAD
# ref: refs/heads/main
# In detached HEAD state, it points directly to a commit
git checkout a1b2c3d
cat .git/HEAD
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
3.2 refs Directory
# A branch is simply a file containing a commit hash
cat .git/refs/heads/main
# d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3
# You can even create branches manually!
echo "d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3" > .git/refs/heads/my-branch
3.3 objects Directory
Objects are stored using the first 2 characters of the SHA-1 hash as a directory name, and the remaining 38 characters as the filename.
# Hash: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec
# Location: .git/objects/09/07f4a3c4740fa3a5c919cb4447fdb1f1a66aec
# Objects are compressed with zlib
python3 -c "
import zlib
with open('.git/objects/09/07f4a3c4740fa3a5c919cb4447fdb1f1a66aec', 'rb') as f:
print(zlib.decompress(f.read()))
"
3.4 Index File (Staging Area)
# Check staging area contents
git ls-files --stage
# 100644 a1b2c3d4... 0 README.md
# 100644 d4e5f6a7... 0 src/index.ts
# Index is a binary file starting with "DIRC" signature
hexdump -C .git/index | head -3
4. DAG and Commit Graph
4.1 What is a DAG (Directed Acyclic Graph)
Git's commit history is a Directed Acyclic Graph (DAG) data structure:
- Directed: Commits point to their parents (child-to-parent direction)
- Acyclic: No circular references (A cannot be both parent and child of B)
- Graph: Composed of nodes (commits) and edges (parent references)
A ◀── B ◀── C ◀── D (main)
▲
└── E ◀── F (feature)
4.2 Visualizing the Commit Graph
# Visualize the graph
git log --oneline --graph --all
# Example output:
# * f1a2b3c (HEAD -> main) Merge branch 'feature'
# |\
# | * d4e5f6a (feature) feat: add search
# | * a7b8c9d feat: add filter
# |/
# * 1e2f3a4 initial commit
4.3 Parent Pointers
# First parent only (useful for merge commits)
git log --first-parent --oneline
# Access parent commits
git cat-file -p HEAD^1 # First parent
git cat-file -p HEAD^2 # Second parent (merge commits)
git cat-file -p HEAD~3 # 3 ancestors up
Parent reference syntax:
HEAD~1 = HEAD^ = first parent
HEAD~2 = HEAD^^ = first parent's first parent
HEAD^2 = second parent (only on merge commits)
5. Branches and Tags — Just Pointers
5.1 Branches Are Pointers
A Git branch is nothing more than a 40-byte file pointing to a specific commit.
# Creating a branch = creating a file
git branch feature
# This is essentially:
# echo $(git rev-parse HEAD) > .git/refs/heads/feature
# Switching branches = modifying the HEAD file
git checkout feature
# This essentially:
# 1. Writes "ref: refs/heads/feature" to .git/HEAD
# 2. Updates working tree to match the commit's tree
# 3. Updates index to match the tree
.git/refs/heads/
├── main → d4e5f6a (commit)
├── feature → a7b8c9d (commit)
└── hotfix → 1e2f3a4 (commit)
.git/HEAD → ref: refs/heads/main
5.2 The Role of HEAD
HEAD is a pointer to the currently checked-out location.
Normal state:
HEAD → refs/heads/main → commit d4e5f6a
Detached HEAD state:
HEAD → commit a7b8c9d (directly points to a commit)
Detached HEAD warning: Commits made in this state are not referenced by any branch. When you check out another branch, those commits become unreachable and may be deleted by git gc.
# Save work from detached HEAD state
git checkout -b recovery-branch
5.3 Lightweight vs Annotated Tags
# Lightweight tag: simple reference (just a commit hash in refs/tags/)
git tag v1.0.0-rc1
# Annotated tag: creates a separate tag object
git tag -a v1.0.0 -m "Release 1.0.0"
# See the difference
git cat-file -t v1.0.0-rc1 # commit
git cat-file -t v1.0.0 # tag
6. Merge Internals
6.1 Fast-Forward Merge
When main has no additional commits after the feature branch diverged:
Before:
A ◀── B ◀── C (main)
◀── D ◀── E (feature)
After (fast-forward):
A ◀── B ◀── C ◀── D ◀── E (main, feature)
# Fast-forward merge
git checkout main
git merge feature
# "Fast-forward" message displayed
# Force a merge commit instead of fast-forward
git merge --no-ff feature
Fast-forward only moves the pointer — no new commit is created.
6.2 Three-Way Merge
When both branches have new commits:
Before:
A ◀── B ◀── C ◀── F (main)
◀── D ◀── E (feature)
After (3-way merge):
A ◀── B ◀── C ◀── F ◀── G (main) [merge commit]
◀── D ◀── E ──────┘
(feature)
Three-Way Merge Algorithm:
- Find the common ancestor (Merge Base): B is the common ancestor
- Calculate diffs from both branches: B to F, and B to E
- Combine changes: Auto-merge if no conflict; manual resolution if conflicted
# Find merge base
git merge-base main feature
# Output: B's SHA-1 hash
# Compare the 3 trees for merge
git diff $(git merge-base main feature) main # base vs main
git diff $(git merge-base main feature) feature # base vs feature
6.3 Recursive Strategy
When there are multiple merge bases, Git uses the recursive strategy.
A ◀── B ◀── E (main)
▲ ◀── F (feature)
└── C ◀── D
(cross-merge history)
In this case, Git:
- Finds multiple merge bases
- First merges the merge bases into a virtual merge
- Uses that result as the actual merge base
# Specify merge strategy
git merge -s recursive feature
git merge -s ort feature # Git 2.34+ default (Ostensibly Recursive's Twin)
6.4 Octopus Merge
Used when merging 3 or more branches simultaneously:
git merge feature1 feature2 feature3
A ◀── B (main)
◀── C (feature1)
◀── D (feature2)
◀── E (feature3)
→
A ◀── B ◀── M (main) [3 parents]
◀── C ──────┘
◀── D ──────┘
◀── E ──────┘
Note: Octopus merge fails if there are conflicts.
7. Rebase Internals
7.1 The Essence of Rebase: Recreating Commits
Rebase copies existing commits and recreates them on top of a new base.
Before:
A ◀── B ◀── C (main)
◀── D ◀── E (feature)
After rebase (git checkout feature && git rebase main):
A ◀── B ◀── C (main)
◀── D' ◀── E' (feature)
Important: D' and E' are different commits from D and E. They have the same content but different parents, so their SHA-1 hashes differ.
7.2 Rebase Internal Steps
git checkout feature
git rebase main
What Git does internally:
- Find commits on feature that are not on main (D, E)
- Save patches to temporary storage (
.git/rebase-apply/or.git/rebase-merge/) - Reset feature to main's latest commit (C)
- Apply saved patches one by one to create new commits (D', E')
# Check temporary files during rebase
ls .git/rebase-merge/
# done # completed commits
# git-rebase-todo # remaining commits
# head-name # original branch name
# onto # rebase target commit
7.3 Interactive Rebase
git rebase -i HEAD~3
# Contents of .git/rebase-merge/git-rebase-todo:
pick a1b2c3d feat: add login
pick d4e5f6a feat: add signup
pick 7b8c9d0 fix: typo in login
# Commands:
# pick = use commit
# reword = change message
# edit = modify commit then continue
# squash = merge with previous (combine messages)
# fixup = merge with previous (discard message)
# drop = remove commit
7.4 Rebase vs Merge Comparison
| Aspect | Merge | Rebase |
|---|---|---|
| History | Preserves branches (non-linear) | Linear |
| Existing commits | Unchanged | New commits created |
| Conflict resolution | Once | Per commit |
| Shared branches | Safe | Dangerous (force push needed) |
| Merge commit | Created | None |
Golden rule: Never rebase commits that have been pushed. Others may have based their work on those commits.
8. Cherry-Pick and Revert
8.1 Cherry-Pick Internals
Cherry-pick applies only the changes from a specific commit onto the current branch.
git cherry-pick d4e5f6a
Internal operation:
- Calculate the diff between the target commit (d4e5f6a) and its parent
- Apply that diff to the current HEAD
- Create a new commit (same message, different SHA-1)
Before:
A ◀── B ◀── C (main)
◀── D ◀── E (feature)
git checkout main && git cherry-pick E:
A ◀── B ◀── C ◀── E' (main) [only E's changes copied]
◀── D ◀── E (feature)
8.2 Revert Internals
Revert creates a new commit that applies the reverse patch of a specific commit.
git revert d4e5f6a
Before:
A ◀── B ◀── C (main)
git revert B:
A ◀── B ◀── C ◀── B' (main) [new commit undoing B's changes]
Revert does not rewrite history, making it safe for shared branches.
8.3 Reverting Merge Commits
# When reverting a merge commit, specify which parent to use as reference
git revert -m 1 MERGE_COMMIT_HASH
# -m 1: revert based on first parent (main)
# -m 2: revert based on second parent (feature)
9. Reflog — The Safety Net
9.1 What is Reflog
Reflog records all changes to HEAD and branch references. It is the essential tool for recovery when you accidentally delete commits or mess up history with rebase.
# View reflog
git reflog
# d4e5f6a HEAD@{0}: commit: feat: add auth
# a1b2c3d HEAD@{1}: checkout: moving from feature to main
# 7b8c9d0 HEAD@{2}: commit: feat: add search
# 1e2f3a4 HEAD@{3}: rebase finished
# ...
# Reflog for a specific branch
git reflog show feature
9.2 Recovery Scenarios Using Reflog
Scenario 1: Accidental hard reset
# Mistake!
git reset --hard HEAD~3
# Find previous state in reflog
git reflog
# a1b2c3d HEAD@{1}: previous state
# Recover
git reset --hard a1b2c3d
Scenario 2: Undo rebase
# Pre-rebase state is in the reflog
git reflog
# ... HEAD@{5}: rebase (start): checkout main
# Recover to pre-rebase state
git reset --hard HEAD@{5}
Scenario 3: Recover deleted branch
# Delete branch
git branch -D feature
# Find the branch's last commit in reflog
git reflog | grep feature
# Recreate branch
git branch feature a1b2c3d
9.3 Reflog Expiry
# Default expiry periods
# Reachable entries: 90 days
# Unreachable entries: 30 days
# Configure expiry
git config gc.reflogExpire "180 days"
git config gc.reflogExpireUnreachable "60 days"
# Manual expiry
git reflog expire --expire=now --all
10. Pack Files and Garbage Collection
10.1 Loose Objects vs Packed Objects
Git initially stores each object as an individual file (loose object). When objects accumulate, they are compressed into pack files.
# Count loose objects
find .git/objects -type f | grep -v 'pack\|info' | wc -l
# View pack files
ls .git/objects/pack/
# pack-a1b2c3d4e5f6.idx (index)
# pack-a1b2c3d4e5f6.pack (data)
10.2 Delta Compression
Pack files use delta compression. Similar files store only the differences.
# Inspect pack file contents
git verify-pack -v .git/objects/pack/pack-*.idx
# SHA-1 type size size-in-pack offset depth base-SHA-1
# a1b2c3d blob 10240 3521 12 0
# d4e5f6a blob 10245 45 1200 1 a1b2c3d # delta!
In the example above, d4e5f6a stores only the 45-byte difference from a1b2c3d.
10.3 git gc (Garbage Collection)
# Run garbage collection
git gc
# What it does:
# 1. Compresses loose objects into pack files
# 2. Removes unreachable objects
# 3. Cleans up reflog
# 4. Compresses refs into packed-refs
# More aggressive GC
git gc --aggressive --prune=now
# View GC statistics
git count-objects -v
# count: 0 (loose objects)
# size: 0 (loose objects size, KB)
# in-pack: 1234 (packed objects)
# packs: 1 (number of pack files)
# size-pack: 5678 (pack file size, KB)
# prune-packable: 0
# garbage: 0
10.4 Large File Issues and Solutions
# Find the largest files in the repository
git rev-list --objects --all | \
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
sed -n 's/^blob //p' | sort -rnk2 | head -10
# Remove large files with BFG Repo-Cleaner
bfg --strip-blobs-bigger-than 100M
# git filter-repo (recommended)
git filter-repo --strip-blobs-bigger-than 100M
11. Plumbing vs Porcelain Commands
Git commands are divided into two layers.
11.1 Porcelain — User-Friendly
High-level commands used daily:
git add, git commit, git push, git pull
git branch, git checkout, git merge, git rebase
git log, git diff, git status
git stash, git tag, git remote
11.2 Plumbing — Low-Level
Low-level commands used internally by Git:
# Object manipulation
git hash-object # Compute and store object hash
git cat-file # Inspect object content/type/size
git write-tree # Create tree object from index
git commit-tree # Create commit object from tree
git update-ref # Update references
# Index manipulation
git update-index # Add files to index
git ls-files # List index contents
git read-tree # Read tree into index
# Transfer
git pack-objects # Create pack files
git unpack-objects # Unpack pack files
git send-pack # Send objects
git receive-pack # Receive objects
11.3 Creating a Commit with Plumbing Only
The process of creating a commit without porcelain commands:
# 1. Create blob
echo "Hello World" | git hash-object -w --stdin
# a1b2c3d...
# 2. Add to index
git update-index --add --cacheinfo 100644 a1b2c3d hello.txt
# 3. Create tree
git write-tree
# d4e5f6a...
# 4. Create commit
echo "first commit" | git commit-tree d4e5f6a
# 7b8c9d0...
# 5. Point branch to this commit
git update-ref refs/heads/main 7b8c9d0
12. Interview Questions (10 Questions)
Q1. What happens internally when you run git add?
Model answer: The file content is hashed with SHA-1 and stored as a blob object in .git/objects/. Then the blob's hash and file path are recorded in .git/index (staging area). The previous version remains intact; only the new version is added.
Q2. Explain the internal structure of a Git branch.
Model answer: A branch is a 40-byte file in .git/refs/heads/. It contains only the SHA-1 hash of the commit the branch points to. Switching branches modifies the HEAD file and updates the working tree and index to match that commit's tree.
Q3. Explain the internal differences between merge and rebase.
Model answer: Merge creates a new merge commit using a three-way merge of both branches' latest commits and their common ancestor. Rebase copies current branch commits and recreates them on top of the target branch one by one. Since rebase creates new commits, the SHA-1 hashes change.
Q4. What is detached HEAD and why is it dangerous?
Model answer: Detached HEAD occurs when HEAD points directly to a commit instead of through a branch. Commits made in this state are not referenced by any branch. When you check out another branch, those commits become unreachable and may be removed by git gc.
Q5. How do you recover deleted commits using reflog?
Model answer: Use git reflog to view the history of HEAD changes and find the hash of the commit to recover. Then use git reset --hard HASH or git checkout -b recovery HASH. Reflog retains entries for 30 days (unreachable) to 90 days (reachable) by default.
Q6. What are pack files and why are they needed?
Model answer: Git initially stores each object as an individual file (loose object), but file system performance degrades as objects accumulate. Pack files compress multiple objects into a single file and use delta compression to store only differences between similar objects. They are automatically created during git gc or git push.
Q7. What impact do SHA-1 collisions have on Git, and how is it being addressed?
Model answer: A SHA-1 collision means different content produces the same hash, breaking data integrity. The 2017 SHAttered attack demonstrated this. Git responded by adding collision detection logic and transitioning to SHA-256. Git 2.29+ supports --object-format=sha256.
Q8. Explain the internal workings of git clone --depth 1.
Model answer: Shallow clone fetches only part of the history. The server sends a pack file containing only the latest commit and its required trees and blobs. The .git/shallow file records the shallow boundary commits, making earlier history inaccessible.
Q9. Explain the three-way merge algorithm.
Model answer: Find the common ancestor (merge base) of both branches, then calculate the diff from the base to each branch. Changes on only one side are applied automatically. When both sides modify the same section differently, it is marked as a conflict. Git uses git merge-base to find the common ancestor.
Q10. Describe the process of creating a commit using only plumbing commands.
Model answer: (1) Create a blob with git hash-object -w, (2) add to index with git update-index, (3) create a tree with git write-tree, (4) create a commit with git commit-tree, (5) update the branch reference with git update-ref.
13. Quiz (5 Questions)
Q1. Which of the following is NOT a Git object type? (a) blob (b) tree (c) branch (d) commit (e) tag
Answer: (c) branch
A branch is not a Git object. It is a reference stored as a text file in .git/refs/heads/. Git has four object types: blob, tree, commit, and tag.
Q2. When do git rebase main and git merge main produce the same result?
Answer: When fast-forward is possible
When the current branch diverged from main and main has no additional commits, merge performs a fast-forward and rebase produces an identical result. Both yield the same linear history.
Q3. How do you recover a deleted commit after git reset --hard HEAD~1?
Answer: Find the deleted commit's hash in git reflog and recover with git reset --hard HASH or git branch recovery HASH. The reflog records all HEAD changes, so the pre-reset commit hash is preserved.
Q4. If 3 files have identical content but different names, how many blobs does Git store?
Answer: 1
Git uses content-addressable storage, so the SHA-1 hash of the content is the key. Identical content produces the same hash, so only 1 blob is stored. Filenames and paths are stored in tree objects.
Q5. When can a merge commit have 3 or more parents?
Answer: Octopus merge
Running git merge branch1 branch2 branch3 performs an octopus merge, creating a merge commit with 3 or more parents. However, octopus merge fails if there are conflicts.
14. References
- Pro Git Book - Git Internals — Official documentation
- Git from the Bottom Up — John Wiegley
- How Git Works Internally — GitHub Blog
- Git Object Model — Official documentation
- SHAttered Attack — SHA-1 collision demonstration
- Git SHA-256 Transition — SHA-256 transition plan
- Merge Strategies in Git — Official documentation
- Git Rebase Documentation — Official documentation
- Git Internals - Transfer Protocols
- Unpacking Git Packfiles — Recurse Center
- Git Delta Compression — Matthew McCullough
- Think Like a Git — Understanding Git through DAG and graph theory