Merancang Ulang Monorepo Git: Arsitektur CAS-Native untuk Manusia dan Agen Pengodean

Setiap organisasi engineering yang terus berkembang pada akhirnya harus memilih antara banyak repositori dan satu repositori. Dimulai dengan puluhan repositori kecil, hidup terasa sederhana pada awalnya. Setiap pustaka memiliki issue tracker sendiri, pipeline CI sendiri, dan git clone yang selesai dalam hitungan detik. Kemudian Anda mengubah sebuah struct dalam paket jaringan inti dan menghabiskan sisa sore untuk membuka pull request kenaikan versi di enam repositori hilir, sambil menunggu antrean CI habis secara berurutan topologis.
Memindahkan semuanya ke monorepo Git tradisional berarti Anda menukar koordinasi lintas repositori dengan ukuran repositori. Seiring riwayat commit bertambah panjang dan aset biner menumpuk di revisi-revisi sebelumnya, git fetch melambat. Working tree membengkak hingga gigabyte. Tak lama kemudian, seseorang mengusulkan untuk membangun daemon virtual filesystem khusus, server code review tersendiri, atau sekumpulan remote build worker hanya agar pengembangan harian tetap dapat ditoleransi.
Ketika agen pengodean otonom bergabung dengan tim, kedua mode kegagalan tersebut menjadi semakin buruk. Polyrepo membutakan agen terhadap pemanggil lintas proyek dan memaksanya mengelola kredensial di berbagai repositori. Monorepo tradisional menghabiskan disk container dan bandwidth jaringan agen pada setiap inisialisasi tugas, sementara agen-agen yang bekerja bersamaan pada cabang yang sama menciptakan perlombaan rebase tanpa henti.
Terinspirasi oleh arsitektur google3, kami membangun perubahan atomik dengan satu trunk, onboarding sparse dalam waktu kurang dari satu detik, tata kelola OWNERS yang hierarkis, serta mirror open-source mandiri dengan Git plumbing standar dan object storage komoditas.
Desain ini tidak memerlukan daemon filesystem khusus maupun appliance review pihak ketiga, karena memperlakukan Git sebagai Merkle-tree Content-Addressable Store (CAS).
Kesalahpahaman Mendasar di Balik Lambatnya Monorepo Git
Ketika pengembang mengeluh bahwa Git tidak skalabel untuk monorepo besar, mereka jarang menyentuh batas pada object database Git. Di dalam .git/objects, Git menyimpan tiga primitif yang tidak dapat diubah, diindeks berdasarkan hash konten SHA-1 (atau SHA-256):
blob: Isi byte mentah dari sebuah file tunggal.tree: Daftar direktori yang memetakan nama file dan izin ke hashblobatautreeanak.commit: Header teks kecil (umumnya 200 hingga 300 byte) yang menunjuk ke satu hashtreeroot, nol atau lebih hashcommitinduk, dan metadata penulis.
Jika sebuah monorepo berisi 50.000 file dan Anda mengedit satu baris pada satu file, commit baru menggunakan kembali hash blob dan tree yang sama persis untuk 49.999 file lainnya.
Monorepo Git tradisional terasa berat karena konvensi membebani satu referensi, refs/heads/main, dengan tiga tugas yang saling bertentangan:
- Ia menyimpan seluruh pohon direktori terpadu dari setiap proyek.
- Ia menyeret seluruh rantai commit historis yang terhubung induk hingga hari pertama.
- Ia berada di dalam
refs/heads/*, namespace yang diiklankan dan dinegosiasikan oleh server Git secara default pada setiapgit clonedangit fetch.
Setiap kali pengembang atau runner CI melakukan fetch refs/heads/main, klien dan server menegosiasikan keterjangkauan commit graph di ribuan revisi historis. Jika seseorang tidak sengaja meng-commit arsip 100 MB tiga tahun lalu dan menghapusnya keesokan harinya, blob-nya tetap tergantung pada tree historis yang dapat dijangkau dari refs/heads/main.
Memisahkan Namespace Referensi
Protokol wire Git hanya mengiklankan refs/heads/* dan refs/tags/* selama clone standar. Referensi di bawah namespace kustom seperti refs/trunk/* atau refs/ws/* berada di object store yang sama persis pada server, memperoleh manfaat kompresi delta packfile yang sama, dan dapat di-fetch atau di-push secara eksplisit, namun tidak menimbulkan overhead apa pun pada clone default.
Memisahkan tugas-tugas ini ke referensi yang berbeda dalam satu repositori menghilangkan bottleneck:
1refs/heads/main Bidang kontrol saja (~20 KB: aturan, .gitconfig, konfigurasi sinkronisasi, skrip)
2refs/trunk/main Commit snapshot tanpa induk yang menunjuk ke root tree terpadu terbaru
3refs/trunk/history Rantai commit linear yang terhubung induk dan berbagi SHA-1 root tree yang sama persis
4refs/ws/<actor>/<id> Snapshot workspace terisolasi (changelist) per pengembang atau agen
5refs/ws/cl/<change-id>/<tip> Patchset tidak dapat diubah: setiap tip dari sebuah change yang dikirim untuk review
6refs/heads/land/<id> Pemicu merge-queue sementara yang langsung dihapus setelah landing
1. Bidang Kontrol (refs/heads/main)
Cabang default tidak berisi kode sumber aplikasi. Cabang ini hanya memuat aturan operasional repositori (AGENTS.md, README.md), .gitconfig bersama berisi alias alur kerja, konfigurasi sinkronisasi Starlark (sync.star), serta skrip merge di sisi server. Meng-clone cabang default mentransfer sekitar 20 KB dan selesai dalam hitungan milidetik.
2. Snapshot Trunk Tanpa Induk (refs/trunk/main)
refs/trunk/main menunjuk ke source tree terpadu lengkap dari seluruh organisasi (//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), tetapi objek commit-nya dibuat tanpa flag induk -p.
Karena refs/trunk/main tidak memiliki induk (parents = []), fetch-nya tidak memerlukan negosiasi commit-graph. Server Git tidak menelusuri riwayat; ia hanya mengirim commit head tunggal beserta objek tree direktori saat ini. Binary historis atau file yang dihapus bulan lalu tidak dapat dijangkau dari refs/trunk/main dan tidak pernah ditransfer.
3. Riwayat Audit Linear (refs/trunk/history)
Pengembang tetap memerlukan git blame dan git log ketika menyelidiki regresi. Setiap kali sebuah perubahan di-land, skrip merge membuat dua objek commit yang menunjuk ke hash SHA-1 $NEW_TREE root yang persis sama:
1# Snapshot commit tanpa induk untuk checkout workspace depth-1 secara instan
2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
3
4# Commit berinduk untuk git log dan git blame yang linear
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"
Menyimpan refs/trunk/main dan refs/trunk/history sekaligus tidak menggandakan jejak penyimpanan. Karena kedua commit mereferensikan hash $NEW_TREE yang identik, content-addressable store Git mendeduplikasi 100% objek tree dan blob. Biaya penyimpanan tambahan dari snapshot tanpa induk hanyalah satu header commit berukuran 250 byte.
4. Workspace Terisolasi (refs/ws/<actor>/<id>)
Cabang yang sedang dikerjakan didorong ke refs/ws/<actor>/<id>, bukan refs/heads/*. Seratus agen pengodean dapat menyimpan checkpoint setiap menit tanpa mengotori daftar cabang atau memperlambat git fetch bagi insinyur manusia.
Onboarding dalam Empat Langkah: Cara Kerja Checkout Sparse + Blobless
Dengan tata letak referensi ini, pengembang baru atau container agen pengodean yang bersifat sementara tidak perlu memasang CLI kustom atau driver FUSE. Onboarding membutuhkan empat langkah dengan Git standar dan alias repositori:
1# 1. Clone control plane 20 KB dengan penyaringan blob dan mode sparse diaktifkan
2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
3cd monorepo
4
5# 2. Muat alias repositori dan pasang refs/trunk/*
6git config include.path ../.gitconfig
7git trunk-init
8
9# 3. Deklarasikan direktori domain yang dibutuhkan untuk tugas ini
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Buat workspace terisolasi yang berakar pada snapshot trunk terbaru
13git ws-new alice/optimize-framing
Setiap langkah hanya mengambil apa yang dibutuhkannya:
git clone --filter=blob:none --sparsemengunduh control plane 20 KB padarefs/heads/main.git trunk-initmengonfigurasi+refs/trunk/*:refs/remotes/origin/trunk/*dan menjalankan fetch blobless. Git mengunduh metadatatreeuntukrefs/trunk/main(umumnya beberapa puluh kilobyte) dan nol file blob.git sparse-checkout set net/relay ipc/codecmenginstruksikan index filter mode cone milik Git untuk hanya memunculkan direktori root dan kedua subtree tersebut.git ws-newberpindah ke cabang baru yang berakar padaorigin/trunk/main. Git kemudian meminta secara on-demand hanya objekblobdi dalamnet/relaydanipc/codec. 95% monorepo lainnya tidak pernah diunduh ke disk.
Cakrawala Masa Depan: Memasang refs/trunk/main sebagai Virtual Filesystem (VFS)
Sparse checkout dengan clone blobless mencakup alur kerja harian tanpa perangkat klien tambahan. Karena refs/trunk/main sudah berupa Merkle tree murni di dalam Content-Addressable Store, arsitektur ini juga menyediakan fondasi langsung bagi Virtual Filesystem (VFS) userspace opsional melalui FUSE di Linux, FileProvider di macOS, atau ProjFS di Windows:
- Instant Full-Tree Mount: Daemon mount lokal yang ringan membaca objek
treeroot darirefs/trunk/maindan memproyeksikan seluruh hierarki direktori monorepo ke working directory dalam hitungan milidetik, sehingga tidak perlu menjalankangit sparse-checkout set. - On-Demand Blob Faulting: Isi file tetap virtual hingga compiler, IDE, atau agen mengeluarkan system call
open(2)atauread(2). Daemon kemudian mengambilblobSHA-1 yang bersesuaian dari endpoint CAS dan menyimpannya dalam cache pada penyimpanan NVMe lokal. - Copy-on-Write Overlay for Workspaces: Perubahan lokal ditulis ke lapisan overlay atas yang ringan. Menjalankan
git ws-savehanya men-hash path yang kotor di overlay tersebut, menulis objekblobdantreeyang diperbarui, lalu mendorong commit yang dihasilkan kerefs/ws/<actor>/<id>tanpa memindai direktori yang tidak berubah.
Model Changelist pada Git Murni: Landing Sisi Server di Memori
Dalam alur kerja Git tradisional, menggabungkan cabang fitur menghasilkan merge commit dengan dua induk, yang membuat riwayat menjadi non-linear, atau mengharuskan pengembang melakukan rebase secara manual dan berlomba dengan push konkuren ke main.
Terinspirasi oleh model Changelist (CL) google3, setiap workspace dalam arsitektur kami merepresentasikan satu perubahan atomik terhadap snapshot trunk. Saat bekerja secara lokal di ws/alice/optimize-framing, seorang pengembang atau agen mungkin membuat sepuluh commit inkremental (wip, fix tests, lint). Ketika perubahan siap di-land, server memampatkan delta workspace menjadi satu revisi trunk linear tanpa pernah melakukan checkout working directory pada runner CI.
Berikut cara skrip landing (land.sh) dieksekusi di memori menggunakan plumbing Git 2.40+:
1# 1. Identifikasi commit snapshot root tempat workspace bercabang
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. Hitung root tree hasil merge di memori tanpa checkout working 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 "Merge conflict detected; run git ws-sync to rebase." >&2
15 exit 1
16fi
Pada langkah 2:
- Terlepas dari berapa banyak commit lokal yang dibuat pada cabang workspace, skrip hanya membandingkan tiga hash tree: snapshot dasar asli (
$BASE_TREE), snapshot trunk saat ini ($MAIN_TREE), dan tree workspace akhir ($WORK_TREE). - Jika
refs/trunk/mainmaju di//web/blogsementara Alice mengedit//net/relay,git merge-tree --write-treemenghitung root tree gabungan$NEW_TREElangsung di dalam object store Git dalam hitungan milidetik, tanpa menulis file ke filesystem runner. - Setelah pemeriksaan kebijakan di bawah ini lolos, satu
git push --atomicdengan--force-with-leasememperbaruirefs/trunk/maindan menambahkan satu commit linear kerefs/trunk/history, lalu pemicu landingrefs/heads/land/<id>dihapus.
Menegakkan Batas Arsitektur: Visibilitas Empat Tingkat dan OWNERS Hierarkis
Mengonsolidasikan kode ke dalam satu repositori hanya berhasil jika batas antardomain ditegakkan secara mekanis sebelum sebuah commit dapat di-land. Tepat setelah git merge-tree --write-tree menghasilkan $NEW_TREE (dan sebelum git push --atomic memperbarui trunk), mesin kebijakan kami menjalankan dua pemeriksaan O(delta) atas git diff-tree "$MAIN_TREE" "$NEW_TREE".
1. Visibilitas Domain Empat Tingkat
Root tree dibagi menjadi empat tingkat visibilitas:
| Prefiks Direktori | Tingkat | Impor yang Diizinkan | Impor yang Dilarang | Gerbang OWNERS |
|---|---|---|---|---|
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/* | Public Production | Public Production, //third_party/* | //corp/*, //experimental/* | Diberlakukan |
//third_party/* | External Vendor | Mandiri atau //third_party/* | Semua domain selain third_party | Diberlakukan |
//corp/* | Internal Private | Public Production, //corp/*, //third_party/* | //experimental/* | Diberlakukan |
//experimental/<actor>/* | Sandbox | Paket non-corp mana pun | //corp/* | Dikecualikan (disetujui otomatis) |
Jika paket publik di //web/* mencoba mengimpor paket internal dari //corp/* atau prototipe yang belum matang dari //experimental/*, job landing dibatalkan sebelum menyentuh refs/trunk/main.
2. Verifikasi OWNERS Hierarkis Tanpa Server Gerrit
Verifier menegakkan file OWNERS tingkat direktori dengan aturan pewarisan:
- Untuk setiap path yang diubah antara
$MAIN_TREEdan$NEW_TREE, verifier menelusuri ke atas dari direktori file hingga//OWNERS. - File
OWNERSapa pun dapat mendeklarasikanset noparentuntuk memblokir persetujuan yang diwariskan dari direktori induk, atauper-file *.sql=@dba-teamuntuk membatasi persetujuan berdasarkan glob nama file. - Invarian Anti-Manipulasi: File
OWNERSyang ada selalu dibaca dari$MAIN_TREE(tree trunk saat ini), bukan dari$NEW_TREE. Penulis yang belum disetujui tidak dapat memberikan izin kepada dirinya sendiri dengan mengeditOWNERSdalam commit workspace yang sama. - Pengecualian Sandbox: Setiap path di bawah
//experimental/<actor>/*melewati pemeriksaanOWNERSsehingga insinyur dan agen dapat membuat prototipe dengan bebas menggunakangit ws-exp <actor> <project>, dan kemudian meluluskan paket tersebut ke domain produksi dengangit ws-graduate.
Code Review Tanpa Hambatan: PR Sementara dan Diff Snapshot
Karena refs/heads/main hanya memuat control plane dan refs/trunk/main adalah commit tanpa induk, membuka Pull Request GitHub standar langsung dari cabang workspace ke main akan mencoba membandingkan riwayat yang tidak berhubungan. Kami menyelesaikannya dengan tiga alur kerja review yang disesuaikan untuk jenis perubahan yang berbeda:
- Instant Two-Dot Workspace Diffs (
git ws-save+ Edge Diff Router) Mendorong workspace dengangit ws-savemengeksposrefs/ws/<actor>/<id>melalui edge worker ringan. Worker ini menyelesaikanrefs/trunk/maindan referensi workspace menjadi SHA commit masing-masing, lalu mengalihkan ke tampilan compare two-dot GitHub (/compare/<trunk-sha>..<ws-sha>). Untuk patch kecil atau perubahan yang dihasilkan agen, reviewer dapat memeriksa diff dan mengklik Land langsung di antarmuka web. - Ephemeral GitHub Pull Requests (
git ws-pr) Untuk review baris per baris di GitHub,git ws-prmendorong commit snapshot root ($(git rev-list --max-parents=0 HEAD)) kerefs/heads/base/<id>dan ujung workspace kerefs/heads/review/<id>. Kemudian membuka Pull Request GitHub yang membandingkanreview/<id>denganbase/<id>. Karena cabang dasar adalah snapshot tempat workspace bercabang, tab Files changed di GitHub hanya menampilkan delta workspace. Ketika reviewer mengklik Merge pull request, webhook mempromosikan commit head kerefs/heads/land/<id>, menjalankan pipeline landing di memori, dan secara otomatis menghapusrefs/heads/base/<id>danrefs/heads/review/<id>. - External Mirror Pull Requests Kontributor open-source tidak perlu mempelajari alur kerja monorepo. Mereka melakukan fork dan membuka pull request ke repositori mirror mandiri seperti biasa.
Perubahan, Patchset, dan Pembawaan Persetujuan
Nama cabang tidak dapat mengidentifikasi sebuah perubahan. Seorang agen mungkin mengganti nama cabangnya, melakukan amend pada tip, atau melakukan rebase ke trunk yang lebih baru beberapa kali sebelum landing. Seperti pada Gerrit, alur kerja ini mengidentifikasi perubahan melalui trailer Change-Id: I<40 hex> pada pesan commit tip. git ws-save, git ws-pr, dan git ws-land menambahkan trailer ini jika tip belum memilikinya dan mempertahankannya jika sudah ada, sehingga id tetap bertahan melalui amend, rebase, dan perubahan nama. Commit trunk yang di-land tetap menyimpan trailer tersebut dan mencatat nomor pull request review-nya, sehingga git trunk-cl <number|Change-Id> menemukannya di refs/trunk/history.
Setiap tip yang dikirim untuk review menjadi sebuah patchset. git ws-pr mendorongnya ke referensi tidak dapat diubah refs/ws/cl/<change-id>/<tip> di samping cabang review. Menjalankan git ws-pr lagi setelah amend atau git ws-sync menambahkan patchset ke pull request yang sama. git ws-ps menampilkan daftar patchset dari perubahan saat ini, dan git ws-psdiff <a> <b> mencetak interdiff antara dua patchset, sehingga reviewer hanya membaca apa yang berubah sejak putaran terakhir.
GitHub mengikat persetujuan pada satu commit, sehingga setiap rebase biasanya memerlukan review baru. Pipeline landing juga menerima persetujuan yang diberikan pada patchset lain dari perubahan yang sama, asalkan kedua patchset membuat patch yang sama terhadap snapshot dasar masing-masing. Pipeline membandingkan git patch-id --verbatim atas diff biner lengkap. Rebase bersih ke trunk yang lebih baru dapat di-land tanpa review ulang, sedangkan setiap perubahan konten, termasuk perubahan spasi atau mode file, memerlukan persetujuan baru.
Sinkronisasi Dua Arah O(delta) dan Cabang Produksi Terpisah
Banyak organisasi mempublikasikan paket individual sebagai repositori GitHub mandiri agar pengguna eksternal dapat menjalankan go get, cargo install, atau melakukan fork pada satu proyek tanpa menyentuh monorepo. Beberapa proyek, seperti blog statis atau aplikasi web dengan gerbang rilis, juga memelihara cabang production terpisah di samping main.
Mesin sinkronisasi Starlark kami menangani ekspor keluar dan kontribusi masuk dalam waktu O(delta), yaitu sebanding dengan jumlah file yang berubah dalam sebuah commit, bukan dengan ukuran monorepo:
- Blobless Index Splicing: Saat menyisipkan subtree repositori mirror ke atau dari root tree monorepo, mesin menetapkan
GIT_NO_LAZY_FETCH=1dan menjalankangit update-index --index-infodiikutigit write-tree --missing-ok. Subtree dan blob yang tidak berubah tetap dirujuk melalui SHA-1 dan tidak pernah diunduh ke runner CI. - Delta-Only Blob Prefetching: Sebelum mendorong commit hasil ekspor ke mirror mandiri, skrip menjalankan
git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"dan melakukan prefetch hanya pada blob lama dan baru dari file yang diubah melaluigit cat-file --batch. Dengan demikian Git dapat menghitung thin delta pack dan mendorongnya ke repositori mirror dalam hitungan detik. - Workflow and Release Branch Preservation: Selama ekspor keluar, synchronizer mempertahankan direktori
.github/workflows/milik setiap repositori mandiri sehingga runner CI publik menjalankan pengujian proyek pada mirror. Setiap workflow dalam konfigurasi Starlark menargetkan cabang eksplisit sepertimain, yang mempertahankan gerbang rilis bagi repositori yang melakukan deployment dari cabangproductionterpisah. Landing monorepo disinkronkan dengan cabangmainpada mirror, dan commit metadata atau terjemahan otomatis padamaindisinkronkan kembali kerefs/trunk/mainmelalui webhook. Mempromosikanmainkeproductiontetap merupakan langkah rilis yang eksplisit.
Penutup
Arsitektur ini memperlakukan object store Git sebagai Content-Addressable Store, memisahkan refs/heads/main dari refs/trunk/main dan refs/trunk/history, mengidentifikasi setiap perubahan melalui Change-Id dengan patchset yang tidak dapat diubah, serta menggabungkan tree di memori dengan git merge-tree --write-tree. Biaya onboarding bertumbuh mengikuti direktori yang dibutuhkan suatu tugas, sedangkan biaya landing dan sinkronisasi mirror bertumbuh mengikuti ukuran perubahan, bukan ukuran repositori.