GoSuda

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

By Lemon Mint views ...

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):

  1. blob: Isi byte mentah dari sebuah file tunggal.
  2. tree: Daftar direktori yang memetakan nama file dan izin ke hash blob atau tree anak.
  3. commit: Header teks kecil (umumnya 200 hingga 300 byte) yang menunjuk ke satu hash tree root, nol atau lebih hash commit induk, 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 setiap git clone dan git 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:

  1. git clone --filter=blob:none --sparse mengunduh control plane 20 KB pada refs/heads/main.
  2. git trunk-init mengonfigurasi +refs/trunk/*:refs/remotes/origin/trunk/* dan menjalankan fetch blobless. Git mengunduh metadata tree untuk refs/trunk/main (umumnya beberapa puluh kilobyte) dan nol file blob.
  3. git sparse-checkout set net/relay ipc/codec menginstruksikan index filter mode cone milik Git untuk hanya memunculkan direktori root dan kedua subtree tersebut.
  4. git ws-new berpindah ke cabang baru yang berakar pada origin/trunk/main. Git kemudian meminta secara on-demand hanya objek blob di dalam net/relay dan ipc/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:

  1. Instant Full-Tree Mount: Daemon mount lokal yang ringan membaca objek tree root dari refs/trunk/main dan memproyeksikan seluruh hierarki direktori monorepo ke working directory dalam hitungan milidetik, sehingga tidak perlu menjalankan git sparse-checkout set.
  2. On-Demand Blob Faulting: Isi file tetap virtual hingga compiler, IDE, atau agen mengeluarkan system call open(2) atau read(2). Daemon kemudian mengambil blob SHA-1 yang bersesuaian dari endpoint CAS dan menyimpannya dalam cache pada penyimpanan NVMe lokal.
  3. Copy-on-Write Overlay for Workspaces: Perubahan lokal ditulis ke lapisan overlay atas yang ringan. Menjalankan git ws-save hanya men-hash path yang kotor di overlay tersebut, menulis objek blob dan tree yang diperbarui, lalu mendorong commit yang dihasilkan ke refs/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/main maju di //web/blog sementara Alice mengedit //net/relay, git merge-tree --write-tree menghitung root tree gabungan $NEW_TREE langsung di dalam object store Git dalam hitungan milidetik, tanpa menulis file ke filesystem runner.
  • Setelah pemeriksaan kebijakan di bawah ini lolos, satu git push --atomic dengan --force-with-lease memperbarui refs/trunk/main dan menambahkan satu commit linear ke refs/trunk/history, lalu pemicu landing refs/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 DirektoriTingkatImpor yang DiizinkanImpor yang DilarangGerbang OWNERS
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*Public ProductionPublic Production, //third_party/*//corp/*, //experimental/*Diberlakukan
//third_party/*External VendorMandiri atau //third_party/*Semua domain selain third_partyDiberlakukan
//corp/*Internal PrivatePublic Production, //corp/*, //third_party/*//experimental/*Diberlakukan
//experimental/<actor>/*SandboxPaket 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_TREE dan $NEW_TREE, verifier menelusuri ke atas dari direktori file hingga //OWNERS.
  • File OWNERS apa pun dapat mendeklarasikan set noparent untuk memblokir persetujuan yang diwariskan dari direktori induk, atau per-file *.sql=@dba-team untuk membatasi persetujuan berdasarkan glob nama file.
  • Invarian Anti-Manipulasi: File OWNERS yang 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 mengedit OWNERS dalam commit workspace yang sama.
  • Pengecualian Sandbox: Setiap path di bawah //experimental/<actor>/* melewati pemeriksaan OWNERS sehingga insinyur dan agen dapat membuat prototipe dengan bebas menggunakan git ws-exp <actor> <project>, dan kemudian meluluskan paket tersebut ke domain produksi dengan git 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:

  1. Instant Two-Dot Workspace Diffs (git ws-save + Edge Diff Router) Mendorong workspace dengan git ws-save mengekspos refs/ws/<actor>/<id> melalui edge worker ringan. Worker ini menyelesaikan refs/trunk/main dan 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.
  2. Ephemeral GitHub Pull Requests (git ws-pr) Untuk review baris per baris di GitHub, git ws-pr mendorong commit snapshot root ($(git rev-list --max-parents=0 HEAD)) ke refs/heads/base/<id> dan ujung workspace ke refs/heads/review/<id>. Kemudian membuka Pull Request GitHub yang membandingkan review/<id> dengan base/<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 ke refs/heads/land/<id>, menjalankan pipeline landing di memori, dan secara otomatis menghapus refs/heads/base/<id> dan refs/heads/review/<id>.
  3. 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=1 dan menjalankan git update-index --index-info diikuti git 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 melalui git 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 seperti main, yang mempertahankan gerbang rilis bagi repositori yang melakukan deployment dari cabang production terpisah. Landing monorepo disinkronkan dengan cabang main pada mirror, dan commit metadata atau terjemahan otomatis pada main disinkronkan kembali ke refs/trunk/main melalui webhook. Mempromosikan main ke production tetap 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.