-
Notifications
You must be signed in to change notification settings - Fork 3
Storage Format
.srcs 파일이 실제로 어떻게 생겼는지, 그리고 커밋할 때 바이트 단위로
무슨 일이 일어나는지를 다룬다. 이 문서는 System Overview에서 설명한
"역방향 델타" 개념을 코드 레벨로 한 단계 더 들어간 버전이다.
v2 파일은 첫 줄에 헤더가 있다.
# SimpleRCS v2.0; hash_algo=sha256; encoding=utf-8;
v1은 헤더가 없는 옛날 포맷이다 (아래 "v1과의 차이" 참고).
블록은 key @value@; 형태의 줄을 여러 개 이어붙인 것이고, 빈 줄 하나로
다음 블록과 구분된다. @ 문자는 값 안에서 @@로 이스케이프된다.
ver @1.2@;
date @2026-08-04T12:00:00@;
author @wkpark@;
log @fix typo@;
delta @0a1,1
> new line
@;
prev_hash @9f86d0...@;
hash @a3f5c2...@;
필드는 이런 식으로 채워진다.
-
ver,date,author,log— 항상 있음 -
text— 이 블록이 전체 텍스트를 담고 있을 때 (텍스트 HEAD) -
delta— 이 블록이 델타(다음 버전에서 이 버전으로 되돌리는 명령)를 담고 있을 때 -
binary— 콘텐츠가 바이너리일 때 (text/delta대신 씀).<길이>;base64,<인코딩된 데이터>형태 -
prev_hash,hash— v2부터. 각각 이전 블록 해시, 이 블록의 해시 -
signature— GPG 서명이 붙어 있으면 여러 줄 반복
여기가 좀 미묘한 지점이다. 해시는 저장된 델타가 아니라 그 버전의
논리적 전체 텍스트를 대상으로 계산한다 (codec.calculate_block_hash).
페이로드는 ver|date|author|log|<전체텍스트>|<이전해시> 순서로 이어
붙여서 해시한다.
왜 이렇게 하냐면 — HEAD였던 블록이 다음 커밋에서 델타로 바뀌어도, 그 블록의 해시값 자체는 바뀌면 안 되기 때문이다. 저장 표현이 바뀌어도 "이 버전이 실제로 담고 있던 내용"은 그대로니까, 해시도 그대로 유지돼야 체인이 끊기지 않는다.
commit()을 호출하면 (simple_rcs.py 커밋 메서드 안쪽):
- 지금 HEAD를 델타 블록 형태로 다시 인코딩한다 (
is_delta=True로_format_block호출) - 새 콘텐츠를 전체 텍스트 블록으로 인코딩한다 (
is_delta=False) - 스트림을 이전 HEAD 블록이 시작하던 위치로 되감는다
(
stream.seek(head_info["start"])) - "재인코딩된 이전 HEAD" + "새 HEAD"를 한 번의
write()호출로 그 자리에 쓴다 -
truncate()로 남는 꼬리를 잘라낸다
self.stream.seek(self.head_info["start"])
self.stream.write(old_block_bytes + new_block_bytes)
self.stream.truncate()한 번의 write()로 묶어서 쓰는 이유가 코드 주석에 명시돼 있다 — 두
번에 나눠 쓰면 프로세스가 그 사이에 죽었을 때 "이전 HEAD는 덮어썼는데
새 HEAD는 없는" 반쪽짜리 상태가 남을 수 있어서다.
정리하면: 매 커밋마다 마지막 블록 하나만 재작성하고, 그 앞의 모든 이력은 파일 안에서 원래 있던 위치 그대로 남는다. "파일 끝에 계속 이어붙이기"처럼 보이지만 정확히는 "마지막 블록 교체 + 새 블록 추가"다.
commit(..., snapshot=True)를 쓰면 3번 단계에서 이전 HEAD를 델타 대신
전체 텍스트로 저장한다. 그러면 그 지점에서 델타 체인이 끊긴다 —
그 버전을 읽을 때 델타를 하나도 안 거치고 바로 전체 텍스트를 읽을 수
있다는 뜻이다. 히스토리가 아주 길어질 게 예상되는 지점에서 미리 찍어두면
과거 조회 성능을 그 구간만큼 확보할 수 있다. 다만 자동으로 찍히는 게
아니라 호출하는 쪽에서 선택해야 한다.
checkout(ver)는 HEAD에서 시작해서 목표 버전을 찾을 때까지 뒤로 한
블록씩 걸어가며 델타를 적용한다 (_get_prev_block). 스냅샷 블록을
만나면 그 지점에서 바로 점프한다. 파일 전체를 메모리에 올리지 않고
필요한 블록만 순서대로 읽는다.
v1은 실제로 배포된 적 없는 초기 포맷이다 — 헤더가 없고, 델타 블록도
그냥 text 키를 썼다(v2의 delta 키 구분이 없음). 해시체인도, GPG
서명도 없다. simple_rcs.py에 self._version < 2 분기로 읽기 지원만
남아 있다. v1→v2 마이그레이션 도구는 저장소 히스토리에는 남아있지만
(add-then-revert 패턴으로 커밋 0c7783e/90f2706) 현재 트리에는 없다.
-
텍스트: RCS 스타일
diff -n스크립트. 사람이 봐도 읽을 수 있는 add/delete 명령 목록이다. -
바이너리: BSDIFF40 호환 패치. base64/base85로 인코딩해서 텍스트
블록 안에 넣는다. 포맷이 표준이라 시스템에 설치된
bsdiff/bspatch도구와도 상호운용된다.
리비전마다 전체 스냅샷을 찍는 게 아니기 때문에, 파일 전체 크기는 "리비전 수 × 페이지 크기"가 아니라 "델타 크기 합 + 전체 텍스트 하나"에 가깝다. 한 줄만 고친 커밋은 보통 수백 바이트짜리 델타 하나만 늘어난다. 이 특성을 DB에 저장할 때 실제로 어떻게 활용할 수 있는지는 Wiki Backend Design에 실측과 함께 정리해뒀다.