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

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):
blob: Surový bajtový obsah jedného súboru.tree: Výpis adresára, ktorý mapuje názvy súborov a oprávnenia na hasheblobalebo podriadenýchtree.commit: Malá textová hlavička (typicky 200 až 300 bajtov), ktorá ukazuje na jeden koreňový hashtree, nula alebo viac rodičovských hashovcommita 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ždomgit cloneagit fetchvyjedná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:
git clone --filter=blob:none --sparsestiahne 20 KB control plane narefs/heads/main.git trunk-initnakonfiguruje+refs/trunk/*:refs/remotes/origin/trunk/*a spustí blobless fetch. Git stiahne metadátatreeprerefs/trunk/main(typicky niekoľko desiatok kilobajtov) a nula súborových blobov.git sparse-checkout set net/relay ipc/codecnariaďuje cone-mode indexovému filtru Gitu, aby zmaterializoval iba koreňový adresár a tieto dva podstromy.git ws-newprepne na novú vetvu zakotvenú vorigin/trunk/main. Práve v tomto okamihu Git na požiadanie vyžiada iba objektyblobvnútrinet/relayaipc/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:
- Okamžité pripojenie celého stromu: Ľahký lokálny mount démon načíta koreňový objekt
treezrefs/trunk/maina 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. - 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)aleboread(2). Démon potom stiahne zodpovedajúciblobs hashom SHA-1 z CAS endpointu a uloží ho do lokálneho NVMe úložiska. - Copy-on-Write prekryvná vrstva pre pracovné priestory: Lokálne úpravy zapisujú do ľahkej hornej prekryvnej vrstvy. Spustenie
git ws-savezahashuje iba zmenené cesty v tejto vrstve, zapíše aktualizované objektyblobatreea pushne výsledný commit dorefs/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/mainposunul v//web/blog, zatiaľ čo Alice upravovala//net/relay,git merge-tree --write-treevypočíta kombinovaný koreňový strom$NEW_TREEpriamo 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 --atomics--force-with-leaseaktualizujerefs/trunk/main, pridá jeden lineárny commit dorefs/trunk/historya zmaže spúšťač zlúčeniarefs/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é importy | Zakázané importy | Brána OWNERS |
|---|---|---|---|---|
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/* | Public Production | Public Production, //third_party/* | //corp/*, //experimental/* | Vynucovaná |
//third_party/* | External Vendor | Samostatné alebo //third_party/* | Všetky domény mimo third_party | Vynucovaná |
//corp/* | Internal Private | Public Production, //corp/*, //third_party/* | //experimental/* | Vynucovaná |
//experimental/<actor>/* | Sandbox | Aký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_TREEa$NEW_TREEoverovač prechádza smerom nahor od adresára súboru k//OWNERS. - Ľubovoľný súbor
OWNERSmôže deklarovaťset noparent, čím zablokuje dedenie schválení z nadradených adresárov, aleboper-file *.sql=@dba-team, čím obmedzí schválenia podľa glob vzoru názvu súboru. - Invariant proti manipulácii: Existujúce súbory
OWNERSsa vždy čítajú z$MAIN_TREE(aktuálny strom trunku), nikdy z$NEW_TREE. Neschválený autor si nemôže udeliť oprávnenie úpravouOWNERSv rovnakom commite pracovného priestoru. - Výnimka pre sandbox: Každá cesta pod
//experimental/<actor>/*obchádza kontrolyOWNERS, aby mohli inžinieri a agenti voľne prototypovať pomocougit ws-exp <actor> <project>a neskôr balík presunúť do produkčnej domény pomocougit 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:
- Okamžité dvojbodkové diferencie pracovného priestoru (
git ws-save+ Edge Diff Router) Pushnutie pracovného priestoru pomocougit ws-savesprístupnírefs/ws/<actor>/<id>cez ľahkého edge workera. Tento worker preložírefs/trunk/maina 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í. - Efemérne pull requesty na GitHube (
git ws-pr) Pre riadkovú kontrolu na GitHubegit ws-prpushne koreňový commit snímky ($(git rev-list --max-parents=0 HEAD)) dorefs/heads/base/<id>a vrchol pracovného priestoru dorefs/heads/review/<id>. Potom otvorí pull request na GitHube, ktorý porovnávareview/<id>sbase/<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 dorefs/heads/land/<id>, spustí zlučovací pipeline v pamäti a automaticky zmažerefs/heads/base/<id>ajrefs/heads/review/<id>. - 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=1a spustígit update-index --index-info, po ktorom nasledujegit 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 cezgit 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íkladmain, čím sa zachovajú vydané brány pre repozitáre, ktoré nasadzujú z osobitnej vetvyproduction. Zlúčenia v monorepozitári sa synchronizujú s vetvoumainzrkadla a automatizované metadátové alebo prekladové commity namainsa cez webhook synchronizujú späť dorefs/trunk/main. Povýšeniemainnaproductionzostá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.