Git Monorepo'yu Yeniden Düşünmek: İnsanlar ve Kodlama Ajanları İçin CAS Temelli Bir Mimari

Büyüyen her mühendislik organizasyonu er ya da geç aynı ayrımla karşılaşır. Onlarca küçük depoyla başlandığında, hayat ilk başta basit görünür. Her kütüphanenin kendi sorun izleyicisi, kendi CI hattı ve siz kahvenizden bir yudum almadan tamamlanan bir git clone işlemi vardır. Ardından vergi gelir. Çekirdek bir ağ paketindeki bir struct'ı değiştirirsiniz ve öğleden sonranın geri kalanını altı aşağı akış deposunda sürüm yükseltme pull request'leri açarak, CI kuyruklarının topolojik sırayla boşalmasını bekleyerek geçirirsiniz.
Her şeyi geleneksel bir Git monorepo'suna taşıdığınızda, koordinasyon acısını fiziksel acıyla takas etmiş olursunuz. Commit geçmişi derinleştikçe ve geçmiş revizyonlarda ikili varlıklar biriktikçe git fetch yavaşlar. Çalışma ağaçları gigabaytlara şişer. Kısa süre sonra biri, günlük geliştirmeyi katlanılabilir tutmak için özel bir sanal dosya sistemi daemon'ı, ayrı bir kod inceleme sunucusu ya da bir grup uzak derleme işçisi kurmayı önerir.
Otonom kodlama ajanları ekibe katıldığında, her iki hata modu da kötüleşir. Polyrepo'lar bir ajanı projeler arası çağrıcılardan habersiz bırakır ve onu kimlik bilgilerini depolar arasında jonglör gibi yönetmeye zorlar. Geleneksel monorepo'lar ise her görev başlatmada ajanın konteyner diskini ve ağ bant genişliğini tüketir; aynı dalda çalışan eşzamanlı ajanlar ise bitmeyen rebase yarışları yaratır.
Üçüncü bir seçenek istedik. google3 mimarisinden ilham alarak basit bir soru sorduk: Yalnızca standart Git altyapısı ve sıradan nesne depolamasını kullanarak tek gövdeli (single-trunk) atomik değişiklikler, saniyenin altında seyrek (sparse) oryantasyon, hiyerarşik OWNERS yönetişimi ve bağımsız açık kaynak aynaları elde edebilir miyiz?
Cevap evet çıktı ve bunun için özel bir dosya sistemi daemon'ına ya da üçüncü taraf bir inceleme cihazına ihtiyaç duyulmadı. Yalnızca Git'i gerçekte altında yatan şey için kullanmak gerekiyordu: Merkle ağacı tabanlı bir İçerik Adreslenebilir Depo (Content-Addressable Store, CAS).
Yavaş Git Monorepo'larının Merkezindeki Yanlış Anlama
Geliştiriciler Git'in büyük monorepo'lara ölçeklenmediğinden yakındığında, nadiren Git'in nesne veritabanında bir sınıra çarpmaktadırlar. .git/objects içinde Git, SHA-1 (veya SHA-256) içerik karmasıyla anahtarlanmış üç değişmez ilkel saklar:
blob: Tek bir dosyanın ham bayt içeriği.tree: Dosya adlarını ve izinleriniblobveya alttreekarmalarına eşleyen bir dizin listesi.commit: Tek bir köktreekarmasına, sıfır veya daha fazla üstcommitkarmasına ve yazar meta verilerine işaret eden küçük bir metin başlığı (genellikle 200 ila 300 bayt).
İçerik adreslenebilir bir depo olarak bu tasarım dikkat çekici ölçüde verimlidir. Bir monorepo 50.000 dosya içeriyorsa ve bir dosyadaki tek bir satırı düzenlerseniz, yeni commit diğer 49.999 dosyanın tam blob ve tree karmalarını yeniden kullanır.
Peki geleneksel bir Git monorepo'su neden ağır hissettirir? Çünkü gelenek, tek bir referansa, refs/heads/main'e, üç çelişkili görev yükler:
- Her projenin tüm birleşik dizin ağacını tutar.
- İlk güne kadar uzanan tüm üst bağlantılı tarihsel commit zincirini sürükler.
- Git sunucularının her
git clonevegit fetchişleminde varsayılan olarak duyurduğu ve üzerinde pazarlık yaptığı ad alanı olanrefs/heads/*içinde yer alır.
Bir geliştirici ya da CI çalıştırıcısı refs/heads/main'i her getirdiğinde, istemci ve sunucu binlerce tarihsel revizyon boyunca commit grafiği erişilebilirliği üzerinde pazarlık yapar. Biri üç yıl önce yanlışlıkla 100 MB'lık bir arşiv commit ettiyse ve ertesi gün sildiyse, onun blob'u hâlâ refs/heads/main'den erişilebilen tarihsel bir tree'ye bağlıdır.
Referans Ad Alanını Dört Katmana Ayırmak
Git'in ağ protokolü, standart bir klonlama sırasında yalnızca refs/heads/* ve refs/tags/* ad alanlarını duyurur. refs/trunk/* veya refs/ws/* gibi özel ad alanları altındaki referanslar, sunucudaki tam aynı nesne deposunda yaşar, aynı packfile delta sıkıştırmasından yararlanır ve açıkça getirilebilir ya da gönderilebilir; yine de varsayılan klonlamalara sıfır ek yük getirmez.
Tek bir depo içindeki dört referans ad alanı arasında sorumlulukları bölerek darboğaz ortadan kalkar:
1refs/heads/main Yalnızca kontrol düzlemi (~20 KB: kurallar, .gitconfig, senkronizasyon yapılandırması, betikler)
2refs/trunk/main En son birleşik kök tree'ye işaret eden üst öğesiz (parentless) anlık görüntü commit'i
3refs/trunk/history Tam olarak kök tree SHA-1 değerini paylaşan doğrusal üst bağlantılı commit zinciri
4refs/ws/<actor>/<id> Geliştirici veya ajan başına izole çalışma alanı anlık görüntüleri (changelist'ler)
5refs/heads/land/<id> Birleştirildiği anda silinen geçici birleştirme kuyruğu tetikleyicisi
1. Kontrol Düzlemi (refs/heads/main)
Varsayılan dal hiçbir uygulama kaynak kodu içermez. Yalnızca depo işletim kurallarını (AGENTS.md, README.md), iş akışı takma adlarından oluşan paylaşılan bir .gitconfig dosyasını, Starlark senkronizasyon yapılandırmasını (sync.star) ve sunucu tarafı birleştirme betiklerini izler. Varsayılan dalı klonlamak yaklaşık 20 KB aktarır ve milisaniyeler içinde tamamlanır.
2. Üst Öğesiz Trunk Anlık Görüntüsü (refs/trunk/main)
refs/trunk/main, tüm kuruluşun tam, birleşik kaynak ağacına (//web/*, //net/*, //ipc/*, //devtools/*, //corp/*, //third_party/*, //experimental/*) işaret eder, ancak commit nesnesi -p üst bayrağı olmadan oluşturulur.
refs/trunk/main sıfır üst öğeye sahip olduğundan (parents = []), onu getirmek sıfır commit grafiği pazarlığı gerektirir. Git sunucusu geçmişi gezmez; tek baş commit'i ve geçerli dizin tree nesnelerini gönderir. Geçen ayki tarihsel ikili dosyalar ya da silinmiş dosyalar refs/trunk/main'den erişilemez ve asla aktarılmaz.
3. Doğrusal Denetim Geçmişi (refs/trunk/history)
Geliştiricilerin regresyonları araştırırken yine de git blame ve git log komutlarına ihtiyacı vardır. Bir değişiklik her indirildiğinde, birleştirme betiği tam olarak aynı kök $NEW_TREE SHA-1 karmasına işaret eden iki commit nesnesi oluşturur:
1# Anlık depth-1 çalışma alanı checkout'ları için üst öğesiz anlık görüntü commit'i
2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
3
4# Doğrusal git log ve git blame için üst bağlantılı commit
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"
Hem refs/trunk/main hem de refs/trunk/history tutmak, deponun depolama ayak izini iki katına çıkarır mı? Kesinlikle hayır. Her iki commit de aynı $NEW_TREE karmasına başvurduğundan, Git'in içerik adreslenebilir deposu tree ve blob nesnelerinin %100'ünü tekilleştirir. Üst öğesiz anlık görüntünün ek depolama maliyeti tek bir 250 baytlık commit başlığıdır.
4. İzole Çalışma Alanları (refs/ws/<actor>/<id>)
Devam eden çalışma dalları refs/heads/* yerine refs/ws/<actor>/<id> konumuna gönderilir. Yüzlerce kodlama ajanı her dakika kontrol noktaları kaydedebilir; bu, dal açılır listelerini kirletmez ve insan mühendisler için git fetch işlemini yavaşlatmaz.
Beş Komutta Oryantasyon: Seyrek + Blobsuz Checkout Nasıl Çalışır
Bu referans düzeniyle yeni bir geliştirici ya da geçici bir kodlama ajanı konteyneri özel bir CLI veya FUSE sürücüsü kurmaz. Oryantasyon beş standart Git komutu alır:
1# 1. Blob filtreleme ve seyrek mod etkinleştirilmiş 20 KB kontrol düzlemini klonla
2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
3cd monorepo
4
5# 2. Depo takma adlarını yükle ve refs/trunk/*'i ekle
6git config include.path ../.gitconfig
7git trunk-init
8
9# 3. Bu görev için gereken alan dizinlerini bildir
10git sparse-checkout set net/relay ipc/codec
11
12# 4. En son trunk anlık görüntüsüne köklenmiş izole bir çalışma alanı oluştur
13git ws-new alice/optimize-framing
Her adım yalnızca gereken minimum işi yapar:
git clone --filter=blob:none --sparsekomuturefs/heads/mainüzerindeki 20 KB kontrol düzlemini indirir.git trunk-initkomutu+refs/trunk/*:refs/remotes/origin/trunk/*ayarını yapar ve blobsuz bir fetch çalıştırır. Git,refs/trunk/mainiçintreemeta verilerini (genellikle birkaç düzine kilobayt) indirir ve sıfır dosya blob'u indirmez.git sparse-checkout set net/relay ipc/codeckomutu, Git'in cone modundaki index filtresine yalnızca kök dizini ve bu iki alt ağacı gerçekleştirmesini söyler.git ws-newkomutu,origin/trunk/main'e köklenmiş yeni bir dala geçer. Tam bu anda Git, yalnızcanet/relayveipc/codeciçindekiblobnesnelerini talep üzerine ister. Monorepo'nun geri kalan %95'i asla diske indirilmez.
Gelecek Ufku: refs/trunk/main'i Sanal Dosya Sistemi (VFS) Olarak Bağlamak
Blobsuz klonlarla seyrek checkout, ek istemci araçları olmadan günlük iş akışlarını karşılar. refs/trunk/main, bir İçerik Adreslenebilir Depoda zaten saf bir Merkle ağacı olduğundan, bu mimari Linux'ta FUSE, macOS'ta FileProvider veya Windows'ta ProjFS aracılığıyla isteğe bağlı bir kullanıcı alanı Sanal Dosya Sistemi (VFS) için doğrudan bir temel de sağlar:
- Anlık Tam Ağaç Bağlama: Hafif bir yerel bağlama daemon'ı
refs/trunk/main'in köktreenesnesini okur ve tüm monorepo dizin hiyerarşisini milisaniyeler içinde çalışma dizinine yansıtır; böylecegit sparse-checkout setçalıştırma ihtiyacı ortadan kalkar. - İsteğe Bağlı Blob Yüklemesi: Bir derleyici, IDE veya ajan bir
open(2)ya daread(2)sistem çağrısı yapana kadar dosya içerikleri sanal kalır. Daemon bunun üzerine ilgili SHA-1blob'unu CAS uç noktasından getirir ve yerel NVMe depolamada önbelleğe alır. - Çalışma Alanları İçin Kopyala-Yaz Üst Katman: Yerel düzenlemeler hafif bir üst katmana yazılır.
git ws-saveçalıştırmak yalnızca bu katmandaki kirli yolların karmasını alır, güncellenmişblobvetreenesnelerini yazar ve ortaya çıkan commit'i değiştirilmemiş dizinleri taramadanrefs/ws/<actor>/<id>'ye gönderir.
Saf Git Üzerinde Changelist Modeli: Bellek İçi Sunucu Taraflı Birleştirme
Geleneksel bir Git iş akışında, bir özellik dalını birleştirmek ya iki üst öğeli bir birleştirme commit'i oluşturur (geçmişi karmaşık bir yük istasyonuna çevirir) ya da geliştiricilerin manuel olarak rebase yapmasını ve main'e gönderim yapan diğer mühendislerle yarışmasını gerektirir.
google3 Changelist (CL) modelinden ilham alan mimarimizdeki her çalışma alanı, trunk anlık görüntüsüne karşı tek bir atomik değişikliği temsil eder. ws/alice/optimize-framing içinde yerel olarak çalışırken bir geliştirici ya da ajan on artımlı commit yapabilir (wip, fix tests, lint). Değişiklik birleştirilmeye hazır olduğunda, sunucu CI çalıştırıcısında asla bir çalışma dizini checkout etmeden çalışma alanı farkını tek bir doğrusal trunk revizyonuna çöker.
Land betiği (land.sh), Git 2.40+ tesisatını kullanarak bellek içinde şu şekilde çalışır:
1# 1. Çalışma alanı dalının ayrıldığı kök anlık görüntü commit'ini belirle
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. Çalışma ağacı checkout'u olmadan, birleştirilmiş kök ağacı bellekte hesapla
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
- adımı dikkatle inceleyin:
- Çalışma alanı dalında kaç yerel commit yapılmış olursa olsun, betik yalnızca üç tree karmasını karşılaştırır: özgün temel anlık görüntü (
$BASE_TREE), mevcut trunk anlık görüntüsü ($MAIN_TREE) ve nihai çalışma alanı ağacı ($WORK_TREE). - Alice
//net/relayüzerinde çalışırkenrefs/trunk/main//web/blogiçinde ilerlediyse,git merge-tree --write-treebirleşik kök ağaç$NEW_TREE'yi doğrudan Git nesne deposunun içinde milisaniyeler içinde hesaplar. Çalıştırıcının dosya sistemine hiçbir dosya yazılmaz. - Son olarak,
--force-with-leaseilegit push --atomictek bir işlemderefs/trunk/main'i günceller,refs/trunk/history'e bir doğrusal commit ekler verefs/heads/land/<id>iniş tetikleyicisini siler.
Mimari Sınırları Zorunlu Kılmak: Dört Katmanlı Görünürlük ve Hiyerarşik OWNERS
Kodu tek bir depoda birleştirmek, alanlar arasındaki sınırlar bir commit inmeden önce mekanik olarak zorunlu kılındığında işe yarar. git merge-tree --write-tree $NEW_TREE'yi ürettikten hemen sonra ve git push --atomic trunk'ı güncellemeden önce, politika motorumuz git diff-tree "$MAIN_TREE" "$NEW_TREE" üzerinde iki O(delta) kontrol çalıştırır.
1. Dört Katmanlı Alan Görünürlüğü
Kök ağaç dört görünürlük katmanına bölünmüştür:
| Dizin Öneki | Katman | İzin Verilen İçe Aktarımlar | Yasaklanan İçe Aktarımlar | OWNERS Kapısı |
|---|---|---|---|---|
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/* | Halka Açık Üretim | Halka Açık Üretim, //third_party/* | //corp/*, //experimental/* | Zorunlu |
//third_party/* | Harici Satıcı | Kendi içinde bağımsız veya //third_party/* | Tüm third_party dışı alanlar | Zorunlu |
//corp/* | Dahili Özel | Halka Açık Üretim, //corp/*, //third_party/* | //experimental/* | Zorunlu |
//experimental/<actor>/* | Sandbox | corp dışındaki herhangi bir paket | //corp/* | Muaf (otomatik onaylı) |
//web/* içindeki herkese açık bir paket //corp/* içindeki dahili bir paketi ya da //experimental/* içindeki olgunlaşmamış bir prototipi içe aktarmaya çalışırsa, iniş işi refs/trunk/main'e dokunmadan iptal edilir.
2. Gerrit Sunucusu Olmadan Hiyerarşik OWNERS Doğrulaması
Miras kurallarıyla dizin düzeyinde OWNERS dosyalarını zorunlu kılmak için özel bir Gerrit örneğine ihtiyacınız yoktur:
$MAIN_TREEile$NEW_TREEarasında değiştirilen her yol için doğrulayıcı, dosyanın dizininden//OWNERS'a kadar yukarı doğru yürür.- Herhangi bir
OWNERSdosyası, üst dizinlerden gelen onayları engellemek içinset noparentbildirebilir ya da onayları dosya adı glob'una göre kapsamak içinper-file *.sql=@dba-teamkullanabilir. - Karşı Kurcalama Değişmezi: Mevcut
OWNERSdosyaları her zaman$MAIN_TREE'den (mevcut trunk ağacı) okunur, asla$NEW_TREE'den okunmaz. Onaysız bir yazar, aynı çalışma alanı commit'indeOWNERSdosyasını düzenleyerek kendine izin veremez. - Sandbox Muafiyeti:
//experimental/<actor>/*altındaki herhangi bir yol, mühendislerin ve ajanlarıngit ws-exp <actor> <project>ile serbestçe prototip oluşturabilmesi ve daha sonragit ws-graduateile paketi bir üretim alanına taşıyabilmesi içinOWNERSkontrollerini atlar.
Sürtünmesiz Kod İncelemesi: Geçici PR'lar ve Anlık Görüntü Farkları
refs/heads/main yalnızca kontrol düzlemini tuttuğu ve refs/trunk/main üst öğesiz bir commit olduğu için, bir çalışma alanı dalından main'e doğrudan standart bir GitHub Pull Request açmak ilgisiz geçmişleri karşılaştırmaya çalışırdı. Bunu farklı değişikliklere göre uyarlanmış üç inceleme iş akışıyla çözdük:
- Anlık İki Nokta Çalışma Alanı Farkları (
git ws-save+ Kenar Fark Yönlendiricisi)git ws-saveile bir çalışma alanını göndermek,refs/ws/<actor>/<id>'yi hafif bir kenar worker'ı aracılığıyla açığa çıkarır. Bu worker,refs/trunk/mainve çalışma alanı referansını commit SHA'larına çözümler ve GitHub'ın iki nokta karşılaştırma görünümüne (/compare/<trunk-sha>..<ws-sha>) yönlendirir. Küçük yamalar ya da ajan tarafından üretilen değişiklikler için incelemeciler farkı inceleyebilir ve doğrudan web arayüzünde Land düğmesine tıklayabilir. - Geçici GitHub Pull Request'leri (
git ws-pr) GitHub üzerinde satır satır inceleme içingit ws-pr, kök anlık görüntü commit'ini ($(git rev-list --max-parents=0 HEAD))refs/heads/base/<id>'ye ve çalışma alanı ucunurefs/heads/review/<id>'ye gönderir. Ardındanreview/<id>'yibase/<id>ile karşılaştıran bir GitHub Pull Request açar. Temel dal, çalışma alanının ayrıldığı tam anlık görüntü olduğundan, GitHub'ın Files changed sekmesi yalnızca çalışma alanında değiştirilen tam satırları gösterir. İnceleyici Merge pull request düğmesine tıkladığında, bir webhook head commit'irefs/heads/land/<id>'ye yükseltir, bellek içi iniş hattını çalıştırır ve hemrefs/heads/base/<id>hem derefs/heads/review/<id>'yi otomatik olarak siler. - Harici Ayna Pull Request'leri Açık kaynak katkıda bulunanların monorepo iş akışını öğrenmesi hiç gerekmez. Her zamanki gibi çatallar oluşturur ve bağımsız ayna depolara karşı pull request açarlar.
O(delta) Çift Yönlü Senkronizasyon ve Ayrı Üretim Dalları
Birçok kuruluş, dış kullanıcıların monorepo'ya dokunmadan go get, cargo install yapabilmesi ya da tek bir projeyi çatallayabilmesi için bireysel paketleri bağımsız GitHub depoları olarak yayınlar. Statik bloglar ya da sürüm kapılı web uygulamaları gibi bazı projeler de main ile birlikte ayrı bir production dalı tutar.
Starlark senkronizasyon motorumuz hem giden dışa aktarımları hem de gelen katkıları, monorepo'nun boyutuyla değil bir commit'te değişen dosya sayısıyla orantılı olan O(delta) zamanında yönetir:
- Blobsuz Dizin Ekleme: Bir ayna deponun alt ağacını monorepo kök ağacına eklerken ya da çıkarırken motor
GIT_NO_LAZY_FETCH=1ayarlar vegit update-index --index-infoardındangit write-tree --missing-okçalıştırır. Değiştirilmemiş alt ağaçlar ve blob'lar CI çalıştırıcısına asla indirilmeden yalnızca SHA-1 karmalarıyla referanslanır. - Yalnızca Delta Blob Önceden Getirme: Dışa aktarılan bir commit'i bağımsız bir ayna depoya göndermeden önce betik
git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"çalıştırır ve yalnızca değiştirilen dosyaların eski ve yeni blob'larınıgit cat-file --batcharacılığıyla önceden getirir. Git böylece ince bir delta paketi hesaplayabilir ve ayna depoya saniyeler içinde gönderebilir. - İş Akışı ve Sürüm Dalı Koruması: Dışa aktarım sırasında senkronizatör, her bağımsız deponun kendi
.github/workflows/dizinini korur; böylece herkese açık CI çalıştırıcıları proje testlerini ayna üzerinde çalıştırır. Starlark yapılandırmasındaki her iş akışımaingibi açık bir dalı hedefler; bu da ayrı birproductiondalından dağıtım yapan depolar için sürüm kapılarını korur. Monorepo inişleri ayna deponunmaindalıyla senkronize olur vemainüzerindeki otomatik meta veri ya da çeviri commit'leri webhook aracılığıylarefs/trunk/main'e geri senkronize olur.main'inproduction'a yükseltilmesi açık bir sürüm adımı olarak kalır.
Kapanış Düşünceleri
Yüksek hızlı bir monorepo oluşturmak, standart Git'i bırakmayı ya da karmaşık bir özel altyapı sunucusu filosunu işletmeyi gerektirmez. Bu mimari, Git'in nesne deposunu bir İçerik Adreslenebilir Depo olarak ele alır, refs/heads/main'i refs/trunk/main ve refs/trunk/history'den ayırır ve ağaçları git merge-tree --write-tree ile bellekte birleştirir. Sonuç olarak, hem insan mühendisler hem de otonom kodlama ajanları bir çalışma alanını bir saniyenin altında klonlayabilir ve yüzlerce proje genelinde temiz biçimde ölçeklenebilir.