GoSuda

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

By Lemon Mint views ...

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:

  1. blob: Tek bir dosyanın ham bayt içeriği.
  2. tree: Dosya adlarını ve izinlerini blob veya alt tree karmalarına eşleyen bir dizin listesi.
  3. commit: Tek bir kök tree karmasına, sıfır veya daha fazla üst commit karması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 clone ve git fetch işleminde varsayılan olarak duyurduğu ve üzerinde pazarlık yaptığı ad alanı olan refs/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:

  1. git clone --filter=blob:none --sparse komutu refs/heads/main üzerindeki 20 KB kontrol düzlemini indirir.
  2. git trunk-init komutu +refs/trunk/*:refs/remotes/origin/trunk/* ayarını yapar ve blobsuz bir fetch çalıştırır. Git, refs/trunk/main için tree meta verilerini (genellikle birkaç düzine kilobayt) indirir ve sıfır dosya blob'u indirmez.
  3. git sparse-checkout set net/relay ipc/codec komutu, Git'in cone modundaki index filtresine yalnızca kök dizini ve bu iki alt ağacı gerçekleştirmesini söyler.
  4. git ws-new komutu, origin/trunk/main'e köklenmiş yeni bir dala geçer. Tam bu anda Git, yalnızca net/relay ve ipc/codec içindeki blob nesnelerini 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:

  1. Anlık Tam Ağaç Bağlama: Hafif bir yerel bağlama daemon'ı refs/trunk/main'in kök tree nesnesini okur ve tüm monorepo dizin hiyerarşisini milisaniyeler içinde çalışma dizinine yansıtır; böylece git sparse-checkout set çalıştırma ihtiyacı ortadan kalkar.
  2. İsteğe Bağlı Blob Yüklemesi: Bir derleyici, IDE veya ajan bir open(2) ya da read(2) sistem çağrısı yapana kadar dosya içerikleri sanal kalır. Daemon bunun üzerine ilgili SHA-1 blob'unu CAS uç noktasından getirir ve yerel NVMe depolamada önbelleğe alır.
  3. Ç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ş blob ve tree nesnelerini yazar ve ortaya çıkan commit'i değiştirilmemiş dizinleri taramadan refs/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
  1. 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ışırken refs/trunk/main //web/blog içinde ilerlediyse, git merge-tree --write-tree birleş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-lease ile git push --atomic tek bir işlemde refs/trunk/main'i günceller, refs/trunk/history'e bir doğrusal commit ekler ve refs/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 ÖnekiKatmanİzin Verilen İçe AktarımlarYasaklanan İçe AktarımlarOWNERS Kapısı
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*Halka Açık ÜretimHalka 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ışı alanlarZorunlu
//corp/*Dahili ÖzelHalka Açık Üretim, //corp/*, //third_party/*//experimental/*Zorunlu
//experimental/<actor>/*Sandboxcorp 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_TREE ile $NEW_TREE arası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 OWNERS dosyası, üst dizinlerden gelen onayları engellemek için set noparent bildirebilir ya da onayları dosya adı glob'una göre kapsamak için per-file *.sql=@dba-team kullanabilir.
  • Karşı Kurcalama Değişmezi: Mevcut OWNERS dosyaları her zaman $MAIN_TREE'den (mevcut trunk ağacı) okunur, asla $NEW_TREE'den okunmaz. Onaysız bir yazar, aynı çalışma alanı commit'inde OWNERS dosyasını düzenleyerek kendine izin veremez.
  • Sandbox Muafiyeti: //experimental/<actor>/* altındaki herhangi bir yol, mühendislerin ve ajanların git ws-exp <actor> <project> ile serbestçe prototip oluşturabilmesi ve daha sonra git ws-graduate ile paketi bir üretim alanına taşıyabilmesi için OWNERS kontrollerini 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:

  1. Anlık İki Nokta Çalışma Alanı Farkları (git ws-save + Kenar Fark Yönlendiricisi) git ws-save ile 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/main ve ç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.
  2. Geçici GitHub Pull Request'leri (git ws-pr) GitHub üzerinde satır satır inceleme için git 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ı ucunu refs/heads/review/<id>'ye gönderir. Ardından review/<id>'yi base/<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'i refs/heads/land/<id>'ye yükseltir, bellek içi iniş hattını çalıştırır ve hem refs/heads/base/<id> hem de refs/heads/review/<id>'yi otomatik olarak siler.
  3. 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=1 ayarlar ve git update-index --index-info ardından git 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 --batch aracı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ışı main gibi açık bir dalı hedefler; bu da ayrı bir production dalından dağıtım yapan depolar için sürüm kapılarını korur. Monorepo inişleri ayna deponun main dalıyla senkronize olur ve main üzerindeki otomatik meta veri ya da çeviri commit'leri webhook aracılığıyla refs/trunk/main'e geri senkronize olur. main'in production'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.