GoSuda

Prehodnotenie Git monorepa: CAS-natívna architektúra pre ľudí a kódovacích agentov

By Lemon Mint views ...

Každá rastúca inžinierska organizácia sa nakoniec dostane k rovnakej križovatke. Na začiatku máte desiatky malých repozitárov a život ppôsobí najprv jednoducho. Každá knižnica má vlastný issue tracker, vlastný CI pipeline a príkaz git clone, ktorý skončí skôr, než si stihnete odpiť kávu. Potom prichádza daň. Zmeníte štruktúru (struct) v jadrovom sieťovom balíku a zvyšok popoludnia strávite otváraním pull requestov na zvýšenie verzie v šiestich nadväzujúcich repozitároch a čakaním, kým sa CI fronty vyprázdnia v topologickom poradí.

Ak všetko presuniete do tradičného Git monorepozitára, vymeníte koordinačné problémy za problémy s fyzikálnymi obmedzeniami. Ako história commitov narastá a binárne aktíva sa hromadia v minulých revíziách, git fetch spomaľuje. Pracovné stromy narastajú na gigabajty. Čoskoro niekto navrhne zriadiť vlastný démon virtuálneho súborového systému, dedikovaný server na code review alebo flotilu vzdialených build workerov, len aby bol každodenný vývoj znesiteľný.

Keď sa do tímu pridajú autonómni agenti na písanie kódu, obidva režimy zlyhania sa zhoršia. Polyrepozitáre zbavujú agenta prehľadu o volajúcich naprieč projektami a nútia ho žonglovať s poverovacími údajmi medzi repozitármi. Tradičné monorepozitáre zahltia kontajnerový disk agenta a sieťovú šírku pásma pri každej inicializácii úlohy, zatiaľ čo súbežní agenti pracujúci na tej istej vetve vytvárajú nekonečné závody pri rebase.

Chceli sme tretiu možnosť. Inšpirovaní architektúrou google3 sme si položili jednoduchú otázku: Dokážeme získať atomické zmeny v jedinom trunku, riedke (sparse) onboardovanie v rámci sekúnd, hierarchickú správu OWNERS a samostatné open-source zrkadlá, pričom použijeme len štandardnú Git infraštruktúru a bežné objektové úložisko?

Odpoveď bola áno a nevyžadovala si vlastný démon súborového systému ani komerčný appliance na code review. Stačilo využiť Git na to, čím v skutočnosti je pod kapotou: Merkle-tree Content-Addressable Store (CAS).

Omyl v srdci pomalých Git monorepozitárov

Keď vývojári reptajú, že Git sa neškáluje na veľké monorepozitáre, zriedkakedy narážajú na limit databázy objektov v Gite. Vnútri .git/objects Git ukladá tri nemenné primitíva, ktoré sú identifikované hashom ich obsahu SHA-1 (alebo SHA-256):

  1. blob: Surový bajtový obsah jedného súboru.
  2. tree: Výpis adresára, ktorý mapuje názvy súborov a oprávnenia na hashe blob alebo podriadených tree.
  3. commit: Malá textová hlavička (typicky 200 až 300 bajtov), ktorá ukazuje na jeden koreňový hash tree, nula alebo viac rodičovských hashov commit a metadáta autora.

Ako content-addressable store je tento návrh pozoruhodne efektívny. Ak monorepozitár obsahuje 50 000 súborov a upravíte jeden riadok v jednom súbore, nový commit znovu použije presné hashe blob a tree pre ostatných 49 999 súborov.

