[이제와서 시작하는 GitHub 마스터하기 - 심화편 #2] Git 내부 동작 원리: .git 폴더의 비밀
들어가며
GitHub 마스터하기 심화편 두 번째 시간입니다. 이번에는 Git의 내부 동작 원리를 깊이 있게 탐구해보겠습니다. Git이 어떻게 파일을 저장하고, 커밋을 관리하며, 브랜치를 다루는지 이해하면 Git을 훨씬 더 효과적으로 사용할 수 있습니다. .git 폴더 속 숨겨진 비밀을 파헤쳐봅시다.
1. Git의 핵심 개념: Content-Addressable Storage
Git은 Key-Value 데이터베이스
1
2
3
4
5
6
7
8
9
10
11
# Git의 핵심 원리 시연
echo 'Hello, Git!' | git hash-object --stdin
# 출력: d5b8f77ce1dc1a37b29885026055c8656c3e0ceb
# 동일한 내용은 항상 같은 해시
echo 'Hello, Git!' | git hash-object --stdin
# 출력: d5b8f77ce1dc1a37b29885026055c8656c3e0ceb
# 내용이 조금만 달라도 완전히 다른 해시
echo 'Hello, Git!!' | git hash-object --stdin
# 출력: b7e23ec29af22b0b930771f0e8f8c6c6e8897b3e
SHA-1 해시의 의미
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# git_hash.py - Git 해시 생성 원리
import hashlib
import zlib
def git_hash_object(content, obj_type='blob'):
"""Git이 객체를 해싱하는 방법"""
# Git 객체 형식: "타입 크기\0내용"
store = f"{obj_type} {len(content)}\0{content}"
# SHA-1 해시 계산
sha1 = hashlib.sha1(store.encode()).hexdigest()
# 실제 저장될 압축 데이터
compressed = zlib.compress(store.encode())
return {
'hash': sha1,
'raw_content': store,
'compressed_size': len(compressed),
'original_size': len(content)
}
# 예제
result = git_hash_object("Hello, Git!")
print(f"Hash: {result['hash']}")
print(f"Original size: {result['original_size']} bytes")
print(f"Compressed size: {result['compressed_size']} bytes")
2. .git 디렉토리 구조 심화 분석
디렉토리 구조와 역할
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.git/
├── HEAD # 현재 체크아웃된 참조
├── config # 저장소별 설정
├── description # GitWeb용 설명
├── hooks/ # 클라이언트/서버 훅
├── info/ # 전역 exclude 패턴
├── objects/ # 모든 데이터 저장소
│ ├── 00/ # 해시의 첫 2자리로 분류
│ ├── 01/
│ ├── ...
│ ├── info/ # 추가 객체 정보
│ └── pack/ # 팩 파일 (압축된 객체들)
├── refs/ # 참조 (브랜치, 태그)
│ ├── heads/ # 로컬 브랜치
│ ├── remotes/ # 원격 브랜치
│ └── tags/ # 태그
├── logs/ # 참조 로그 (reflog)
├── index # 스테이징 영역
└── packed-refs # 성능을 위한 참조 압축
Git 객체 타입 상세
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 1. Blob (Binary Large Object) - 파일 내용
echo "File content" > test.txt
git add test.txt
git ls-files --stage
# 100644 7a9c... 0 test.txt
# Blob 객체 내용 확인
git cat-file -p 7a9c...
# File content
# 2. Tree - 디렉토리 구조
git write-tree
# 4b825dc642cb6eb9a060e54bf8d69288fbee4904
git cat-file -p 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# 100644 blob 7a9c... test.txt
# 3. Commit - 스냅샷 + 메타데이터
git commit -m "Initial commit"
git cat-file -p HEAD
# tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# author Your Name <email> 1234567890 +0000
# committer Your Name <email> 1234567890 +0000
#
# Initial commit
# 4. Tag - 특정 커밋 참조
git tag -a v1.0 -m "Version 1.0"
git cat-file -p v1.0
# object abc123...
# type commit
# tag v1.0
# tagger Your Name <email> 1234567890 +0000
#
# Version 1.0
3. Git의 3단계 아키텍처
Working Directory, Staging Area, Repository
MiniGit은 파일 내용을 객체로 저장하고 인덱스와 커밋을 만드는 설명용 모형입니다. 실제 Git과 호환되는 구현이라고 가정하지 마세요. 실행 예제의 file1.txt와 file2.txt도 먼저 준비해야 합니다. 이후 실제 Git 명령으로 같은 세 단계를 비교하는 것이 이 절의 목표입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# git_stages.py - Git의 3단계 시뮬레이션
import os
import hashlib
import json
from datetime import datetime
class MiniGit:
def __init__(self, repo_path='.minigit'):
self.repo_path = repo_path
self.objects_path = os.path.join(repo_path, 'objects')
self.index_path = os.path.join(repo_path, 'index')
self.head_path = os.path.join(repo_path, 'HEAD')
# 초기화
os.makedirs(self.objects_path, exist_ok=True)
def hash_object(self, content, obj_type='blob'):
"""객체 해싱 및 저장"""
header = f"{obj_type} {len(content)}\0"
store = header + content
sha = hashlib.sha1(store.encode()).hexdigest()
# 객체 저장
obj_dir = os.path.join(self.objects_path, sha[:2])
os.makedirs(obj_dir, exist_ok=True)
obj_path = os.path.join(obj_dir, sha[2:])
with open(obj_path, 'wb') as f:
f.write(store.encode())
return sha
def add(self, filename):
"""파일을 스테이징 영역에 추가"""
with open(filename, 'r') as f:
content = f.read()
# Blob 객체 생성
sha = self.hash_object(content)
# 인덱스 업데이트
index = self.read_index()
index[filename] = {
'sha': sha,
'mtime': os.path.getmtime(filename),
'size': os.path.getsize(filename)
}
self.write_index(index)
print(f"Added {filename} ({sha[:7]})")
def commit(self, message):
"""커밋 생성"""
index = self.read_index()
if not index:
print("Nothing to commit")
return
# Tree 객체 생성
tree_content = ""
for filename, info in sorted(index.items()):
tree_content += f"100644 blob {info['sha']}\t{filename}\n"
tree_sha = self.hash_object(tree_content, 'tree')
# Commit 객체 생성
parent = self.get_head()
commit_content = f"tree {tree_sha}\n"
if parent:
commit_content += f"parent {parent}\n"
commit_content += f"author MiniGit <mini@git.com> {int(datetime.now().timestamp())} +0000\n"
commit_content += f"committer MiniGit <mini@git.com> {int(datetime.now().timestamp())} +0000\n"
commit_content += f"\n{message}\n"
commit_sha = self.hash_object(commit_content, 'commit')
# HEAD 업데이트
with open(self.head_path, 'w') as f:
f.write(commit_sha)
# 인덱스 클리어
self.write_index({})
print(f"Committed {commit_sha[:7]}: {message}")
def read_index(self):
"""인덱스 읽기"""
if not os.path.exists(self.index_path):
return {}
with open(self.index_path, 'r') as f:
return json.load(f)
def write_index(self, index):
"""인덱스 쓰기"""
with open(self.index_path, 'w') as f:
json.dump(index, f)
def get_head(self):
"""현재 HEAD 커밋"""
if not os.path.exists(self.head_path):
return None
with open(self.head_path, 'r') as f:
return f.read().strip()
# 사용 예제
git = MiniGit()
git.add('file1.txt')
git.add('file2.txt')
git.commit('Initial commit')
실제 Git 명령어로 내부 동작 추적
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 파일 추가 과정 추적
echo "Hello" > hello.txt
strace -e trace=file git add hello.txt 2>&1 | grep -E "(open|write)"
# 2. 인덱스 파일 분석
hexdump -C .git/index | head -20
# 3. 객체 데이터베이스 직접 조작
# Blob 객체 수동 생성
echo -en "blob 6\0Hello\n" | shasum
# 1c59427adc4b205a270d8f810310394962e79a8b
# 객체 저장
mkdir -p .git/objects/1c
echo -en "blob 6\0Hello\n" | zlib-flate -compress > .git/objects/1c/59427adc4b205a270d8f810310394962e79a8b
# 확인
git cat-file -p 1c59427adc4b205a270d8f810310394962e79a8b
# Hello
4. Pack 파일과 델타 압축
Pack 파일 생성과 분석
1
2
3
4
5
6
7
8
9
10
# Pack 파일 수동 생성
git gc --aggressive
# Pack 파일 내용 확인
git verify-pack -v .git/objects/pack/pack-*.idx
# Pack 파일 언팩
mkdir unpacked
cd unpacked
git unpack-objects < ../.git/objects/pack/pack-*.pack
델타 압축 원리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# delta_compression.py - Git 델타 압축 시뮬레이션
import difflib
class DeltaCompression:
def __init__(self):
self.objects = {}
def store_object(self, content):
"""객체 저장 (델타 압축 적용)"""
# 기존 객체들과 비교
best_base = None
best_delta = None
min_size = len(content)
for sha, base_content in self.objects.items():
delta = self.create_delta(base_content, content)
if len(delta) < min_size:
best_base = sha
best_delta = delta
min_size = len(delta)
# 델타가 더 효율적이면 델타로 저장
if best_delta and min_size < len(content) * 0.5:
return self.store_delta(best_base, best_delta)
else:
return self.store_full(content)
def create_delta(self, base, target):
"""델타 생성"""
# 실제 Git은 더 복잡한 알고리즘 사용
delta = []
matcher = difflib.SequenceMatcher(None, base, target)
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == 'equal':
delta.append(('copy', i1, i2 - i1))
elif tag == 'insert':
delta.append(('insert', target[j1:j2]))
elif tag == 'replace':
delta.append(('insert', target[j1:j2]))
return delta
def apply_delta(self, base_content, delta):
"""델타 적용하여 원본 복원"""
result = []
for cmd in delta:
if cmd[0] == 'copy':
_, start, length = cmd
result.append(base_content[start:start+length])
elif cmd[0] == 'insert':
_, data = cmd
result.append(data)
return ''.join(result)
# 예제
dc = DeltaCompression()
dc.objects['base'] = "Hello, World! This is a test."
content = "Hello, World! This is a test with small changes."
dc.store_object(content)
5. Git References와 Reflog
References 시스템 이해
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 참조 타입들
find .git/refs -type f | head -10
# 2. Symbolic references
cat .git/HEAD
# ref: refs/heads/main
# 3. 참조 수동 생성
echo "abc123..." > .git/refs/heads/experimental
# 4. Packed refs
cat .git/packed-refs
# 5. 참조 업데이트 로그
tail -10 .git/logs/HEAD
tail -10 .git/logs/refs/heads/main
Reflog 심화 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. Reflog 전체 보기
git reflog show --all
# 2. 특정 참조의 reflog
git reflog show main
# 3. Reflog 항목 상세 정보
git reflog show --date=iso
# 4. Reflog 기반 복구
# 실수로 reset한 경우
git reset --hard HEAD~3
# 복구
git reflog
git reset --hard HEAD@{1}
# 5. Reflog 정리
git reflog expire --expire=now --all
git gc --prune=now --aggressive
Reflog 데이터 구조 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# reflog_analyzer.py
import re
from datetime import datetime
class ReflogAnalyzer:
def __init__(self, reflog_file):
self.entries = self.parse_reflog(reflog_file)
def parse_reflog(self, filename):
"""Reflog 파일 파싱"""
entries = []
with open(filename, 'r') as f:
for line in f:
match = re.match(
r'(\w{40}) (\w{40}) (.+?) (\d+) ([+-]\d{4})\t(.+)',
line.strip()
)
if match:
old_sha, new_sha, author, timestamp, tz, message = match.groups()
entries.append({
'old_sha': old_sha,
'new_sha': new_sha,
'author': author,
'timestamp': datetime.fromtimestamp(int(timestamp)),
'timezone': tz,
'message': message
})
return entries
def find_lost_commits(self):
"""잃어버린 커밋 찾기"""
all_shas = set()
current_shas = set()
for entry in self.entries:
all_shas.add(entry['old_sha'])
all_shas.add(entry['new_sha'])
# 현재 접근 가능한 커밋
# 실제로는 git rev-list --all 사용
current_shas = self.get_reachable_commits()
lost = all_shas - current_shas
return lost
def analyze_patterns(self):
"""작업 패턴 분석"""
patterns = {
'commits': 0,
'resets': 0,
'checkouts': 0,
'merges': 0,
'rebases': 0
}
for entry in self.entries:
msg = entry['message']
if 'commit' in msg:
patterns['commits'] += 1
elif 'reset' in msg:
patterns['resets'] += 1
elif 'checkout' in msg:
patterns['checkouts'] += 1
elif 'merge' in msg:
patterns['merges'] += 1
elif 'rebase' in msg:
patterns['rebases'] += 1
return patterns
# 사용 예제
analyzer = ReflogAnalyzer('.git/logs/HEAD')
patterns = analyzer.analyze_patterns()
print(f"Work patterns: {patterns}")
6. Git Hooks 내부 동작
Hook 실행 메커니즘
1
2
3
4
5
6
# hooks 디렉토리 구조
ls -la .git/hooks/
# Hook 샘플 활성화
cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
고급 Hook 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/usr/bin/env python3
# .git/hooks/pre-commit
"""고급 pre-commit hook"""
import subprocess
import sys
import re
def run_command(cmd):
"""명령 실행 및 결과 반환"""
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.returncode, result.stdout, result.stderr
def check_file_size():
"""대용량 파일 체크"""
MAX_SIZE = 100 * 1024 * 1024 # 100MB
rc, stdout, _ = run_command("git diff --cached --name-only")
if rc != 0:
return False
for filename in stdout.strip().split('\n'):
if not filename:
continue
rc, size, _ = run_command(f"git cat-file -s :0:{filename}")
if rc == 0 and int(size) > MAX_SIZE:
print(f"Error: {filename} is too large ({int(size)/1024/1024:.1f}MB)")
return False
return True
def check_secrets():
"""민감 정보 검사"""
patterns = [
r'(?i)api[_-]?key.*=.*["\'][\w]{20,}["\']',
r'(?i)secret.*=.*["\'][\w]{20,}["\']',
r'(?i)password.*=.*["\'].+["\']',
r'-----BEGIN (RSA |EC )?PRIVATE KEY-----',
r'[0-9a-f]{40}', # SHA-1 like
]
rc, files, _ = run_command("git diff --cached --name-only")
if rc != 0:
return True
for filename in files.strip().split('\n'):
if not filename:
continue
rc, content, _ = run_command(f"git show :{filename}")
if rc != 0:
continue
for pattern in patterns:
if re.search(pattern, content):
print(f"Warning: Possible secret in {filename}")
print("Use 'git commit --no-verify' to bypass")
return False
return True
def check_commit_message():
"""커밋 메시지 규칙 검사"""
rc, msg, _ = run_command("git log -1 --pretty=%B")
if rc != 0:
return True
# Conventional Commits 형식 체크
pattern = r'^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+'
if not re.match(pattern, msg):
print("Error: Commit message doesn't follow conventional format")
print("Format: type(scope): description")
return False
return True
def main():
"""메인 검증 로직"""
checks = [
("Checking file sizes...", check_file_size),
("Scanning for secrets...", check_secrets),
]
for message, check_func in checks:
print(message)
if not check_func():
sys.exit(1)
print("All checks passed!")
sys.exit(0)
if __name__ == "__main__":
main()
7. Git 객체 스토리지 최적화
객체 저장소 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# git_storage_analyzer.py
import os
import zlib
import hashlib
from collections import defaultdict
class GitStorageAnalyzer:
def __init__(self, git_dir='.git'):
self.git_dir = git_dir
self.objects_dir = os.path.join(git_dir, 'objects')
def analyze_objects(self):
"""객체 저장소 분석"""
stats = defaultdict(lambda: {'count': 0, 'size': 0, 'compressed': 0})
# Loose objects
for root, dirs, files in os.walk(self.objects_dir):
# pack 디렉토리 제외
if 'pack' in root:
continue
for file in files:
if len(file) == 38: # SHA-1 나머지 부분
path = os.path.join(root, file)
obj_type, size, compressed_size = self.analyze_object(path)
stats[obj_type]['count'] += 1
stats[obj_type]['size'] += size
stats[obj_type]['compressed'] += compressed_size
return stats
def analyze_object(self, path):
"""개별 객체 분석"""
with open(path, 'rb') as f:
compressed = f.read()
# 압축 해제
decompressed = zlib.decompress(compressed)
# 헤더 파싱
header_end = decompressed.find(b'\0')
header = decompressed[:header_end].decode()
obj_type, size_str = header.split(' ')
return obj_type, int(size_str), len(compressed)
def find_duplicates(self):
"""중복 객체 찾기"""
content_map = defaultdict(list)
for root, dirs, files in os.walk(self.objects_dir):
if 'pack' in root:
continue
for file in files:
if len(file) == 38:
path = os.path.join(root, file)
sha = os.path.basename(root) + file
with open(path, 'rb') as f:
content = f.read()
content_hash = hashlib.md5(content).hexdigest()
content_map[content_hash].append(sha)
# 중복 찾기
duplicates = {k: v for k, v in content_map.items() if len(v) > 1}
return duplicates
def suggest_gc_settings(self, stats):
"""GC 설정 제안"""
total_objects = sum(s['count'] for s in stats.values())
total_size = sum(s['size'] for s in stats.values())
suggestions = []
if total_objects > 10000:
suggestions.append("git config gc.auto 5000")
suggestions.append("Consider running 'git gc --aggressive'")
if total_size > 100 * 1024 * 1024: # 100MB
suggestions.append("git config pack.packSizeLimit 10m")
suggestions.append("Consider using 'git repack -a -d'")
return suggestions
# 사용 예제
analyzer = GitStorageAnalyzer()
stats = analyzer.analyze_objects()
print("Object Statistics:")
for obj_type, data in stats.items():
compression_ratio = (1 - data['compressed'] / data['size']) * 100 if data['size'] > 0 else 0
print(f"{obj_type}: {data['count']} objects, "
f"{data['size']/1024:.1f}KB -> {data['compressed']/1024:.1f}KB "
f"({compression_ratio:.1f}% compression)")
8. Git 네트워크 프로토콜
Git 전송 프로토콜 분석
1
2
3
4
5
6
7
8
# 1. HTTP(S) 프로토콜 디버깅
GIT_TRACE_PACKET=1 GIT_TRACE=1 GIT_CURL_VERBOSE=1 git clone https://github.com/user/repo
# 2. SSH 프로토콜 디버깅
GIT_SSH_COMMAND="ssh -v" git clone git@github.com:user/repo
# 3. Git 프로토콜 분석
GIT_TRACE_PACKET=1 git ls-remote git://github.com/user/repo
Pack 프로토콜 구현
아래 GitProtocol은 참조 광고와 want 목록의 형식을 추적하는 시뮬레이션입니다. 실제 서버와 연결해 clone·fetch를 끝내는 클라이언트는 아닙니다. 길이 접두사와 내용이 어디서 나뉘는지 확인한 뒤 출력된 want 목록이 입력 객체 ID와 연결되는지 살펴보세요.
심화 참고: pkt-line과 want 목록 형식 시뮬레이션
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# git_protocol.py - Git 네트워크 프로토콜 시뮬레이션
import struct
import zlib
class GitProtocol:
def __init__(self):
self.capabilities = [
'multi_ack_detailed',
'side-band-64k',
'ofs-delta',
'agent=git/2.0.0'
]
def pkt_line(self, data):
"""Git pkt-line 형식으로 인코딩"""
if data is None:
return b'0000'
if isinstance(data, str):
data = data.encode()
length = len(data) + 4
return f'{length:04x}'.encode() + data
def parse_pkt_line(self, data):
"""pkt-line 파싱"""
lines = []
offset = 0
while offset < len(data):
length_hex = data[offset:offset+4].decode()
if length_hex == '0000':
break
length = int(length_hex, 16)
line = data[offset+4:offset+length]
lines.append(line)
offset += length
return lines
def ref_advertisement(self, refs):
"""참조 광고 생성"""
lines = []
# 첫 번째 참조는 capabilities 포함
first_ref = list(refs.items())[0]
caps = '\0' + ' '.join(self.capabilities)
lines.append(f"{first_ref[1]} {first_ref[0]}{caps}")
# 나머지 참조들
for ref, sha in list(refs.items())[1:]:
lines.append(f"{sha} {ref}")
# pkt-line 형식으로 변환
result = b''
for line in lines:
result += self.pkt_line(line)
result += self.pkt_line(None) # flush
return result
def parse_want_list(self, data):
"""클라이언트 want 리스트 파싱"""
wants = []
lines = self.parse_pkt_line(data)
for line in lines:
if line.startswith(b'want '):
sha = line[5:45].decode()
wants.append(sha)
return wants
def create_pack(self, objects):
"""Pack 데이터 생성"""
# Pack 헤더
signature = b'PACK'
version = struct.pack('>I', 2)
count = struct.pack('>I', len(objects))
pack_data = signature + version + count
# 객체들 추가
for obj in objects:
# 간단한 구현 (실제는 더 복잡)
obj_type = 1 # commit
size = len(obj['data'])
# 타입과 크기 인코딩
header = (obj_type << 4) | (size & 0x0f)
size >>= 4
while size:
header |= 0x80
pack_data += bytes([header])
header = size & 0x7f
size >>= 7
pack_data += bytes([header])
# 압축된 데이터
pack_data += zlib.compress(obj['data'])
# 체크섬
# 실제로는 SHA-1 사용
checksum = b'\x00' * 20
pack_data += checksum
return pack_data
# 프로토콜 시뮬레이션
protocol = GitProtocol()
# 서버 측 참조 광고
refs = {
'refs/heads/main': 'abc123' + '0' * 34,
'refs/heads/develop': 'def456' + '0' * 34,
}
ref_adv = protocol.ref_advertisement(refs)
print(f"Reference advertisement: {len(ref_adv)} bytes")
# 클라이언트 요청
want_request = protocol.pkt_line('want abc1230000000000000000000000000000000000')
wants = protocol.parse_want_list(want_request)
print(f"Client wants: {wants}")
9. Git 성능 최적화 기법
고급 설정과 튜닝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 코어 설정
git config core.preloadindex true # 인덱스 미리 로드
git config core.fscache true # 파일시스템 캐시
git config core.commitGraph true # 커밋 그래프 사용
# 2. Pack 설정
git config pack.threads 0 # CPU 코어 수만큼 스레드
git config pack.windowMemory 1g # Pack 윈도우 메모리
git config pack.packSizeLimit 2g # Pack 파일 크기 제한
# 3. GC 설정
git config gc.auto 5000 # 자동 GC 트리거
git config gc.autopacklimit 50 # Pack 파일 수 제한
git config gc.pruneExpire 2.weeks.ago
# 4. 전송 설정
git config http.postBuffer 524288000 # 500MB
git config core.compression 9 # 최대 압축
# 5. Diff 설정
git config diff.algorithm histogram # 더 나은 diff 알고리즘
git config diff.renames copies # 복사 감지
성능 벤치마킹
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/env python3
# git_benchmark.py
import time
import subprocess
import statistics
class GitBenchmark:
def __init__(self, repo_path):
self.repo_path = repo_path
def benchmark_operation(self, operation, iterations=5):
"""Git 작업 벤치마킹"""
times = []
for _ in range(iterations):
start = time.time()
subprocess.run(operation, shell=True, cwd=self.repo_path,
capture_output=True)
end = time.time()
times.append(end - start)
return {
'mean': statistics.mean(times),
'median': statistics.median(times),
'stdev': statistics.stdev(times) if len(times) > 1 else 0,
'min': min(times),
'max': max(times)
}
def run_benchmarks(self):
"""주요 작업 벤치마크"""
operations = {
'status': 'git status',
'log': 'git log --oneline -100',
'diff': 'git diff HEAD~10',
'add_all': 'git add -A',
'commit': 'git commit --amend --no-edit',
'checkout': 'git checkout HEAD~1 && git checkout -'
}
results = {}
for name, cmd in operations.items():
print(f"Benchmarking {name}...")
results[name] = self.benchmark_operation(cmd)
return results
def optimize_repo(self):
"""저장소 최적화"""
optimizations = [
('Garbage collection', 'git gc --aggressive'),
('Repack', 'git repack -a -d -f --depth=250 --window=250'),
('Commit graph', 'git commit-graph write --reachable'),
('Prune', 'git prune --expire now')
]
for desc, cmd in optimizations:
print(f"Running {desc}...")
subprocess.run(cmd, shell=True, cwd=self.repo_path)
# 사용 예제
benchmark = GitBenchmark('/path/to/large/repo')
print("Before optimization:")
before = benchmark.run_benchmarks()
benchmark.optimize_repo()
print("\nAfter optimization:")
after = benchmark.run_benchmarks()
# 개선율 계산
for op in before:
improvement = (before[op]['mean'] - after[op]['mean']) / before[op]['mean'] * 100
print(f"{op}: {improvement:.1f}% improvement")
10. Git 데이터 복구
손상된 저장소 복구
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 저장소 검증
git fsck --full --no-reflogs
# 2. 손상된 객체 찾기
find .git/objects -type f -empty
# 3. 객체 복구 시도
git hash-object -w path/to/file
# 4. 인덱스 재구성
rm .git/index
git reset
# 5. Reflog에서 복구
git reflog expire --expire=now --all
git fsck --full --no-reflogs | grep commit
고급 복구 스크립트
이 코드는 손상 후보 탐색과 복구 시도를 연결하는 참고 도구입니다. 먼저 복사한 테스트 저장소에서 객체 분석 결과를 git fsck 결과와 비교하세요. 이 글에는 원본 데이터 복구를 보장하는 검증이 없으므로 “스크립트가 끝남”과 “필요한 커밋·파일이 돌아옴”을 따로 확인해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/usr/bin/env python3
# git_recovery.py
import os
import subprocess
import zlib
import hashlib
class GitRecovery:
def __init__(self, git_dir='.git'):
self.git_dir = git_dir
def find_corrupted_objects(self):
"""손상된 객체 찾기"""
corrupted = []
result = subprocess.run(['git', 'fsck', '--full'],
capture_output=True, text=True)
for line in result.stderr.split('\n'):
if 'corrupt' in line or 'missing' in line:
# SHA 추출
parts = line.split()
for part in parts:
if len(part) == 40 and all(c in '0123456789abcdef' for c in part):
corrupted.append(part)
return corrupted
def recover_from_pack(self, sha):
"""Pack 파일에서 객체 복구 시도"""
pack_dir = os.path.join(self.git_dir, 'objects', 'pack')
for pack_file in os.listdir(pack_dir):
if pack_file.endswith('.idx'):
pack_base = pack_file[:-4]
# 인덱스에서 객체 찾기
result = subprocess.run(
['git', 'verify-pack', '-v', os.path.join(pack_dir, pack_file)],
capture_output=True, text=True
)
if sha in result.stdout:
print(f"Found {sha} in {pack_base}")
# 객체 추출
self.extract_from_pack(pack_base, sha)
return True
return False
def recover_from_remote(self, remote='origin'):
"""원격 저장소에서 복구"""
print(f"Attempting recovery from {remote}...")
# 모든 참조 가져오기
subprocess.run(['git', 'fetch', remote, '--all'])
# fsck 재실행
result = subprocess.run(['git', 'fsck', '--full'],
capture_output=True, text=True)
return 'error' not in result.stderr.lower()
def rebuild_index(self):
"""인덱스 재구성"""
index_path = os.path.join(self.git_dir, 'index')
# 백업
if os.path.exists(index_path):
os.rename(index_path, index_path + '.backup')
# 재구성
subprocess.run(['git', 'read-tree', 'HEAD'])
def emergency_backup(self, backup_path):
"""긴급 백업"""
import shutil
print(f"Creating emergency backup at {backup_path}...")
# 중요 파일들 백업
important_files = [
'objects',
'refs',
'logs',
'config',
'HEAD',
'packed-refs'
]
os.makedirs(backup_path, exist_ok=True)
for item in important_files:
src = os.path.join(self.git_dir, item)
dst = os.path.join(backup_path, item)
if os.path.exists(src):
if os.path.isdir(src):
shutil.copytree(src, dst, dirs_exist_ok=True)
else:
shutil.copy2(src, dst)
print("Backup completed")
# 사용 예제
recovery = GitRecovery()
# 손상된 객체 찾기
corrupted = recovery.find_corrupted_objects()
print(f"Found {len(corrupted)} corrupted objects")
# 복구 시도
for sha in corrupted:
if recovery.recover_from_pack(sha):
print(f"Recovered {sha}")
else:
print(f"Failed to recover {sha}")
# 원격에서 복구
if corrupted:
recovery.recover_from_remote()
마무리
Git의 내부 동작 원리를 이해하면 Git을 훨씬 더 효과적으로 사용할 수 있습니다. .git 폴더는 단순한 데이터 저장소가 아니라, 정교하게 설계된 버전 관리 시스템의 핵심입니다.
핵심 포인트:
- Git은 Content-Addressable 파일시스템
- 모든 것은 객체(Blob, Tree, Commit, Tag)
- Pack 파일로 효율적인 저장
- Reflog로 모든 작업 추적 가능
- 다양한 복구 옵션 제공
Git의 내부를 이해하면 문제 해결 능력이 크게 향상되고, 더 고급 기능들을 자신 있게 사용할 수 있습니다.
다음 심화편에서는 고급 브랜치 전략과 릴리스 관리에 대해 다루겠습니다.
📚 GitHub 마스터하기 시리즈
🌱 기초편 (입문자)
💼 실전편 (중급자)
🚀 고급편 (전문가)
- GitHub Actions 입문
- Actions 고급 활용
- Webhooks와 API
- GitHub Apps 개발
- 보안 기능
- GitHub Packages
- Codespaces
- GitHub CLI
- 통계와 인사이트
!["[이제와서 시작하는 GitHub 마스터하기 - 심화편 #2] Git 내부 동작 원리: .git 폴더의 비밀"](/assets/img/posts/github/github-expert-02.png)