Tim kecil di chapter sebelumnya punya satu repository untuk satu produk. Tumbuh sedikit lagi dan bentuknya berubah: satu tim kini memegang situs web, layanan API, dan pustaka utilitas bersama — tiga proyek yang saling menyentuh. Ada dua jawaban struktural: pecah menjadi tiga repository (polyrepo), atau satukan dalam satu repository (monorepo). Monorepo menang saat refactor harus berjalan serentak — mengganti nama fungsi di pustaka beserta seluruh pemakainya dalam satu commit — dan menagih harganya lewat disiplin baru: karena semua tinggal serumah, setiap commit harus menyatakan wilayahnya, dan riwayat yang tadinya alami per repo kini harus bisa dibaca per aplikasi.

Struktur yang Menyatakan Wilayah #

Disiplin monorepo dimulai dari direktori. Struktur berikut adalah repository platform yang kita bangun di sesi ini: tiga wilayah kerja di bawah apps/ dan packages/, satu berkas pemilik di akar:

DirektoriIsinyaPemiliknya
apps/web/aplikasi situs (HTML, JS)Budi
apps/api/layanan backend (Python)Siti
packages/util/pustaka bersamakeduanya

Struktur ini bukan sekadar kerapian — nama direktori menjadi alamat untuk semua disiplin berikutnya: scope commit, filter riwayat, lingkup CI, sampai pemilik review. Format apps/ + packages/ adalah konvensi yang lazim, bukan ketentuan Git; yang penting konsisten sejak commit pertama.

Commit Scoped dan Riwayat per Path #

Dengan banyak proyek satu rumah, pesan commit wajib menyebut wilayahnya lewat scope — pola type(scope): deskripsi dari Conventional Commits . Sesi ini menumbuhkan kedua aplikasi beriringan:

bash
mkdir platform && cd platform
git init -b main
git config user.name "Budi Santoso"
git config user.email "[email protected]"
mkdir -p apps/web apps/api packages/util
printf 'beranda web\n' > apps/web/index.html
printf 'print("api")\n' > apps/api/main.py
printf 'util bersama\n' > packages/util/helpers.txt
git add .
git commit -m "chore: scaffold monorepo"

printf '/apps/web/    @budi\n/apps/api/    @siti\n/packages/    @budi @siti\n' > CODEOWNERS
git add CODEOWNERS
git commit -m "chore: tetapkan pemilik berkas"

printf 'function cari() {}\n' > apps/web/cari.js
git add apps/web/cari.js
git commit -m "feat(web): tambah komponen pencarian"
printf 'def cari():\n    pass\n' > apps/api/cari.py
git add apps/api/cari.py
git commit -m "feat(api): tambah endpoint pencarian"
printf 'mode gelap\n' > apps/web/tema.css
git add apps/web/tema.css
git commit -m "feat(web): tambah mode gelap"

Output yang Dihasilkan (tiga commit terakhir):

text
[main 6393c24] feat(web): tambah komponen pencarian
 1 file changed, 1 insertion(+)
 create mode 100644 apps/web/cari.js
text
[main 8f2cbf2] feat(api): tambah endpoint pencarian
 1 file changed, 2 insertions(+)
 create mode 100644 apps/api/cari.py
text
[main 4f2d80d] feat(web): tambah mode gelap
 1 file changed, 1 insertion(+)
 create mode 100644 apps/web/tema.css

Riwayat penuhnya bercampur — dan di sinilah path menjadi penyaring. Bandingkan tiga log berikut:

bash
git log --oneline
git log --oneline -- apps/web
git log --oneline -- apps/api

Output yang Dihasilkan (tiga perintah):

text
4f2d80d feat(web): tambah mode gelap
8f2cbf2 feat(api): tambah endpoint pencarian
6393c24 feat(web): tambah komponen pencarian
9fc4d9f chore: tetapkan pemilik berkas
bec0f49 chore: scaffold monorepo
text
4f2d80d feat(web): tambah mode gelap
6393c24 feat(web): tambah komponen pencarian
bec0f49 chore: scaffold monorepo
text
8f2cbf2 feat(api): tambah endpoint pencarian
bec0f49 chore: scaffold monorepo

