Prehodnotenie Git monorepo: CAS-native architektúra pre ľudí a kódovacích agentov

Každá rastúca inžinierska organizácia si nakoniec musí vybrať medzi mnohými repozitármi a jedným. Keď začnete s desiatkami malých repozitárov, život je spočiatku jednoduchý. Každá knižnica má vlastný systém na sledovanie problémov, vlastnú CI pipeline a git clone, ktorý sa dokončí v priebehu sekúnd. Potom zmeníte štruktúru 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, pričom čakáte, kým sa CI fronty vyprázdnia v topologickom poradí.
Ak všetko presuniete do tradičného Git monorepozitára, výmenou za koordináciu medzi repozitármi získate veľkosť repozitára ako problém. Keď sa história commitov prehlbuje a binárne assety sa hromadia v minulých revíziách, git fetch sa spomaľuje. Pracovné stromy narastú na gigabajty. Čoskoro niekto navrhne zriadiť vlastný virtuálny súborový démon, 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 pre programovanie, obe zlyhania sa zhoršia. Polyrepozitáre zaslepia agenta voči volajúcim v iných projektoch a nútia ho žonglovať s prihlasovacími údajmi naprieč repozitármi. Tradičné monorepozitáre pri každej inicializácii úlohy zahltia disk kontajnera agenta a sieťovú šírku pásma, zatiaľ čo súbežní agenti pracujúci na tej istej vetve vytvárajú nekonečné rebase preteky.
Inšpirovaní architektúrou google3 sme vytvorili jednokmeňové atomické zmeny, sub-sekundové riedke onboardovanie, hierarchickú správu OWNERS a samostatné open-source zrkadlá so štandardnými Git nástrojmi a bežným objektovým úložiskom.
Návrh nepotrebuje ani vlastný súborový démon, ani externú appliance na review, pretože s Gitom zaobchádza ako s Merkle-tree Content-Addressable Store (CAS).
Mylná predstava v srdci pomalých Git monorepozitárov
Keď vývojári sťažujú, že Git sa neškáluje na veľké monorepozitáre, zriedkakedy narážajú na limit objektovej databázy Gitu. V .git/objects Git ukladá tri nemenné primitíva 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 podradenýchtree.commit: Malá textová hlavička (zvyčajne 200 až 300 bajtov), ktorá ukazuje na jeden koreňový hashtree, nula alebo viac rodičovských hashovcommita metadáta autora.
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.
Tradičný Git monorepozitár pôsobí ťažkopádne, pretože konvencia preťažuje jednu referenciu, refs/heads/main, tromi navzájom si odporujúcimi úlohami:
- Drží celý zjednotený adresárový strom každého projektu.
- Ťahá so sebou celý reťazec historických commitov naviazaných na rodičov až do prvého dňa.
- Nachádza sa v
refs/heads/*, mennom priestore, ktorý Git servery štandardne oznamujú a s ktorým sa dohadujú pri každomgit cloneagit fetch.
Zakaždým, keď vývojár alebo CI runner stiahne refs/heads/main, klient a server sa dohodnú na dosiahnuteľnosti v grafe commitov naprieč tisíckami historických revízií. Ak niekto pred tromi 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.
Oddelenie menného priestoru referencií
Git wire protokol počas štandardného klonovania oznamuje iba refs/heads/* a refs/tags/*. Referencie v používateľských menných priestoroch, napríklad refs/trunk/* alebo refs/ws/*, žijú v presne tom istom objektovom úložisku na serveri, profitujú z rovnakej delta kompresie v packfile a možno ich explicitne stiahnuť alebo odoslať, no nespôsobujú žiadnu réžiu pri predvolených klonoch.
Rozdelenie týchto úloh medzi samostatné referencie v jednom repozitári odstraňuje úzke miesto:
1refs/heads/main Iba riadiaca rovina (~20 KB: pravidlá, .gitconfig, konfigurácia synchronizácie, skripty)
2refs/trunk/main Snapshot commit bez rodiča ukazujúci na najnovší zjednotený koreňový strom
3refs/trunk/history Lineárny reťazec commitov naviazaných na rodičov, zdieľajúci presne koreňový strom SHA-1
4refs/ws/<actor>/<id> Izolované snapshoty pracovných priestorov (changelists) pre každého vývojára alebo agenta
5refs/ws/cl/<change-id>/<tip> Nemenné patchsety: každý vrchol zmeny odoslaný na review
6refs/heads/land/<id> Efemérny spúšťač merge queue, ihneď po zlúčení vymazaný
1. Riadiaca rovina (refs/heads/main)
Predvolená vetva neobsahuje žiadny zdrojový kód aplikácie. Sleduje iba pravidlá prevádzky 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 dokončí sa v milisekundách.
2. Snapshot zjednoteného kmeňa bez rodiča (refs/trunk/main)
refs/trunk/main ukazuje na kompletný, zjednotený zdrojový strom celej organizácie (//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), ale jeho commit objekt vzniká bez prepínača -p pre rodiča.
Keďže refs/trunk/main nemá žiadnych rodičov (parents = []), jeho stiahnutie nevyžaduje žiadne dohadovanie o grafe commitov. Git server neprechádza históriu; pošle jediný hlavný commit a jeho aktuálne adresárové objekty 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 pristátí zmeny skript na zlučovanie vytvorí dva commit objekty, ktoré ukazujú na presne rovnaký koreňový hash SHA-1 $NEW_TREE:
1# Snapshot commit bez rodiča pre okamžité checkouty pracovného priestoru s hĺbkou 1
2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
3
4# Commit naviazaný 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"
Uchovanie refs/trunk/main aj refs/trunk/history neznásobuje nároky na úložisko. Keďže oba commity odkazujú na identický $NEW_TREE hash, Git's content-addressable store deduplikuje 100 % objektov tree a blob. Dodatočný náklad na snapshot bez rodiča predstavuje jediná hlavička commitu s veľkosťou 250 bajtov.
4. Izolované pracovné priestory (refs/ws/<actor>/<id>)
Vetvy s rozpracovanou prácou sa odosielajú do refs/ws/<actor>/<id>, nie do refs/heads/*. Sto agentov na programovanie môže ukladať checkpointy každú minútu bez zahlcovania rozbaľovacích zoznamov vetiev alebo spomaľovania git fetch pre ľudských inžinierov.
Onboarding v štyroch krokoch: ako funguje riedky checkout s blobless klonovaním
S touto referenčnou štruktúrou nový vývojár alebo efemérny kontajner s agentom na programovanie neinštaluje vlastné CLI ani FUSE ovládač. Onboarding trvá štyri kroky so štandardným Gitom a aliasmi repozitára:
1# 1. Klonovanie 20 KB riadiacej roviny 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 zakoreneného v najnovšom snapshote kmeňa
13git ws-new alice/optimize-framing
Každý krok stiahne iba to, čo potrebuje:
git clone --filter=blob:none --sparsestiahne 20 KB riadiacej roviny zrefs/heads/main.git trunk-initnakonfiguruje+refs/trunk/*:refs/remotes/origin/trunk/*a spustí blobless fetch. Git stiahne metadátatreeprerefs/trunk/main(zvyčajne niekoľko desiatok kilobajtov) a nula súborových blobov.git sparse-checkout set net/relay ipc/codecnariadi filtru indexu v cone režime Gitu, aby materializoval iba koreňový adresár a tieto dva podstromy.git ws-newprepne na novú vetvu zakorenenú vorigin/trunk/main. Git potom na požiadanie vyžiada iba objektyblobvnútrinet/relayaipc/codec. Zvyšných 95 % monorepozitára sa na disk nikdy nesťahuje.
Budúci horizont: pripojenie refs/trunk/main ako virtuálneho súborového systému (VFS)
Riedky checkout s blobless klonmi pokrýva každodenné pracovné postupy bez dodatočných klientských nástrojov. Keďže refs/trunk/main je už čistý Merkle strom v Content-Addressable Store, architektúra zároveň 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 na Windows:
- Okamžité pripojenie celého stromu: Ľahký lokálny mount démon načíta koreňový objekt
treezrefs/trunk/maina premietne celú hierarchiu adresárov monorepozitára do pracovného adresára v priebehu milisekúnd, čím odpadá potreba spúšťaťgit sparse-checkout set. - Dopytovanie 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úciblobSHA-1 z CAS endpointu a uloží ho do lokálneho NVMe úložiska. - Copy-on-Write prekrytie pre pracovné priestory: Lokálne úpravy sa zapisujú do ľahkej hornej prekrývacej vrstvy. Spustenie
git ws-savezahashuje iba zmenené cesty v tomto prekrytí, zapíše aktualizované objektyblobatreea odošle výsledný commit dorefs/ws/<actor>/<id>bez prehľadávania nezmenených adresárov.
Model changelistov na čistom Gite: zlučovanie na serveri v pamäti
V tradičnom Git pracovnom postupe zlúčenie feature vetvy buď vytvorí merge commit s dvoma rodičmi, čo robí históriu nelineárnou, alebo vyžaduje, aby vývojári manuálne robili rebase a súťažili so súbežnými pushmi do main.
Inšpirovaní modelom Changelist (CL) z google3 predstavuje každý pracovný priestor v našej architektúre jednu atomickú zmenu voči snapshotu kmeňa. 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 pristátie, server zbalí rozdiel pracovného priestoru do jedinej lineárnej revízie kmeňa bez toho, aby na CI runneri kedy vykonal checkout pracovného adresára.
Takto skript pristátia (land.sh) beží v pamäti pomocou nástrojov Git 2.40+:
1# 1. Identifikácia koreňového snapshot commitu, z ktorého sa pracovný priestor odvetvil
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ý konflikt pri zlučovaní; spustite git ws-sync na rebase." >&2
15 exit 1
16fi
V kroku 2:
- Bez ohľadu na to, koľko lokálnych commitov vzniklo vo vetve pracovného priestoru, skript porovnáva iba tri hashe stromov: pôvodný základný snapshot (
$BASE_TREE), aktuálny snapshot kmeňa ($MAIN_TREE) a výsledný strom pracovného priestoru ($WORK_TREE). - Ak sa
refs/trunk/mainposunul v//web/blog, kým Alice upravovala//net/relay,git merge-tree --write-treevypočíta kombinovaný koreňový strom$NEW_TREEpriamo v objektovom úložisku Gitu v priebehu milisekúnd, bez zapisovania súborov na súborový systém runnera. - Po úspešnom prejdení nižšie uvedených kontrol politík jeden
git push --atomics--force-with-leaseaktualizujerefs/trunk/main, pripojí jeden lineárny commit dorefs/trunk/historya spúšťacia referencia pristátiarefs/heads/land/<id>sa vymaže.
Vynucovanie hraníc architektúry: š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 pred tým, ako môže commit pristáť. Hneď po tom, ako git merge-tree --write-tree vytvorí $NEW_TREE (a pred tým, ako git push --atomic aktualizuje kmeň), náš policy engine spustí dve kontroly so zložitosťou O(delta) nad git diff-tree "$MAIN_TREE" "$NEW_TREE".
1. Štvorúrovňová viditeľnosť domén
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/* | Verejná produkcia | Verejná produkcia, //third_party/* | //corp/*, //experimental/* | Vynucovaná |
//third_party/* | Externý dodávateľ | Samostatné alebo //third_party/* | Všetky domény okrem third_party | Vynucovaná |
//corp/* | Interný súkromný | Verejná produkcia, //corp/*, //third_party/* | //experimental/* | Vynucovaná |
//experimental/<actor>/* | Pieskovisko | Ľubovoľný balík okrem corp | //corp/* | Vynímaná (automaticky schválené) |
Ak sa verejný balík v //web/* pokúsi importovať interný balík z //corp/* alebo neabsolvovaný prototyp z //experimental/*, úloha pristátia sa preruší skôr, než sa dotkne refs/trunk/main.
2. Hierarchické overovanie OWNERS bez servera Gerrit
Verifikátor vynucuje súbory OWNERS na úrovni adresárov s pravidlami dedičnosti:
- Pre každú cestu zmenenú medzi
$MAIN_TREEa$NEW_TREEverifikátor prechádza smerom nahor od adresára súboru k//OWNERS. - Ľubovoľný súbor
OWNERSmôže deklarovaťset noparent, čím zablokuje dedené schválenia 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 kmeňa), nikdy z$NEW_TREE. Neschválený autor si nemôže udeliť oprávnenie úpravouOWNERSv tom istom commite pracovného priestoru. - Výnimka pre pieskovisko: Každá cesta pod
//experimental/<actor>/*obchádza kontrolyOWNERS, aby inžinieri a agenti mohli voľne prototypovať pomocougit ws-exp <actor> <project>a neskôr balík povýšiť do produkčnej domény pomocougit ws-graduate.
Review kódu bez trenia: efemérne PR a diffy snapshotov
Keďže refs/heads/main obsahuje iba riadiacu rovinu a refs/trunk/main je commit bez rodiča, otvorenie štandardného GitHub Pull Requestu priamo z vetvy pracovného priestoru do main by porovnávalo nesúvisiace históriu. Vyriešili sme to tromi review workflowmi prispôsobenými rôznym typom zmien:
- Okamžité dvojbodkové diffy pracovného priestoru (
git ws-save+ Edge Diff Router) Odoslanie pracovného priestoru pomocougit ws-savesprístupnírefs/ws/<actor>/<id>cez ľahký edge worker. Tento worker preložírefs/trunk/maina referenciu pracovného priestoru na 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ť diff a kliknúť priamo vo webovom rozhraní na Land. - Efemérne GitHub Pull Requesty (
git ws-pr) Pre riadkové review na GitHubegit ws-prodošle koreňový snapshot commit ($(git rev-list --max-parents=0 HEAD)) dorefs/heads/base/<id>a vrchol pracovného priestoru dorefs/heads/review/<id>. Potom otvorí GitHub Pull Request porovnávajúcireview/<id>sbase/<id>. Keďže základná vetva je snapshot, z ktorého sa pracovný priestor odvetvil, záložka Files changed na GitHube zobrazuje iba rozdiel pracovného priestoru. Keď recenzent kliknie na Merge pull request, webhook povýši hlavný commit narefs/heads/land/<id>, spustí zlučovací postup pristátia v pamäti a automaticky vymažerefs/heads/base/<id>ajrefs/heads/review/<id>. - Pull Requesty do externých zrkadiel Prispievatelia z open-source komunity sa nikdy nemusia učiť pracovný postup monorepozitára. Vytvoria fork a otvoria pull requesty voči samostatným zrkadlovým repozitárom ako obvykle.
Zmeny, patchsety a prenos schválení
Názov vetvy nemôže identifikovať zmenu. Agent môže premenovať svoju vetvu, upraviť vrchol alebo viackrát urobiť rebase na novší kmeň, kým zmenu zlúči. Podobne ako v Gerrite, pracovný postup identifikuje zmenu pomocou trailera Change-Id: I<40 hex> v správe vrcholového commitu. git ws-save, git ws-pr a git ws-land tento trailer pridajú, ak ho vrchol nemá, a inak ho ponechajú, takže identifikátor prežije amendy, rebase aj premenovania. Pristátý commit v kmeni si trailer zachová a zaznamená číslo svojho review pull requestu, takže ho git trunk-cl <number|Change-Id> nájde v refs/trunk/history.
Každý vrchol odoslaný na review sa stane patchsetom. git ws-pr ho odošle do nemennej referencie refs/ws/cl/<change-id>/<tip> vedľa review vetiev. Opätovné spustenie git ws-pr po amende alebo git ws-sync pridá patchset do toho istého pull requestu. git ws-ps vypíše patchsety aktuálnej zmeny a git ws-psdiff <a> <b> vypíše interdiff medzi dvoma z nich, takže recenzent číta iba to, čo sa zmenilo od posledného kola.
GitHub viaže schválenie na jeden commit, takže každý rebase by inak vyžadoval nové review. Pristávací postup preto akceptuje aj schválenie udelené inému patchsetu tej istej zmeny, ak oba patchsety robia rovnakú záplatu voči svojmu vlastnému základnému snapshotu. Porovnáva git patch-id --verbatim nad úplným binárnym diffom. Čistý rebase na novší kmeň pristane bez ďalšieho review, zatiaľ čo každá úprava obsahu vrátane zmeny whitespace alebo režimu súboru si vyžaduje nové schválenie.
Obojsmerná synchronizácia O(delta) a oddelené 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 alebo cargo install či forknúť jeden projekt bez zásahu do monorepozitára. Niektoré projekty, napríklad statické blogy alebo webové aplikácie viazané na release, navyše udržiavajú vetvu production popri main.
Náš synchronizačný engine v Starlarku spracúva odchádzajúce exporty aj prichádzajúce príspevky v čase O(delta), úmernom súborom zmeneným v commite, 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-infoa potomgit write-tree --missing-ok. Nezmenené podstromy a bloby zostávajú referencované pomocou SHA-1 a nikdy sa nesťahujú na CI runner. - Predbežné sťahovanie blobov iba pre zmeny: Pred odoslaním exportovaného commitu do samostatného zrkadla skript spustí
git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"a pomocougit cat-file --batchpredbežne stiahne iba starý a nový blob zmenených súborov. Git potom môže vypočítať tenký delta pack a odoslať ho do zrkadlového repozitára v priebehu sekúnd. - Zachovanie workflowov a release 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 Starlarku cieli na explicitnú vetvu, napríkladmain, čím sa zachovávajú release brány pre repozitáre, ktoré nasadzujú zo samostatnej vetvyproduction. Pristátia v monorepozitári sa synchronizujú s vetvoumainzrkadla a automatizované commity metadát alebo prekladov namainsa cez webhook synchronizujú späť dorefs/trunk/main. Povýšeniemainnaproductionzostáva explicitným krokom vydania.
Záverečné myšlienky
Táto architektúra považuje objektové úložisko Gitu za Content-Addressable Store, oddeľuje refs/heads/main od refs/trunk/main a refs/trunk/history, identifikuje každú zmenu pomocou Change-Id s nemennými patchsetmi a zlučuje stromy v pamäti pomocou git merge-tree --write-tree. Náklady na onboarding rastú s adresármi, ktoré úloha potrebuje, a náklady na pristátie a synchronizáciu zrkadiel rastú s veľkosťou zmeny, nie s veľkosťou repozitára.