Prečo sa potom tradičný Git monorepozitár javí ťažkopádny? Pretože konvencia preťažuje jedinú referenciu, refs/heads/main, tromi navzájom konfliktnými úlohami:

  • Drží celý zjednotený adresárový strom každého projektu.
  • Ťahá so sebou celý reťazec historických commitov viažucich sa na rodičov až do prvého dňa.
  • Nachádza sa v refs/heads/*, v menennom priestore, ktorý Git servery štandardne oznamujú a s ktorým pri každom git clone a git fetch vyjednávajú.

Vždy, keď vývojár alebo CI runner stiahne refs/heads/main, klient a server vyjednávajú dosiahnuteľnosť v grafe commitov naprieč tisíckami historických revízií. Ak niekto pred troma rokmi omylom commitol 100 MB archív a na druhý deň ho zmazal, jeho blob stále visí na historickom tree, ktorý je dosiahnuteľný z refs/heads/main.

Rozdelenie menného priestoru referencií na štyri vrstvy

Wire protokol Gitu oznamuje počas štandardného klonovania iba refs/heads/* a refs/tags/*. Referencie v používateľských menných priestoroch, ako refs/trunk/* alebo refs/ws/*, existujú v rovnakom úložisku objektov na serveri, využívajú rovnakú kompresiu delta v packfile a možno ich explicitne fetchovať alebo pushovať, pričom nezaťažujú štandardné klony žiadnou režijnou záťažou.

Rozdelením zodpovedností medzi štyri menné priestory referencií v rámci jediného repozitára sa úzke hrdlo odstráni:

1refs/heads/main          Iba control plane (~20 KB: pravidlá, .gitconfig, sync konfigurácia, skripty)
2refs/trunk/main          Snímka commitu bez rodiča ukazujúca na najnovší zjednotený koreňový strom
3refs/trunk/history       Lineárny reťazec commitov viažucich sa na rodičov, zdieľajúci presne ten istý SHA-1 koreňového stromu
4refs/ws/<actor>/<id>     Izolované snímky pracovných priestorov (changelists) pre každého vývojára alebo agenta
5refs/heads/land/<id>     Dočasný spúšťač merge queue, okamžite zmazaný po zlúčení

1. Control plane (refs/heads/main)

Predvolená vetva neobsahuje žiadny zdrojový kód aplikácie. Sleduje iba prevádzkové pravidlá repozitára (AGENTS.md, README.md), zdieľaný .gitconfig s aliasmi pracovných postupov, konfiguráciu synchronizácie v Starlarku (sync.star) a serverové skripty na zlučovanie. Klonovanie predvolenej vetvy prenesie približne 20 KB a skončí počas niekoľkých milisekúnd.

2. Snímka trunku bez rodiča (refs/trunk/main)

refs/trunk/main ukazuje na kompletný, zjednotený zdrojový strom celej organizácie (//web/*, //net/*, //ipc/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), no jeho commit objekt je vytvorený bez parametra -p.

Keďže refs/trunk/main nemá žiadnych rodičov (parents = []), jeho fetch nevyžaduje žiadne vyjednávanie grafu commitov. Git server neprechádza históriu; pošle jediný commit hlavy a jeho aktuálne objekty adresárových tree. Historické binárne súbory alebo súbory zmazané pred mesiacom nie sú z refs/trunk/main dosiahnuteľné a nikdy sa neprenášajú.

3. Lineárna auditná história (refs/trunk/history)

Vývojári stále potrebujú git blame a git log pri vyšetrovaní regresií. Pri každom zlúčení zmeny skript na zlúčenie vytvorí dva commit objekty, ktoré ukazujú na presne rovnaký koreňový hash SHA-1 $NEW_TREE:

 1# Snímka commitu bez rodiča pre okamžité depth-1 checkouty pracovného priestoru
 2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
 3
 4# Commit viazaný na rodiča pre lineárny git log a git blame
 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"

Zdvojnásobuje udržiavanie refs/trunk/main a refs/trunk/history nároky repozitára na úložisko? Vôbec nie. Keďže oba commity odkazujú na identický hash $NEW_TREE, content-addressable store Gitu deduplikuje 100 % objektov tree a blob. Dodatočný náklad na úložisko pri snímke bez rodiča predstavuje jediná hlavička commitu s veľkosťou 250 bajtov.

4. Izolované pracovné priestory (refs/ws/<actor>/<id>)

Rozpracované vetvy sa pushujú do refs/ws/<actor>/<id>, nie do refs/heads/*. Stovky kódovacích agentov môžu ukladať checkpointy každú minútu bez zanášania rozbaľovacích ponúk vetiev alebo spomaľovania git fetch pre ľudských inžinierov.

Onboarding v piatich príkazoch: ako funguje riedky a blobless checkout

S touto referenčnou štruktúrou nový vývojár alebo efemérny kontajner kódovacieho agenta neinštaluje žiadne vlastné CLI ani FUSE ovládač. Onboarding pozostáva z piatich štandardných príkazov Gitu:

 1# 1. Klonovanie 20 KB control plane s filtrovaním blobov a zapnutým riedkym režimom
 2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
 3cd monorepo
 4
 5# 2. Načítanie aliasov repozitára a pripojenie refs/trunk/*
 6git config include.path ../.gitconfig
 7git trunk-init
 8
 9# 3. Deklarovanie doménových adresárov potrebných pre túto úlohu
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Vytvorenie izolovaného pracovného priestoru zakotveného v najnovšej snímke trunku
13git ws-new alice/optimize-framing

Pod kapotou každý krok vykoná iba minimálnu potrebnú prácu:

  1. git clone --filter=blob:none --sparse stiahne 20 KB control plane na refs/heads/main.
  2. git trunk-init nakonfiguruje +refs/trunk/*:refs/remotes/origin/trunk/* a spustí blobless fetch. Git stiahne metadáta tree pre refs/trunk/main (typicky niekoľko desiatok kilobajtov) a nula súborových blobov.
  3. git sparse-checkout set net/relay ipc/codec nariaďuje cone-mode indexovému filtru Gitu, aby zmaterializoval iba koreňový adresár a tieto dva podstromy.
  4. git ws-new prepne na novú vetvu zakotvenú v origin/trunk/main. Práve v tomto okamihu Git na požiadanie vyžiada iba objekty blob vnútri net/relay a ipc/codec. Zvyšných 95 % monorepozitára sa nikdy nestiahne na disk.

Budúci horizont: pripojenie refs/trunk/main ako virtuálny súborový systém (VFS)

Riedky checkout s blobless klonmi pokrýva každodenné pracovné postupy bez dodatočných klientskych nástrojov. Keďže refs/trunk/main je už čistý Merkle strom v Content-Addressable Store, architektúra tiež poskytuje priamy základ pre voliteľný používateľský virtuálny súborový systém (VFS) cez FUSE na Linuxe, FileProvider na macOS alebo ProjFS vo Windows:

  1. Okamžité pripojenie celého stromu: Ľahký lokálny mount démon načíta koreňový objekt tree z refs/trunk/main a počas niekoľkých milisekúnd premietne celú hierarchiu adresárov monorepozitára do pracovného adresára, čím odpadá potreba spúšťať git sparse-checkout set.
  2. Načítanie blobov na požiadanie: Obsah súborov zostáva virtuálny, kým kompilátor, IDE alebo agent nevydá systémové volanie open(2) alebo read(2). Démon potom stiahne zodpovedajúci blob s hashom SHA-1 z CAS endpointu a uloží ho do lokálneho NVMe úložiska.
  3. Copy-on-Write prekryvná vrstva pre pracovné priestory: Lokálne úpravy zapisujú do ľahkej hornej prekryvnej vrstvy. Spustenie git ws-save zahashuje iba zmenené cesty v tejto vrstve, zapíše aktualizované objekty blob a tree a pushne výsledný commit do refs/ws/<actor>/<id> bez prehľadávania nezmenených adresárov.

Model changelistu na čistom Gite: zlučovanie na serveri v pamäti

V tradičnom Git workflow zlúčenie feature vetvy buď vytvorí merge commit s dvoma rodičmi (čím sa história zmení na zamotanú železničnú stanicu), alebo si vývojári musia ručne robiť rebase a pretekať s ostatnými inžiniermi, ktorí pushujú do main.

Inšpirovaní modelom changelistov (CL) z google3 každý pracovný priestor v našej architektúre predstavuje jedinú atomickú zmenu voči snímke trunku. Počas lokálnej práce v ws/alice/optimize-framing môže vývojár alebo agent urobiť desať inkrementálnych commitov (wip, fix tests, lint). Keď je zmena pripravená na zlúčenie, server skolabuje deltu pracovného priestoru do jedinej lineárnej revízie trunku bez toho, aby na CI runneri kedy vykonal checkout pracovného adresára.

Takto skript na zlúčenie (land.sh) beží v pamäti pomocou plumbingu Gitu 2.40+:

 1# 1. Identifikácia koreňovej snímky commitu, z ktorej sa pracovný priestor vetvil
 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. Výpočet zlúčeného koreňového stromu v pamäti bez checkoutu pracovného stromu
 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 "Zistený merge konflikt; spustite git ws-sync na rebase." >&2
15  exit 1
16fi

Pozorne sa pozrite na krok 2:

  • Bez ohľadu na to, koľko lokálnych commitov vzniklo na vetve pracovného priestoru, skript porovnáva iba tri hashe stromov: pôvodnú základnú snímku ($BASE_TREE), aktuálnu snímku trunku ($MAIN_TREE) a výsledný strom pracovného priestoru ($WORK_TREE).
  • Ak sa refs/trunk/main posunul v //web/blog, zatiaľ čo Alice upravovala //net/relay, git merge-tree --write-tree vypočíta kombinovaný koreňový strom $NEW_TREE priamo v úložisku objektov Gitu počas niekoľkých milisekúnd. Na súborový systém runnera sa nezapíše žiadny súbor.
  • Nakoniec git push --atomic s --force-with-lease aktualizuje refs/trunk/main, pridá jeden lineárny commit do refs/trunk/history a zmaže spúšťač zlúčenia refs/heads/land/<id> v jedinej transakcii.

Vynucovanie architektonických hraníc: štvorúrovňová viditeľnosť a hierarchické OWNERS

Konsolidácia kódu do jedného repozitára funguje iba vtedy, ak sa hranice medzi doménami vynucujú mechanicky ešte predtým, než sa commit môže zlúčiť. Hneď po tom, ako git merge-tree --write-tree vytvorí $NEW_TREE (a pred tým, ako git push --atomic aktualizuje trunk), náš policy engine spustí dve kontroly so zložitosťou O(delta) nad git diff-tree "$MAIN_TREE" "$NEW_TREE".

1. Štvorúrovňová doménová viditeľnosť

Koreňový strom je rozdelený do štyroch úrovní viditeľnosti:

Prefix adresáraÚroveňPovolené importyZakázané importyBrána OWNERS
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*Public ProductionPublic Production, //third_party/*//corp/*, //experimental/*Vynucovaná
//third_party/*External VendorSamostatné alebo //third_party/*Všetky domény mimo third_partyVynucovaná
//corp/*Internal PrivatePublic Production, //corp/*, //third_party/*//experimental/*Vynucovaná
//experimental/<actor>/*SandboxAkýkoľvek balík mimo corp//corp/*Vyňatá (automaticky schválená)

Ak sa verejný balík v //web/* pokúsi importovať interný balík z //corp/* alebo neabsolvovaný prototyp z //experimental/*, úloha zlúčenia sa ukončí skôr, než sa dotkne refs/trunk/main.

2. Hierarchické overovanie OWNERS bez servera Gerrit

Na vynucovanie súborov OWNERS na úrovni adresárov s pravidlami dedenia nepotrebujete vyhradenú inštanciu Gerritu:

  • Pre každú cestu zmenenú medzi $MAIN_TREE a $NEW_TREE overovač prechádza smerom nahor od adresára súboru k //OWNERS.
  • Ľubovoľný súbor OWNERS môže deklarovať set noparent, čím zablokuje dedenie schválení z nadradených adresárov, alebo per-file *.sql=@dba-team, čím obmedzí schválenia podľa glob vzoru názvu súboru.
  • Invariant proti manipulácii: Existujúce súbory OWNERS sa vždy čítajú z $MAIN_TREE (aktuálny strom trunku), nikdy z $NEW_TREE. Neschválený autor si nemôže udeliť oprávnenie úpravou OWNERS v rovnakom commite pracovného priestoru.
  • Výnimka pre sandbox: Každá cesta pod //experimental/<actor>/* obchádza kontroly OWNERS, aby mohli inžinieri a agenti voľne prototypovať pomocou git ws-exp <actor> <project> a neskôr balík presunúť do produkčnej domény pomocou git ws-graduate.

Code review bez trenia: efemérne PR a diferencie snímok

Keďže refs/heads/main obsahuje iba control plane a refs/trunk/main je commit bez rodiča, otvorenie štandardného pull requestu na GitHube priamo z vetvy pracovného priestoru do main by sa pokúsilo porovnávať nesúvisiace histórie. Vyriešili sme to tromi workflow pre review prispôsobenými rôznym zmenám:

  1. Okamžité dvojbodkové diferencie pracovného priestoru (git ws-save + Edge Diff Router) Pushnutie pracovného priestoru pomocou git ws-save sprístupní refs/ws/<actor>/<id> cez ľahkého edge workera. Tento worker preloží refs/trunk/main a referenciu pracovného priestoru na ich SHA commitov a presmeruje na dvojbodkové porovnávacie zobrazenie GitHubu (/compare/<trunk-sha>..<ws-sha>). Pri malých záplatách alebo zmenách generovaných agentmi môžu recenzenti skontrolovať diferenciu a kliknúť na Land priamo vo webovom rozhraní.
  2. Efemérne pull requesty na GitHube (git ws-pr) Pre riadkovú kontrolu na GitHube git ws-pr pushne koreňový commit snímky ($(git rev-list --max-parents=0 HEAD)) do refs/heads/base/<id> a vrchol pracovného priestoru do refs/heads/review/<id>. Potom otvorí pull request na GitHube, ktorý porovnáva review/<id> s base/<id>. Keďže základná vetva je presne tá snímka, z ktorej sa pracovný priestor vetvil, záložka Files changed na GitHube zobrazuje iba presné riadky zmenené v pracovnom priestore. Keď recenzent kliknie na Merge pull request, webhook povýši hlavový commit do refs/heads/land/<id>, spustí zlučovací pipeline v pamäti a automaticky zmaže refs/heads/base/<id> aj refs/heads/review/<id>.
  3. Pull requesty na externých zrkadlách Prispievatelia do open-source projektov sa nikdy nemusia učiť workflow monorepozitára. Forknú repozitár a otvoria pull requesty voči samostatným zrkadlovým repozitárom ako obvykle.

Obojsmerná synchronizácia O(delta) a samostatné produkčné vetvy

Mnohé organizácie publikujú jednotlivé balíky ako samostatné GitHub repozitáre, aby ich externí používatelia mohli použiť cez go get, cargo install alebo forknúť jeden projekt bez zásahu do monorepozitára. Niektoré projekty, ako statické blogy alebo webové aplikácie viazané na vydania, navyše udržiavajú samostatnú vetvu production popri main.

Náš synchronizačný engine v Starlarku zvláda odchádzajúce exporty aj prichádzajúce príspevky v čase O(delta), úmernom počtu súborov zmenených v commite, a nie veľkosti monorepozitára:

  • Splicing indexu bez blobov: Pri vkladaní alebo vyberaní podstromu zrkadlového repozitára do koreňového stromu monorepozitára engine nastaví GIT_NO_LAZY_FETCH=1 a spustí git update-index --index-info, po ktorom nasleduje git write-tree --missing-ok. Nezmenené podstromy a bloby sa odkazujú čisto cez ich hashe SHA-1 bez toho, aby sa kedy stiahli na CI runner.
  • Prefetch blobov iba pre delta: Pred pushnutím exportovaného commitu do samostatného zrkadla skript spustí git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree" a prefetchuje iba staré a nové bloby zmenených súborov cez git cat-file --batch. Git potom dokáže vypočítať tenký delta packfile a pushnúť ho do zrkadlového repozitára počas niekoľkých sekúnd.
  • Zachovanie workflow a vydaných vetiev: Počas odchádzajúceho exportu synchronizátor zachováva vlastný adresár .github/workflows/ každého samostatného repozitára, aby verejné CI runnery spúšťali testy projektu na zrkadle. Každý workflow v konfigurácii Starlark cieli na explicitnú vetvu, napríklad main, čím sa zachovajú vydané brány pre repozitáre, ktoré nasadzujú z osobitnej vetvy production. Zlúčenia v monorepozitári sa synchronizujú s vetvou main zrkadla a automatizované metadátové alebo prekladové commity na main sa cez webhook synchronizujú späť do refs/trunk/main. Povýšenie main na production zostáva explicitným krokom vydania.

Záverečné myšlienky

Budovanie vysokorýchlostného monorepozitára nevyžaduje opustenie štandardného Gitu ani prevádzkovanie zložitej flotily na mieru vytvorenej infraštruktúry. Táto architektúra berie objektové úložisko Gitu ako Content-Addressable Store, oddeľuje refs/heads/main od refs/trunk/main a refs/trunk/history a zlučuje stromy v pamäti pomocou git merge-tree --write-tree. Výsledkom je, že ľudskí inžinieri aj autonómni kódovací agenti môžu klonovať pracovný priestor za menej ako sekundu a čisto sa škálovať naprieč stovkami projektov.