Hash di mesin pembaca berbeda, bentuknya tidak: log menyaring riwayat — apps/web hanya melihat dua commit fiturnya plus scaffold, apps/api hanya melihat satu. Dengan satu argumen path, git log menjawab “apa yang terjadi pada aplikasi ini” tanpa membaca sisanya — pola yang sama berlaku untuk git diff dan git blame. Inilah alasan scope wajib di pesan commit: tanpa feat(web):, log terfilter tetap terbaca, tetapi daftar commit penuh menjadi teka-teki.

CODEOWNERS: Pemilik per Path #

Berkas CODEOWNERS di akar repository memetakan pola path ke pemiliknya — isinya yang kita commit di sesi di atas:

ini
/apps/web/    @budi
/apps/api/    @siti
/packages/    @budi @siti

Setiap baris berpasangan: pola direktori, lalu pemilik. Platform hosting membacanya dan menuntut persetujuan pemilik setiap pull request yang menyentuh pola itu — PR yang mengubah apps/api/ otomatis menunggu Siti, siapa pun pembuatnya. Mekanisme penegakannya dijelaskan di Protected Branches , dan perannya dalam mencegah tabrakan antar kontributor dibahas di Menyelesaikan Konflik Kolaborasi . Format berkasnya didokumentasikan di dokumentasi code owners GitHub .

Sparse Checkout: CI per Aplikasi #

Pipeline CI untuk apps/web tidak perlu mengerjakan apps/api. Clone penuh tetap memuat seluruh riwayat, tetapi working directory bisa dipangkas ke wilayah yang relevan dengan sparse checkout mode cone:

bash
cd ..
git clone platform ci-web
cd ci-web
git sparse-checkout init --cone
git sparse-checkout set apps/web
ls apps
ls

Output yang Dihasilkan (dua perintah terakhir):

text
web
text
CODEOWNERS
apps

Bukti di layar: apps/ hanya berisi web, sementara riwayat Git tetap utuh — git log --oneline -- apps/api tetap bekerja di clone ini. Clone CI menjadi lebih ramping dan pipeline tidak tersesat membaca berkas aplikasi lain; mode cone dan batasannya dibedah lengkap di Sparse Checkout , rincian perintahnya di dokumentasi git sparse-checkout .

Tag per Aplikasi atau Tag Global #

Rilis monorepo menuntut satu keputusan penamaan. Tag v1.0.0 tunggal menyatakan “seluruh platform versi 1.0.0” — sederhana, tetapi aplikasi kecil tidak bisa rilis sendiri. Alternatifnya menomori per aplikasi:

PendekatanBentukCocok saatHarganya
tag globalv1.3.0 untuk seluruh repoaplikasi dirilis bersama, satu nomor produkaplikasi tidak bisa rilis sendiri
tag prefiks aplikasiweb-v1.0.0, api-v2.1.0aplikasi dirilis independenkonvensi prefiks harus dijaga disiplin
campuranglobal untuk rilis platform, prefiks untuk darurat per appkombinasi keduanyadua konvensi hidup bersamaan

Pilihan mana pun tetap memakai alat yang sama — tag annotated di titik rilis, pola Workflow Release — yang berbeda hanya konvensi namanya.

Monorepo atau Polyrepo #

Keputusan struktural terakhir ditaruh di meja penuh:

AspekMonorepoPolyrepo
Refactor lintas aplikasisatu commit atomicPR lintas repo, sinkron manual versi
Riwayat per aplikasidifilter lewat pathalami per repository
Ukuran clonetumbuh besar; sparse checkout meredakankecil per repository
CIharus di-scope per pathalami per repository
Hak akses per timdibatasi lewat CODEOWNERSalami per repository
Keterlihatan lintas proyektinggi — semua orang melihat semuarendah — batas repo membatasi

Aturan pragmatisnya: refactor serentak yang sering dan tim yang kecil condong ke monorepo; tim yang benar-benar terpisah batasnya, atau kebijakan akses yang berbeda total, condong ke polyrepo.

Monorepo ini papan catur penuh kode. Bentuk repository yang isinya justru bukan kode sama sekali — dokumen, panduan, catatan — punya karakter workflow berbeda: perubahannya murah, pembacanya banyak, dan penilainya bukan reviewer kode melainkan editor. Workflow documentasi, chapter berikutnya, memainkan pola untuk repository yang hidup dari kata-kata.