Přehodnocení Git Monorepo: CAS-nativní architektura pro lidi a Coding Agents

Každá rostoucí inženýrská organizace se nakonec rozhoduje mezi mnoha repozitáři a jedním. Začínáte s desítkami malých repozitářů a zpočátku se vše zdá jednoduché. Každá knihovna má vlastní issue tracker, vlastní CI pipeline a git clone, který doběhne během několika sekund. Pak změníte strukturu v základním síťovém balíčku a zbytek odpoledne strávíte otevíráním pull requestů pro zvýšení verze v šesti navazujících repozitářích a čekáním, až se CI fronty uvolní v topologickém pořadí.
Přesunete-li vše do tradičního Git monorepa, vyměníte koordinaci napříč repozitáři za velikost repozitáře. Jak historie commitů houstne a binární soubory se hromadí v minulých revizích, git fetch zpomaluje. Pracovní stromy narůstají na gigabajty. Brzy někdo navrhne nasadit vlastní démona virtuálního souborového systému, dedikovaný server pro code review nebo flotilu vzdálených build workerů, jen aby byl každodenní vývoj snesitelný.
Když se do týmu připojí autonomní agenti pro programování, obě selhání se zhorší. Polyrepa agentovi zakrývají volající z jiných projektů a nutí ho žonglovat s přihlašovacími údaji napříč repozitáři. Tradiční monorepa při inicializaci každého úkolu zahltí diskový prostor kontejneru agenta a síťovou propustnost, zatímco souběžní agenti, kteří zasahují do stejné větve, vytvářejí nekonečné závody při rebase.
Inspirováni architekturou google3 jsme vytvořili atomické změny na jediném trunku, sparse onboarding trvající méně než sekundu, hierarchickou správu OWNERS a samostatná open-source zrcadla se standardními Git nástroji a běžným objektovým úložištěm.
Návrh nepotřebuje ani vlastního démona souborového systému, ani externí appliance pro review, protože Git bere jako Merkle-tree Content-Addressable Store (CAS).
Omyl v jádru pomalých Git monorepo
Když vývojáři reptají, že Git neškáluje na velká monorepa, jen zřídka narážejí na limit Gitovy databáze objektů. Uvnitř .git/objects ukládá Git tři neměnné primitivy identifikované hashem obsahu SHA-1 (nebo SHA-256):
blob: Surový bajtový obsah jednoho souboru.tree: Výpis adresáře, který mapuje názvy souborů a oprávnění na hasheblobnebo podřízenýchtree.commit: Malá textová hlavička (typicky 200 až 300 bajtů), která ukazuje na jeden kořenový hashtree, nula nebo více rodičovských hashůcommita metadata autora.
Pokud monorepo obsahuje 50 000 souborů a upravíte jeden řádek v jednom souboru, nový commit znovu použije přesné hashe blob a tree pro zbylých 49 999 souborů.
Tradiční Git monorepo působí těžkopádně, protože konvence přetěžuje jedinou referenci, refs/heads/main, třemi navzájem si odporujícími úlohami:
- Drží celý sjednocený adresářový strom všech projektů.
- Táhne za sebou celý řetězec historických commitů propojených rodiči zpět na první den.
- Nachází se v
refs/heads/*, jmenném prostoru, který Git servery ve výchozím stavu inzerují a vyjednávají při každémgit cloneagit fetch.
Pokaždé, když vývojář nebo CI runner načte refs/heads/main, klient a server vyjednávají dosažitelnost v grafu commitů napříč tisíci historickými revizemi. Pokud někdo před třemi lety omylem commitnul archiv o velikosti 100 MB a následující den jej smazal, jeho blob stále visí na historickém tree, které je dosažitelné z refs/heads/main.
Oddělení jmenného prostoru referencí
Gitův wire protokol inzeruje během standardního klonování pouze refs/heads/* a refs/tags/*. Reference v vlastních jmenných prostorech, jako refs/trunk/* nebo refs/ws/*, žijí na serveru ve stejném úložišti objektů, využívají stejnou kompresi delta v packfiles a lze je explicitně stáhnout nebo odeslat. Výchozí klony přitom nezatěžují vůbec.
Rozdělení těchto úloh na samostatné reference v jediném repozitáři odstraňuje úzké hrdlo:
1refs/heads/main Pouze řídicí rovina (~20 KB: pravidla, .gitconfig, konfigurace synchronizace, skripty)
2refs/trunk/main Commit snapshotu bez rodiče ukazující na nejnovější sjednocený kořenový strom
3refs/trunk/history Lineární řetězec commitů propojených rodiči sdílející přesný SHA-1 kořenového stromu
4refs/ws/<actor>/<id> Izolované snapshoty pracovních prostorů (changelisty) pro každého vývojáře nebo agenta
5refs/ws/cl/<change-id>/<tip> Neměnné patchsety: každý tip změny odeslaný k review
6refs/heads/land/<id> Dočasný spouštěč merge fronty, ihned po přijetí smazaný
1. Řídicí rovina (refs/heads/main)
Výchozí větev neobsahuje žádný zdrojový kód aplikace. Sleduje pouze provozní pravidla repozitáře (AGENTS.md, README.md), sdílený .gitconfig s aliasy pracovních postupů, konfiguraci synchronizace ve Starlark (sync.star) a serverové skripty pro slučování. Klonování výchozí větve přenese zhruba 20 KB a dokončí se v milisekundách.
2. Snapshot trunku bez rodiče (refs/trunk/main)
refs/trunk/main ukazuje na kompletní, sjednocený zdrojový strom celé organizace (//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), jeho commit objekt však vzniká bez parametru -p.
Protože refs/trunk/main nemá žádné rodiče (parents = []), jeho stažení nevyžaduje žádné vyjednávání grafu commitů. Git server neprochází historii; odešle pouze jediný hlavní commit a jeho aktuální adresářové objekty tree. Historické binární soubory nebo soubory smazané před měsícem nejsou z refs/trunk/main dosažitelné a nikdy se nepřenášejí.
3. Lineární auditní historie (refs/trunk/history)
Vývojáři stále potřebují git blame a git log při vyšetřování regresí. Pokaždé, když se změna zapíše, slučovací skript vytvoří dva commit objekty ukazující na přesně stejný kořenový SHA-1 hash $NEW_TREE:
1# Snapshot commit bez rodiče pro okamžité checkouty pracovního prostoru v hloubce 1
2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
3
4# Commit propojený s rodičem pro lineární 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"
Ponechání obou referencí refs/trunk/main a refs/trunk/history nezdvojnásobí úložný prostor. Protože oba commity odkazují na identický hash $NEW_TREE, obsahově adresovatelné úložiště Gitu deduplikuje 100 % objektů tree a blob. Dodatečným nákladem snapshotu bez rodiče je jediná hlavička commitu o velikosti 250 bajtů.
4. Izolované pracovní prostory (refs/ws/<actor>/<id>)
Větve s rozpracovanou prací se odesílají do refs/ws/<actor>/<id>, nikoli do refs/heads/*. Sto agentů pro programování může ukládat checkpointy každou minutu, aniž by znečišťovali seznamy větví nebo zpomalovali git fetch pro lidské inženýry.
Onboarding ve čtyřech krocích: jak funguje sparse a blobless checkout
S tímto rozložením referencí nemusí nový vývojář ani dočasný kontejner s agentem pro programování instalovat vlastní CLI nebo FUSE ovladač. Onboarding trvá čtyři kroky se standardním Gitem a aliasy repozitáře:
1# 1. Klonování 20KB řídicí roviny s filtrem blobů a zapnutým sparse režimem
2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
3cd monorepo
4
5# 2. Načtení aliasů repozitáře a připojení refs/trunk/*
6git config include.path ../.gitconfig
7git trunk-init
8
9# 3. Deklarace doménových adresářů potřebných pro tento úkol
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Vytvoření izolovaného pracovního prostoru zakořeněného v nejnovějším snapshotu trunku
13git ws-new alice/optimize-framing
Každý krok stáhne pouze to, co potřebuje:
git clone --filter=blob:none --sparsestáhne 20KB řídicí roviny narefs/heads/main.git trunk-initnastaví+refs/trunk/*:refs/remotes/origin/trunk/*a provede blobless fetch. Git stáhne metadatatreeprorefs/trunk/main(obvykle několik desítek kilobajtů) a nula souborových blobů.git sparse-checkout set net/relay ipc/codecpřikazuje cone-mode indexovému filtru Gitu zpřístupnit pouze kořenový adresář a tyto dva podstromy.git ws-newpřepne na novou větev zakořeněnou vorigin/trunk/main. Git poté na vyžádání stáhne pouze objektyblobuvnitřnet/relayaipc/codec. Zbylých 95 % monorepa se na disk nikdy nestáhne.
Budoucí horizont: připojení refs/trunk/main jako virtuální souborový systém (VFS)
Sparse checkout s blobless klony pokrývá každodenní postupy bez dodatečných klientských nástrojů. Protože refs/trunk/main je již čistý Merkle strom v Content-Addressable Store,架 architektura poskytuje také přímý základ pro volitelný userspace virtuální souborový systém (VFS) přes FUSE na Linuxu, FileProvider na macOS nebo ProjFS na Windows:
- Okamžité připojení celého stromu: Lehký lokální démon pro připojení načte kořenový objekt
treezrefs/trunk/maina během milisekund promítne celou hierarchii adresářů monorepa do pracovního adresáře. Odpadá tak nutnost spouštětgit sparse-checkout set. - Načítání blobů na vyžádání: Obsah souborů zůstává virtuální, dokud kompilátor, IDE nebo agent nevyvolá systémové volání
open(2)neboread(2). Démon pak stáhne odpovídající SHA-1blobz CAS endpointu a uloží jej do lokálního úložiště NVMe. - Copy-on-Write překryvná vrstva pro pracovní prostory: Lokální úpravy se zapisují do lehké horní překryvné vrstvy. Spuštění
git ws-savezahashuje pouze změněné cesty v této vrstvě, zapíše aktualizované objektyblobatreea odešle výsledný commit dorefs/ws/<actor>/<id>, aniž by prohledával nezměněné adresáře.
Model changelistů na čistém Gitu: slučování na straně serveru v paměti
V tradičním Git pracovním postupu sloučení větve s funkcí buď vytvoří merge commit se dvěma rodiči, což činí historii nelineární, nebo vyžaduje, aby vývojáři ručně provedli rebase a soupeřili se souběžnými pushi do main.
Inspirováni modelem changelistů (CL) z google3 každý pracovní prostor v naší architektuře představuje jedinou atomickou změnu oproti snapshotu trunku. Zatímco vývojář nebo agent pracuje lokálně v ws/alice/optimize-framing, může udělat deset přírůstkových commitů (wip, fix tests, lint). Až bude změna připravena k zapsání, server zkolabuje deltu pracovního prostoru do jediné lineární revize trunku, aniž by na CI runneru kdy vytvořil pracovní adresář.
Takto spouští skript land.sh slučování v paměti pomocí plumbingu Gitu 2.40+:
1# 1. Určení kořenového snapshot commitu, ze kterého se pracovní prostor oddělil
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 sloučeného kořenové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 "Merge conflict detected; run git ws-sync to rebase." >&2
15 exit 1
16fi
V kroku 2:
- Bez ohledu na počet lokálních commitů v pracovní větvi skript porovnává pouze tři hashe stromů: původní základní snapshot (
$BASE_TREE), aktuální snapshot trunku ($MAIN_TREE) a výsledný strom pracovního prostoru ($WORK_TREE). - Pokud se
refs/trunk/mainposunul v//web/blog, zatímco Alice upravovala//net/relay,git merge-tree --write-treevypočítá kombinovaný kořenový strom$NEW_TREEpřímo v úložišti objektů Gitu během milisekund, aniž by zapisoval soubory na souborový systém runneru. - Po úspěšné kontrole politik uvedených níže jeden
git push --atomics--force-with-leaseaktualizujerefs/trunk/maina připojí jeden lineární commit dorefs/trunk/history. Spouštěcí reference pro přijetírefs/heads/land/<id>se smaže.
Vynucování architektonických hranic: čtyřúrovňová viditelnost a hierarchické OWNERS
Sloučení kódu do jediného repozitáře funguje pouze tehdy, když jsou hranice mezi doménami mechanicky vynucovány dříve, než lze commit zapsat. Ihned poté, co git merge-tree --write-tree vytvoří $NEW_TREE (a před tím, než git push --atomic aktualizuje trunk), spustí náš политický engine dvě kontroly se složitostí O(delta) nad výstupem git diff-tree "$MAIN_TREE" "$NEW_TREE".
1. Čtyřúrovňová viditelnost domén
Kořenový strom je rozdělen do čtyř úrovní viditelnosti:
| Předpona adresáře | Úroveň | Povolené importy | Zakázané importy | Brána OWNERS |
|---|---|---|---|---|
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/* | Veřejná produkce | Veřejná produkce, //third_party/* | //corp/*, //experimental/* | Vynucena |
//third_party/* | Externí dodavatel | Samostatné nebo //third_party/* | Všechny domény mimo third_party | Vynucena |
//corp/* | Interní soukromé | Veřejná produkce, //corp/*, //third_party/* | //experimental/* | Vynucena |
//experimental/<actor>/* | Sandbox | Libovolný balíček mimo corp | //corp/* | Vyjmuto (automaticky schváleno) |
Pokud se veřejný balíček v //web/* pokusí importovat interní balíček z //corp/* nebo nezralý prototyp z //experimental/*, úloha přijetí se přeruší dříve, než se dotkne refs/trunk/main.
2. Hierarchické ověřování OWNERS bez serveru Gerrit
Ověřovací nástroj vynucuje soubory OWNERS na úrovni adresářů s pravidly dědičnosti:
- Pro každou cestu změněnou mezi
$MAIN_TREEa$NEW_TREEověřovací nástroj prochází nahoru od adresáře souboru až k//OWNERS. - Kterýkoli soubor
OWNERSmůže deklarovatset noparent, čímž zablokuje dědění schválení z nadřazených adresářů, neboper-file *.sql=@dba-team, čímž omezí schválení podle globu názvu souboru. - Invariant proti manipulaci: Existující soubory
OWNERSse vždy čtou z$MAIN_TREE(aktuální strom trunku), nikdy z$NEW_TREE. Neschválený autor si nemůže udělit oprávnění úpravouOWNERSve stejném commitu pracovního prostoru. - Výjimka pro sandbox: Jakákoli cesta pod
//experimental/<actor>/*obchází kontrolyOWNERS, takže inženýři a agenti mohou volně prototypovat pomocígit ws-exp <actor> <project>a později balíček povýšit do produkční domény pomocígit ws-graduate.
Review bez tření: dočasné PR a diffy snapshotů
Protože refs/heads/main obsahuje pouze řídicí rovinu a refs/trunk/main je commit bez rodiče, přímé otevření standardního pull requestu na GitHubu z pracovní větve do main by se pokusilo porovnávat nesouvisející historie. Vyřešili jsme to třemi pracovními postupy pro review, přizpůsobenými různým typům změn:
- Okamžité dvoutečkové diffy pracovního prostoru (
git ws-save+ Edge Diff Router) Odeslání pracovního prostoru příkazemgit ws-savezpřístupnírefs/ws/<actor>/<id>přes lehký edge worker. Tento worker přeložírefs/trunk/maina referenci pracovního prostoru na jejich SHA commitů a přesměruje na dvoutečkový porovnávací pohled GitHubu (/compare/<trunk-sha>..<ws-sha>). U malých záplat nebo změn vygenerovaných agentem mohou recenzenti prohlédnout diff a přímo ve webovém rozhraní kliknout na Land. - Dočasné pull requesty na GitHubu (
git ws-pr) Pro řádkovou revizi na GitHubugit ws-prodešle kořenový snapshot commit ($(git rev-list --max-parents=0 HEAD)) dorefs/heads/base/<id>a špičku pracovního prostoru dorefs/heads/review/<id>. Poté otevře pull request na GitHubu porovnávajícíreview/<id>sbase/<id>. Protože základní větev je snapshot, ze kterého se pracovní prostor oddělil, záložka Files changed na GitHubu zobrazuje pouze deltu pracovního prostoru. Když recenzent klikne na Merge pull request, webhook povýší hlavní commit narefs/heads/land/<id>, spustí slučovací pipeline v paměti a automaticky smaže obě referencerefs/heads/base/<id>irefs/heads/review/<id>. - Pull requesty do externích zrcadel Open-source přispěvatelé se nikdy nemusí učit pracovní postup monorepa. Forkují a otevírají pull requesty do samostatných zrcadlových repozitářů jako obvykle.
Změny, patchsety a přenos schválení
Název větve nemůže změnu identifikovat. Agent může přejmenovat svou větev, upravit špičku nebo několikrát provést rebase na novější trunk, než změnu přijme. Stejně jako v Gerritu pracovní postup identifikuje změnu pomocí traileru Change-Id: I<40 hex> ve zprávě o špičkovém commitu. Příkazy git ws-save, git ws-pr a git ws-land trailer přidají, pokud chybí, a jinak jej zachovají, takže identifikátor přežije amend, rebase i přejmenování. Přijatý commit na trunku si trailer ponechá a zaznamenává číslo svého review pull requestu, takže git trunk-cl <number|Change-Id> jej najde v refs/trunk/history.
Každá špička odeslaná k review se stává patchsetem. git ws-pr jej odešle do neměnné reference refs/ws/cl/<change-id>/<tip> vedle review větví. Opětovné spuštění git ws-pr po amendu nebo git ws-sync přidá patchset do stejného pull requestu. git ws-ps vypíše patchsety aktuální změny a git ws-psdiff <a> <b> vytiskne interdiff mezi dvěma z nich, takže recenzent čte pouze to, co se změnilo od posledního kola.
GitHub váže schválení na jeden konkrétní commit, takže každý rebase by normálně vyžadoval nové review. Slučovací pipeline proto přijme i schválení udělené jinému patchsetu stejné změny, pokud oba patchsety provádějí stejnou záplatu vůči svému vlastnímu základnímu snapshotu. Porovnává výstup git patch-id --verbatim nad celým binárním diffem. Čistý rebase na novější trunk se přijme bez dalšího review, zatímco jakákoli úprava obsahu, včetně změny bílých znaků nebo režimu souboru, vyžaduje nové schválení.
Synchronizace O(delta) v obou směrech a oddělené produkční větve
Mnoho organizací publikuje jednotlivé balíčky jako samostatné repozitáře na GitHubu, aby je externí uživatelé mohli použít příkazem go get nebo cargo install či forkovat bez zásahu do monorepa. Některé projekty, například statické blogy nebo webové aplikace řízené vydáními, udržují navíc větev production vedle main.
Náš synchronizační engine ve Starlarku zvládá jak odchozí exporty, tak příchozí příspěvky v čase O(delta), který je úměrný počtu souborů změněných v commitu, nikoli velikosti monorepa:
- Splicování blobless indexu: Při vkládání podstromu zrcadlového repozitáře do kořenového stromu monorepa nebo jeho vyjmutí engine nastaví
GIT_NO_LAZY_FETCH=1a spustígit update-index --index-infoa potégit write-tree --missing-ok. Nezměněné podstromy a bloby zůstávají odkazovány přes SHA-1 a nikdy se nestahují na CI runner. - Předběžné načítání blobů pouze pro delta: Před odesláním exportovaného commitu do samostatného zrcadla skript spustí
git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"a předem načte pouze staré a nové bloby změněných souborů pomocígit cat-file --batch. Git pak může vypočítat tenký delta pack a odeslat jej do zrcadlového repozitáře během několika sekund. - Zachování pracovních postupů a release větví: Při odchozím exportu synchronizátor ponechává adresář
.github/workflows/každého samostatného repozitáře, takže veřejné CI runnery spouštějí testy projektu na zrcadle. Každý workflow v konfiguraci Starlark cílí na explicitní větev, napříkladmain, což zachovává release brány u repozitářů nasazovaných z oddělené větveproduction. Přijetí změn v monorepu se synchronizuje s větvímainzrcadla a automatizované commity metadat nebo překladů namainse zpět synchronizují dorefs/trunk/mainpomocí webhooku. Povýšenímainnaproductionzůstává explicitním krokem vydání.
Závěrečné úvahy
Tato architektura bere objektové úložiště Gitu jako Content-Addressable Store, odděluje refs/heads/main od refs/trunk/main a refs/trunk/history, identifikuje každou změnu pomocí Change-Id s neměnnými patchsety a slučuje stromy v paměti pomocí git merge-tree --write-tree. Náklady na onboarding rostou s adresáři, které úloha potřebuje, a náklady na přijetí změn a synchronizaci zrcadel rostou s velikostí změny, nikoli s velikostí repozitáře.