GoSuda

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

By Lemon Mint views ...

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):

  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 podradených tree.
  3. commit: Malá textová hlavička (zvyčajne 200 až 300 bajtov), ktorá ukazuje na jeden koreňový hash tree, nula alebo viac rodičovských hashov commit a 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ždom git clone a git 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:

  1. git clone --filter=blob:none --sparse stiahne 20 KB riadiacej roviny z 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 (zvyčajne niekoľko desiatok kilobajtov) a nula súborových blobov.
  3. git sparse-checkout set net/relay ipc/codec nariadi filtru indexu v cone režime Gitu, aby materializoval iba koreňový adresár a tieto dva podstromy.
  4. git ws-new prepne na novú vetvu zakorenenú v origin/trunk/main. Git potom na požiadanie vyžiada iba objekty blob vnútri net/relay a ipc/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:

  1. Okamžité pripojenie celého stromu: Ľahký lokálny mount démon načíta koreňový objekt tree z refs/trunk/main a 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.
  2. 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) alebo read(2). Démon potom stiahne zodpovedajúci blob SHA-1 z CAS endpointu a uloží ho do lokálneho NVMe úložiska.
  3. Copy-on-Write prekrytie pre pracovné priestory: Lokálne úpravy sa zapisujú do ľahkej hornej prekrývacej vrstvy. Spustenie git ws-save zahashuje iba zmenené cesty v tomto prekrytí, zapíše aktualizované objekty blob a tree a odošle výsledný commit do refs/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/main posunul v //web/blog, kým Alice upravovala //net/relay, git merge-tree --write-tree vypočíta kombinovaný koreňový strom $NEW_TREE priamo 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 --atomic s --force-with-lease aktualizuje refs/trunk/main, pripojí jeden lineárny commit do refs/trunk/history a spúšťacia referencia pristátia refs/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é importyZakázané importyBrána OWNERS
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*Verejná produkciaVerejná produkcia, //third_party/*//corp/*, //experimental/*Vynucovaná
//third_party/*Externý dodávateľSamostatné alebo //third_party/*Všetky domény okrem third_partyVynucovaná
//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_TREE a $NEW_TREE verifikátor prechádza smerom nahor od adresára súboru k //OWNERS.
  • Ľubovoľný súbor OWNERS môže deklarovať set noparent, čím zablokuje dedené schválenia 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 kmeňa), nikdy z $NEW_TREE. Neschválený autor si nemôže udeliť oprávnenie úpravou OWNERS v tom istom commite pracovného priestoru.
  • Výnimka pre pieskovisko: Každá cesta pod //experimental/<actor>/* obchádza kontroly OWNERS, aby inžinieri a agenti mohli voľne prototypovať pomocou git ws-exp <actor> <project> a neskôr balík povýšiť do produkčnej domény pomocou git 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:

  1. Okamžité dvojbodkové diffy pracovného priestoru (git ws-save + Edge Diff Router) Odoslanie pracovného priestoru pomocou git ws-save sprístupní refs/ws/<actor>/<id> cez ľahký edge worker. Tento worker preloží refs/trunk/main a 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.
  2. Efemérne GitHub Pull Requesty (git ws-pr) Pre riadkové review na GitHube git ws-pr odošle koreňový snapshot commit ($(git rev-list --max-parents=0 HEAD)) do refs/heads/base/<id> a vrchol pracovného priestoru do refs/heads/review/<id>. Potom otvorí GitHub Pull Request porovnávajúci review/<id> s base/<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 na refs/heads/land/<id>, spustí zlučovací postup pristátia v pamäti a automaticky vymaže refs/heads/base/<id> aj refs/heads/review/<id>.
  3. 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=1 a spustí git update-index --index-info a potom git 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 pomocou git cat-file --batch predbež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íklad main, čím sa zachovávajú release brány pre repozitáre, ktoré nasadzujú zo samostatnej vetvy production. Pristátia v monorepozitári sa synchronizujú s vetvou main zrkadla a automatizované commity metadát alebo prekladov 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

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.