GoSuda

Git 모노레포 재고찰: 인간과 코딩 에이전트를 위한 CAS 네이티브 아키텍처

By Lemon Mint views ...

모든 성장하는 엔지니어링 조직은 결국 다중 저장소(many repositories)와 단일 저장소(one repository) 사이에서 선택해야 한다. 수십 개의 작은 저장소로 시작하면 처음에는 삶이 단순해 보인다. 각 라이브러리는 자체 이슈 트래커, 자체 CI 파이프라인, 몇 초 만에 끝나는 git clone을 갖는다. 그러다 핵심 네트워킹 패키지의 struct 하나를 변경하면, 오후 내내 여섯 개의 하위 저장소에 대한 버전 범프(version-bump) 풀 리퀘스트를 열고, 위상 정렬 순서대로 CI 대기열이 비워지기를 기다리게 된다.

모든 것을 전통적인 Git 모노레포로 옮기면, 저장소 간 조정 비용을 저장소 크기의 문제로 맞바꾸게 된다. 커밋 이력이 깊어지고 과거 리비전에 바이너리 자산이 쌓이면서 git fetch는 점점 느려진다. 작업 트리는 기가바이트 단위로 부풀어 오른다. 머지않아 누군가는 일상적인 개발을 견딜 만한 수준으로 유지하기 위해 맞춤형 가상 파일시스템 데몬, 전용 코드 리뷰 서버, 또는 원격 빌드 워커 집단을 구축하자고 제안한다.

자율형 코딩 에이전트가 팀에 합류하면 두 가지 실패 양상은 모두 악화된다. 다중 저장소 구조에서는 에이전트가 프로젝트 간 호출자를 파악하지 못하고, 저장소마다 자격 증명을 오가며 관리해야 한다. 전통적인 모노레포에서는 작업을 초기화할 때마다 에이전트 컨테이너의 디스크와 네트워크 대역폭이 고갈된다. 동시에 여러 에이전트가 같은 브랜치를 건드리면 끝없는 리베이스 경쟁이 발생한다.

google3 아키텍처에서 영감을 받아, 우리는 단일 트렁크 원자적 변경(single-trunk atomic changes), 1초 미만의 희소 온보딩(sub-second sparse onboarding), 계층적 OWNERS 거버넌스, 그리고 표준 Git 기반 요소와 범용 객체 스토리지를 사용하는 독립형 오픈소스 미러를 구축하였다.

이 설계는 맞춤형 파일시스템 데몬도, 서드파티 리뷰 어플라이언스도 필요로 하지 않는데, 그 이유는 Git을 Merkle 트리 기반 Content-Addressable Store(CAS)로 취급하기 때문이다.

느린 Git 모노레포의 핵심에 놓인 오해

개발자들이 Git이 대규모 모노레포로 확장되지 않는다고 불평할 때, 실제로 Git의 객체 데이터베이스가 한계에 도달한 경우는 드물다. .git/objects 내부에서 Git은 SHA-1(또는 SHA-256) 콘텐츠 해시를 키로 하는 세 가지 불변 기본 요소를 저장한다.

  1. blob: 단일 파일의 원시 바이트 내용이다.
  2. tree: 파일 이름과 권한을 blob 또는 하위 tree 해시에 대응시키는 디렉터리 목록이다.
  3. commit: 하나의 루트 tree 해시, 0개 이상의 부모 commit 해시, 그리고 작성자 메타데이터를 가리키는 작은 텍스트 헤더(일반적으로 200~300바이트)이다.

모노레포에 50,000개의 파일이 있고 그중 하나의 파일에서 한 줄을 수정하면, 새 커밋은 나머지 49,999개 파일의 정확히 같은 blob 및 tree 해시를 재사용한다.

전통적인 Git 모노레포가 무겁게 느껴지는 이유는, 관습이 refs/heads/main이라는 단일 참조에 서로 충돌하는 세 가지 역할을 부여하기 때문이다.

  • 모든 프로젝트의 전체 통합 디렉터리 트리를 보유한다.
  • 첫날까지 거슬러 올라가는 전체 부모 연결 역사 커밋 체인을 끌고 다닌다.
  • Git 서버가 모든 git clone 및 git fetch에서 기본적으로 광고하고 협상하는 네임스페이스인 refs/heads/* 안에 존재한다.

개발자나 CI 러너가 refs/heads/main을 fetch할 때마다, 클라이언트와 서버는 수천 개의 과거 리비전에 걸친 커밋 그래프 도달 가능성을 협상한다. 누군가 3년 전에 실수로 100 MB 아카이브를 커밋하고 다음 날 삭제했다면, 그 blob은 여전히 refs/heads/main에서 도달 가능한 과거 tree에 매달려 있다.

참조 네임스페이스의 분리

Git의 와이어 프로토콜은 표준 clone 중에 refs/heads/*와 refs/tags/*만 광고한다. refs/trunk/*나 refs/ws/*와 같은 맞춤형 네임스페이스 아래의 참조는 서버의 동일한 객체 저장소에 존재하며, 동일한 packfile 델타 압축의 이점을 누리고, 명시적으로 fetch하거나 push할 수 있다. 그럼에도 기본 clone에는 어떠한 추가 부담도 주지 않는다.

이처럼 단일 저장소 안의 서로 다른 참조에 역할을 나누어 배분하면 병목이 제거된다.

1refs/heads/main               제어 평면 전용(약 20 KB: 규칙, .gitconfig, 동기화 설정, 스크립트)
2refs/trunk/main               최신 통합 루트 tree를 가리키는 부모 없는 스냅샷 커밋
3refs/trunk/history            정확히 동일한 루트 tree SHA-1을 공유하는 선형 부모 연결 커밋 체인
4refs/ws/<actor>/<id>          개발자 또는 에이전트별 격리된 워크스페이스 스냅샷(변경 목록)
5refs/ws/cl/<change-id>/<tip>  불변 패치셋: 리뷰에 제출된 변경의 모든 tip
6refs/heads/land/<id>          병합 대기열 트리거로, 랜딩 직후 즉시 삭제됨

1. 제어 평면(refs/heads/main)

기본 브랜치에는 애플리케이션 소스 코드가 전혀 없다. 저장소 운영 규칙(AGENTS.md, README.md), 워크플로 별칭을 담은 공유 .gitconfig, Starlark 동기화 설정(sync.star), 그리고 서버 측 병합 스크립트만을 추적한다. 기본 브랜치를 clone하면 약 20 KB가 전송되며 밀리초 단위로 완료된다.

2. 부모 없는 트렁크 스냅샷(refs/trunk/main)

refs/trunk/main은 조직 전체의 완전하고 통합된 소스 트리(//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*, //corp/*, //third_party/*, //experimental/*)를 가리키지만, 해당 커밋 객체는 -p 부모 플래그 없이 생성된다.

refs/trunk/main의 부모는 0개(parents = [])이므로, 이를 fetch하는 데에는 커밋 그래프 협상이 필요하지 않다. Git 서버는 역사를 순회하지 않고, 단일 헤드 커밋과 현재 디렉터리 tree 객체들만 전송한다. 지난달의 과거 바이너리나 삭제된 파일은 refs/trunk/main에서 도달할 수 없으므로 전송되지 않는다.

3. 선형 감사 이력(refs/trunk/history)

개발자는 회귀를 조사할 때 여전히 git blame과 git log가 필요하다. 변경이 랜딩될 때마다 병합 스크립트는 정확히 동일한 루트 $NEW_TREE SHA-1 해시를 가리키는 두 개의 커밋 객체를 생성한다.

 1# 즉시 depth-1 워크스페이스 체크아웃을 위한 부모 없는 스냅샷 커밋
 2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
 3
 4# 선형 git log 및 git blame을 위한 부모 연결 커밋
 5HIST=$(git commit-tree "$NEW_TREE" -p "$OLD_HIST" -m "$RAW_MSG")
 6
 7git push --atomic origin \
 8  --force-with-lease="refs/trunk/main:${OLD_MAIN}" \
 9  --force-with-lease="refs/trunk/history:${OLD_HIST}" \
10  "${SNAP}:refs/trunk/main" \
11  "${HIST}:refs/trunk/history"

refs/trunk/main과 refs/trunk/history를 함께 유지한다고 해서 저장 공간 사용량이 두 배가 되지는 않는다. 두 커밋이 동일한 $NEW_TREE 해시를 참조하므로, Git의 Content-Addressable Store는 tree와 blob 객체를 100% 중복 제거한다. 부모 없는 스냅샷이 추가로 차지하는 저장 공간은 250바이트짜리 커밋 헤더 하나뿐이다.

4. 격리된 워크스페이스(refs/ws/<actor>/<id>)

진행 중인 작업 브랜치는 refs/heads/*가 아니라 refs/ws/<actor>/<id>에 push된다. 백 개의 코딩 에이전트가 1분마다 체크포인트를 저장하더라도 브랜치 드롭다운을 어지럽히지 않으며, 사람인 엔지니어의 git fetch를 느리게 만들지 않는다.

네 단계 온보딩: 희소 및 blobless 체크아웃의 작동 방식

이 참조 레이아웃에서는 새 개발자나 일시적인 코딩 에이전트 컨테이너가 맞춤형 CLI나 FUSE 드라이버를 설치할 필요가 없다. 온보딩은 표준 Git과 저장소의 별칭만으로 네 단계로 이루어진다.

 1# 1. blob 필터링과 희소 모드를 활성화하여 20 KB 제어 평면 clone
 2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
 3cd monorepo
 4
 5# 2. 저장소 별칭을 로드하고 refs/trunk/*를 연결
 6git config include.path ../.gitconfig
 7git trunk-init
 8
 9# 3. 이 작업에 필요한 도메인 디렉터리를 선언
10git sparse-checkout set net/relay ipc/codec
11
12# 4. 최신 트렁크 스냅샷을 기반으로 격리된 워크스페이스 생성
13git ws-new alice/optimize-framing

각 단계는 필요한 것만 fetch한다.

  1. git clone --filter=blob:none --sparse는 refs/heads/main에서 20 KB 제어 평면을 내려받는다.
  2. git trunk-init은 +refs/trunk/*:refs/remotes/origin/trunk/*를 설정하고 blobless fetch를 실행한다. Git은 refs/trunk/main의 tree 메타데이터(보통 수십 킬로바이트)를 내려받으며, 파일 blob은 0개를 내려받는다.
  3. git sparse-checkout set net/relay ipc/codec은 Git의 cone 모드 인덱스 필터에 루트 디렉터리와 그 두 하위 트리만 구체화하도록 지시한다.
  4. git ws-new은 origin/trunk/main을 기반으로 하는 새 브랜치로 전환한다. 이후 Git은 net/relay와 ipc/codec 내부의 blob 객체만 필요할 때 요청한다. 모노레포의 나머지 95%는 디스크에 전혀 내려받지 않는다.

향후 전망: refs/trunk/main을 가상 파일시스템(VFS)으로 마운트하기

blobless clone을 사용한 희소 체크아웃은 추가적인 클라이언트 도구 없이도 일상 워크플로를 충족한다. refs/trunk/main은 이미 Content-Addressable Store 안의 순수한 Merkle 트리이므로, 이 아키텍처는 Linux의 FUSE, macOS의 FileProvider, Windows의 ProjFS를 통한 선택적 사용자 공간 가상 파일시스템(VFS)을 위한 직접적인 기반도 제공한다.

  1. 즉시 전체 트리 마운트: 경량 로컬 마운트 데몬이 refs/trunk/main의 루트 tree 객체를 읽고, 밀리초 안에 모노레포 전체 디렉터리 계층을 작업 디렉터리에 투영하므로, git sparse-checkout set을 실행할 필요가 없어진다.
  2. 온디맨드 blob 페이징: 컴파일러, IDE, 또는 에이전트가 open(2) 또는 read(2) 시스템 호출을 발행하기 전까지 파일 내용은 가상 상태로 남는다. 그러면 데몬은 CAS 엔드포인트에서 해당 SHA-1 blob을 가져와 로컬 NVMe 스토리지에 캐시한다.
  3. 워크스페이스용 Copy-on-Write 오버레이: 로컬 편집 내용은 경량 상위 오버레이 계층에 기록된다. git ws-save를 실행하면 해당 오버레이의 변경된 경로만 해시하고, 갱신된 blob과 tree 객체를 기록한 뒤, 수정되지 않은 디렉터리를 스캔하지 않고 결과 커밋을 refs/ws/<actor>/<id>에 push한다.

순수 Git 기반의 변경 목록 모델: 서버 측 인메모리 랜딩

전통적인 Git 워크플로에서 기능 브랜치를 병합하면, 비선형 역사를 만드는 두 부모 병합 커밋이 생기거나, 개발자가 수동으로 리베이스하며 main에 대한 동시 push와 경쟁해야 한다.

google3의 변경 목록(Changelist, CL) 모델에서 영감을 받아, 우리 아키텍처의 모든 워크스페이스는 트렁크 스냅샷에 대한 단일 원자적 변경을 나타낸다. 개발자나 에이전트는 ws/alice/optimize-framing에서 작업하는 동안 열 개의 점진적 커밋(wip, fix tests, lint)을 만들 수 있다. 변경이 랜딩될 준비가 되면, 서버는 CI 러너에서 작업 디렉터리를 전혀 체크아웃하지 않고 워크스페이스의 차이를 하나의 선형 트렁크 리비전으로 압축한다.

랜딩 스크립트(land.sh)가 Git 2.40+ 플러밍을 사용하여 인메모리에서 실행되는 방식은 다음과 같다.

 1# 1. 워크스페이스가 분기된 루트 스냅샷 커밋을 식별
 2BASE=$(git rev-list --max-parents=0 "$WORK_SHA")
 3
 4BASE_TREE=$(git rev-parse "${BASE}^{tree}")
 5MAIN_TREE=$(git rev-parse "${OLD_MAIN}^{tree}")
 6WORK_TREE=$(git rev-parse "${WORK_SHA}^{tree}")
 7
 8# 2. 작업 트리 체크아웃 없이 인메모리에서 병합된 루트 tree 계산
 9if [ "$MAIN_TREE" = "$BASE_TREE" ]; then
10  NEW_TREE="$WORK_TREE"
11elif OUT=$(git merge-tree --write-tree --merge-base="$BASE_TREE" "$MAIN_TREE" "$WORK_TREE"); then
12  NEW_TREE=$(head -n1 <<<"$OUT")
13else
14  echo "병합 충돌이 감지되었습니다. 리베이스하려면 git ws-sync를 실행하십시오." >&2
15  exit 1
16fi

2단계에서:

  • 워크스페이스 브랜치에서 로컬 커밋을 몇 개 만들었는지와 관계없이, 스크립트는 세 개의 tree 해시만 비교한다. 즉, 원래의 기준 스냅샷($BASE_TREE), 현재 트렁크 스냅샷($MAIN_TREE), 그리고 최종 워크스페이스 tree($WORK_TREE)이다.
  • Alice가 //net/relay를 편집하는 동안 refs/trunk/main이 //web/blog에서 앞으로 진행되었다면, git merge-tree --write-tree는 러너의 파일시스템에 파일을 기록하지 않고 Git 객체 저장소 내부에서 밀리초 안에 결합된 루트 tree $NEW_TREE를 계산한다.
  • 아래의 정책 검사를 통과하면, --force-with-lease를 포함한 하나의 git push --atomic이 refs/trunk/main을 갱신하고 refs/trunk/history에 하나의 선형 커밋을 추가하며, 랜딩 트리거 refs/heads/land/<id>는 삭제된다.

아키텍처 경계의 강제: 4계층 가시성과 계층적 OWNERS

코드를 하나의 저장소로 통합하는 방식은, 커밋이 랜딩되기 전에 도메인 간 경계가 기계적으로 강제될 때에만 성립한다. git merge-tree --write-tree가 $NEW_TREE를 생성한 직후이고 git push --atomic이 트렁크를 갱신하기 전에, 우리의 정책 엔진은 git diff-tree "$MAIN_TREE" "$NEW_TREE"에 대해 두 개의 O(delta) 검사를 실행한다.

1. 4계층 도메인 가시성

루트 tree는 네 개의 가시성 계층으로 나뉜다.

디렉터리 접두사계층허용된 import금지된 importOWNERS 게이트
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*공개 프로덕션공개 프로덕션, //third_party/*//corp/*, //experimental/*강제 적용
//third_party/*외부 벤더자체 포함형 또는 //third_party/*third_party가 아닌 모든 도메인강제 적용
//corp/*내부 비공개공개 프로덕션, //corp/*, //third_party/*//experimental/*강제 적용
//experimental/<actor>/*샌드박스corp이 아닌 모든 패키지//corp/*면제(자동 승인)

//web/*의 공개 패키지가 //corp/*의 내부 패키지나 //experimental/*의 졸업하지 않은 프로토타입을 import하려고 하면, 랜딩 작업은 refs/trunk/main에 손을 대기 전에 중단된다.

2. Gerrit 서버 없이 수행하는 계층적 OWNERS 검증

검증기는 상속 규칙이 적용되는 디렉터리 수준의 OWNERS 파일을 강제한다.

  • $MAIN_TREE와 $NEW_TREE 사이에서 수정된 모든 경로에 대해, 검증기는 파일의 디렉터리에서 //OWNERS까지 상위로 탐색한다.
  • 모든 OWNERS 파일은 상위 디렉터리로부터의 상속된 승인을 차단하기 위해 set noparent를 선언할 수 있으며, 파일 이름 글롭에 따라 승인을 한정하기 위해 per-file *.sql=@dba-team을 선언할 수도 있다.
  • 변조 방지 불변식: 기존 OWNERS 파일은 항상 $NEW_TREE가 아니라 $MAIN_TREE(현재 트렁크 tree)에서 읽는다. 승인되지 않은 작성자는 같은 워크스페이스 커밋에서 OWNERS를 편집하여 스스로에게 권한을 부여할 수 없다.
  • 샌드박스 면제: //experimental/<actor>/* 아래의 모든 경로는 OWNERS 검사를 우회하므로, 엔지니어와 에이전트는 git ws-exp <actor> <project>로 자유롭게 프로토타입을 만들 수 있고, 이후 git ws-graduate로 패키지를 프로덕션 도메인으로 졸업시킬 수 있다.

마찰 없는 코드 리뷰: 일시적 PR과 스냅샷 diff

refs/heads/main은 제어 평면만 보유하고 refs/trunk/main은 부모 없는 커밋이므로, 워크스페이스 브랜치에서 main으로 향하는 표준 GitHub Pull Request를 직접 열면 관련 없는 역사들을 비교하려고 시도하게 된다. 우리는 서로 다른 변경에 맞춘 세 가지 리뷰 워크플로로 이 문제를 해결하였다.

  1. 즉시 두 점(two-dot) 워크스페이스 diff (git ws-save + 엣지 Diff 라우터) git ws-save로 워크스페이스를 push하면 refs/ws/<actor>/<id>가 경량 엣지 워커를 통해 노출된다. 이 워커는 refs/trunk/main과 워크스페이스 참조를 각각의 커밋 SHA로 해석한 뒤, GitHub의 두 점 비교 화면(/compare/<trunk-sha>..<ws-sha>)으로 리디렉션한다. 작은 패치나 에이전트가 생성한 변경의 경우, 리뷰어는 diff를 검토하고 웹 UI에서 바로 Land를 클릭할 수 있다.
  2. 일시적 GitHub Pull Request (git ws-pr) GitHub에서 줄 단위 리뷰를 하려면, git ws-pr이 루트 스냅샷 커밋($(git rev-list --max-parents=0 HEAD))을 refs/heads/base/<id>에, 워크스페이스 tip을 refs/heads/review/<id>에 push한다. 그런 다음 review/<id>를 base/<id>와 비교하는 GitHub Pull Request를 연다. 기준 브랜치가 워크스페이스가 분기된 스냅샷이므로, GitHub의 Files changed 탭에는 워크스페이스의 차이만 표시된다. 리뷰어가 Merge pull request를 클릭하면, 웹훅이 head 커밋을 refs/heads/land/<id>로 승격하고, 인메모리 랜딩 파이프라인을 실행하며, refs/heads/base/<id>와 refs/heads/review/<id>를 모두 자동으로 삭제한다.
  3. 외부 미러 Pull Request 오픈소스 기여자는 모노레포 워크플로를 배울 필요가 전혀 없다. 그들은 평소와 같이 독립형 미러 저장소를 포크하고 그곳에 Pull Request를 연다.

변경, 패치셋, 그리고 승인 이월

브랜치 이름으로는 변경을 식별할 수 없다. 에이전트는 랜딩 전에 브랜치 이름을 바꾸거나, tip을 amend하거나, 더 최신 트렁크에 여러 번 리베이스할 수 있다. Gerrit과 마찬가지로, 이 워크플로는 tip 커밋 메시지의 Change-Id: I<40 hex> 트레일러로 변경을 식별한다. git ws-save, git ws-pr, git ws-land은 tip에 트레일러가 없으면 추가하고, 있으면 그대로 유지하므로, amend, 리베이스, 이름 변경 후에도 식별자가 유지된다. 랜딩된 트렁크 커밋은 트레일러를 보존하고 해당 리뷰 Pull Request의 번호를 기록하므로, git trunk-cl <number|Change-Id>가 refs/trunk/history에서 이를 찾아낸다.

리뷰에 제출된 모든 tip은 패치셋이 된다. git ws-pr은 이를 리뷰 브랜치 옆의 불변 참조 refs/ws/cl/<change-id>/<tip>에 push한다. amend나 git ws-sync 이후 git ws-pr을 다시 실행하면 같은 Pull Request에 패치셋이 추가된다. git ws-ps는 현재 변경의 패치셋 목록을 보여 주고, git ws-psdiff <a> <b>는 두 패치셋 사이의 interdiff를 출력하므로, 리뷰어는 지난 라운드 이후 바뀐 내용만 읽으면 된다.

GitHub은 승인을 단일 커밋에 묶으므로, 리베이스할 때마다 일반적으로 새로운 리뷰가 필요하다. 랜딩 파이프라인은 같은 변경의 다른 패치셋에 부여된 승인도, 두 패치셋이 각자의 기준 스냅샷에 대해 동일한 패치를 만드는 경우에는 수용한다. 이를 위해 전체 바이너리 diff에 대해 git patch-id --verbatim을 비교한다. 최신 트렁크로의 깨끗한 리베이스는 추가 리뷰 없이 랜딩되지만, 공백이나 파일 모드 변경을 포함한 모든 내용 편집에는 새로운 승인이 필요하다.

O(delta) 양방향 동기화와 별도의 프로덕션 브랜치

많은 조직이 외부 사용자가 모노레포를 건드리지 않고 go get, cargo install을 하거나 단일 프로젝트를 포크할 수 있도록 개별 패키지를 독립형 GitHub 저장소로 공개한다. 정적 블로그나 릴리스 게이트가 있는 웹 앱과 같은 일부 프로젝트는 main과 함께 별도의 production 브랜치도 유지한다.

우리의 Starlark 동기화 엔진은 외부로의 내보내기와 내부로의 기여를 모두 O(delta) 시간, 즉 모노레포의 크기가 아니라 커밋에서 변경된 파일의 양에 비례하는 시간에 처리한다.

  • Blobless 인덱스 스플라이싱: 미러 저장소의 하위 트리를 모노레포 루트 tree에 삽입하거나 제거할 때, 엔진은 GIT_NO_LAZY_FETCH=1을 설정하고 git update-index --index-info를 실행한 뒤 git write-tree --missing-ok를 실행한다. 수정되지 않은 하위 트리와 blob은 SHA-1로 참조된 상태를 유지하며, CI 러너로 내려받아지지 않는다.
  • 델타 전용 blob 사전 가져오기: 내보낸 커밋을 독립형 미러에 push하기 전에, 스크립트는 git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"를 실행하고, git cat-file --batch를 통해 수정된 파일의 이전 및 새 blob만 사전에 가져온다. 그러면 Git은 얇은 델타 pack을 계산하고 몇 초 안에 미러 저장소로 push할 수 있다.
  • 워크플로 및 릴리스 브랜치 보존: 외부 내보내기 중에 동기화기는 각 독립형 저장소의 고유한 .github/workflows/ 디렉터리를 보존하므로, 공개 CI 러너가 미러에서 프로젝트 테스트를 실행한다. Starlark 설정의 각 워크플로는 main과 같은 명시적 브랜치를 대상으로 하므로, 별도의 production 브랜치에서 배포하는 저장소의 릴리스 게이트가 보존된다. 모노레포 랜딩은 미러의 main 브랜치와 동기화되며, main에서 자동으로 생성된 메타데이터나 번역 커밋은 웹훅을 통해 refs/trunk/main으로 다시 동기화된다. main을 production으로 승격하는 것은 여전히 명시적인 릴리스 단계로 남는다.

맺음말

이 아키텍처는 Git의 객체 저장소를 Content-Addressable Store로 취급하고, refs/heads/main을 refs/trunk/main 및 refs/trunk/history와 분리하며, 각 변경을 불변 패치셋을 가진 Change-Id로 식별하고, git merge-tree --write-tree로 tree를 인메모리에서 병합한다. 온보딩 비용은 작업에 필요한 디렉터리 수에 따라 증가하며, 랜딩과 미러 동기화 비용은 저장소의 크기가 아니라 변경의 크기에 따라 증가한다.