Přehodnocení Git monorepo: CAS-Native architektura pro lidi a Coding Agents

Každá rostoucí inženýrská organizace se nakonec dostane ke stejné křižovatce. Zpočátku, když začínáte s desítkami malých repozitářů, je život jednoduchý. Každá knihovna má vlastní issue tracker, vlastní CI pipeline a git clone, který skončí dřív, než si stihnete upít kávu. Pak ale přijde daň. Změníte strukturu v základním síťovém balíčku a zbytek odpoledne strávíte otevíráním version-bump pull requestů v šesti navazujících repozitářích a čekáním, až se CI fronty vyprázdní v topologickém pořadí.
Přesunete-li vše do tradičního Git monorepa, vyměníte bolesti koordinace za fyzikální bolesti. Jak historie commitů narůstá a binární assety se hromadí v minulých revizích, git fetch zpomaluje. Pracovní stromy dosahují gigabajtů. Brzy někdo navrhne nasadit vlastní virtuální souborový démon, 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í kódovací agenti, obě selhání se zhorší. Polyrepa agentovi zakrývají volající napříč projekty a nutí ho žonglovat s přihlašovacími údaji napříč repozitáři. Tradiční monorepa při každé inicializaci úlohy zahltí disk kontejneru agenta a síťovou propustnost, zatímco souběžní agenti pracující na stejné větvi vytvářejí nekonečné rebase závody.
Chtěli jsme třetí možnost. Inspirováni architekturou google3 jsme si položili jednoduchou otázku: Dokážeme získat atomické změny na jediném trunku, sparse onboarding v řádu sub-sekund, hierarchickou správu OWNERS a samostatná open-source zrcadla pouze pomocí standardního Git plumbingu a běžného objektového úložiště?
Odpověď zněla ano a nevyžadovala vlastní souborový démon ani externí review zařízení třetí strany. Vyžadovala pouze používat Git k tomu, čím ve skutečnosti je pod kapotou: Merkle-tree Content-Addressable Store (CAS).
Mylná představa v jádru pomalých Git monorepo
Když vývojáři stěžují, že Git neškáluje na velká monorepa, téměř nikdy nenarážejí na limit objektové databáze Gitu. 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řazenýchtree.commit: Malá textová hlavička (typicky 200 až 300 bajtů) odkazující na jediný kořenový hashtree, nula nebo více rodičovských hashůcommita metadata autora.
Jako content-addressable store je tento návrh pozoruhodně efektivní. 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ů.
Proč se tedy tradiční Git monorepo zdá těžké? Protože konvence přetěžuje jedinou referenci, refs/heads/main, třemi navzájem si odporujícími úkoly:
- Drží celý sjednocený adresářový strom všech projektů.
- Táhne za sebou celý řetězec historických commitů propojených rodiči až ke dni nula.
- Nachází se v
refs/heads/*, jmenném prostoru, který Git servery při každémgit cloneagit fetchve výchozím nastavení oznamují a vyjednávají.
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 omylem commitoval před třemi lety 100MB archiv a druhý den ho smazal, jeho blob stále visí na historickém tree, který je dosažitelný z refs/heads/main.
Rozdělení jmenného prostoru referencí do čtyř vrstev
Protokol Gitu při standardním klonování oznamuje pouze refs/heads/* a refs/tags/*. Reference pod vlastními jmennými prostory, jako refs/trunk/* nebo refs/ws/*, žijí na serveru ve stejném objektovém úložišti, těží ze stejné delta komprese v packfile a lze je explicitně načíst nebo nahrát, přitom však výchozí klony nezatěžují žádnou režií.
Rozdělením odpovědností mezi čtyři jmenné prostory referencí v jediném repozitáři úzké hrdlo zmizí:
1refs/heads/main Pouze řídicí rovina (~20 KB: pravidla, .gitconfig, konfigurace synchronizace, skripty)
2refs/trunk/main Snapshot commit bez rodiče ukazující na nejnovější sjednocený kořenový tree
3refs/trunk/history Lineární řetěz commitů propojených rodiči sdílející přesný SHA-1 kořenového tree
4refs/ws/<actor>/<id> Izolované snapshoty pracovního prostoru (changelisty) pro každého vývojáře nebo agenta
5refs/heads/land/<id> Efemérní spouštěč merge queue, okamžitě smazaný po landingu
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 workflow, konfiguraci synchronizace ve Starlarku (sync.star) a serverové merge skripty. Klonování výchozí větve přenese zhruba 20 KB a skončí v řádu milisekund.
2. Snapshot trunku bez rodiče (refs/trunk/main)
refs/trunk/main ukazuje na kompletní, sjednocený zdrojový strom celé organizace (//web/*, //net/*, //ipc/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), ale jeho commit objekt je vytvořen bez parametru -p.
Protože refs/trunk/main nemá žádné rodiče (parents = []), jeho načtení nevyžaduje žádné vyjednávání grafu commitů. Git server neprochází historii; odešle jediný hlavní commit a jeho aktuální adresářové objekty tree. Historické binárky 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ž je změna zapsána, merge skript vytvoří dva commit objekty ukazující na přesně stejný kořenový $NEW_TREE SHA-1 hash:
1# Snapshot commit bez rodiče pro okamžité depth-1 checkouty pracovního prostoru
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"
Zdvojnásobuje udržování refs/trunk/main i refs/trunk/history velikost úložiště repozitáře? Vůbec ne. Protože oba commity odkazují na identický hash $NEW_TREE, content-addressable store deduplikuje 100 % objektů tree a blob. Dodatečný náklad na úložiště snapshotu bez rodiče je pouze jediná hlavička commitu o velikosti 250 bajtů.
4. Izolované pracovní prostory (refs/ws/<actor>/<id>)
Větve s rozpracovanou prací se nahrávají do refs/ws/<actor>/<id>, nikoli do refs/heads/*. Stovka kódovacích agentů může ukládat checkpointy každou minutu, aniž by znečišťovala rozbalovací seznamy větví nebo zpomalovala git fetch pro lidské inženýry.
Onboarding v pěti příkazech: jak funguje sparse a blobless checkout
S touto strukturou referencí si nový vývojář nebo efemérní kontejner kódovacího agenta nemusí instalovat vlastní CLI ani FUSE ovladač. Onboarding zabere pět standardních příkazů Gitu:
1# 1. Naklonujte 20 KB řídicí rovinu 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čtěte aliasy repozitáře a připojte refs/trunk/*
6git config include.path ../.gitconfig
7git trunk-init
8
9# 3. Deklarujte adresářové domény potřebné pro tento úkol
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Vytvořte izolovaný pracovní prostor zakořeněný v nejnovějším snapshotu trunku
13git ws-new alice/optimize-framing
Pod kapotou každý krok provádí pouze minimální nutnou práci:
git clone --filter=blob:none --sparsestáhne 20KB řídicí rovinu narefs/heads/main.git trunk-initnastaví+refs/trunk/*:refs/remotes/origin/trunk/*a spustí blobless fetch. Git stáhne metadatatreeprorefs/trunk/main(typicky 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 zmaterializovat pouze kořenový adresář a tyto dva podstromy.git ws-newpřepne na novou větev zakořeněnou vorigin/trunk/main. V tomto přesném okamžiku Git na vyžádání požádá pouze o objektyblobuvnitřnet/relayaipc/codec. Zbylých 95 % monorepa se nikdy nestáhne na disk.
Budoucí horizont: připojení refs/trunk/main jako virtuálního souborového systému (VFS)
Sparse checkout s blobless klony pokrývá každodenní workflow bez dalších klientských nástrojů. Protože refs/trunk/main je již čistý Merkle strom v Content-Addressable Store, architektura poskytuje i 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í mount démon přečte kořenový objekt
treezrefs/trunk/maina promítne celou hierarchii monorepa do pracovního adresáře během milisekund, čímž odpadá 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ží ho do lokální cache na NVMe úložišti. - Copy-on-Write overlay pro pracovní prostory: Lokální úpravy se zapisují do lehké horní overlay vrstvy. Spuštění
git ws-savehashuje pouze znečištěné cesty v této vrstvě, zapíše aktualizované objektyblobatreea nahraje výsledný commit dorefs/ws/<actor>/<id>, aniž by prohledávalo nezměněné adresáře.
Model changelistu na čistém Gitu: landing na serveru v paměti
V tradičním Git workflow buď sloučení feature větve vytvoří merge commit se dvěma rodiči (a historie se promění v zamotané seřazovací nádraží), nebo vývojáři musí ručně provádět rebase a závodit s ostatními inženýry, kteří nahrávají do main.
Inspirován modelem Changelist (CL) z google3 představuje každý pracovní prostor v naší architektuře jedinou atomickou změnu oproti snapshotu trunku. Zatímco lokálně pracuje v ws/alice/optimize-framing, vývojář nebo agent může vytvořit deset postupných commitů (wip, fix tests, lint). Když je 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í landing skript (land.sh) operace v paměti pomocí plumbing příkazů Gitu 2.40+:
1# 1. Najděte kořenový snapshot commit, 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. Vypočítejte sloučený kořenový tree 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šimněte si pozorně 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 finální strom pracovního prostoru ($WORK_TREE). - Pokud se
refs/trunk/mainposunul v//web/blog, zatímco Alice upravovala//net/relay,git merge-tree --write-treespočítá kombinovaný kořenový tree$NEW_TREEpřímo uvnitř objektového úložiště Gitu během milisekund. Na souborový systém runneru se nezapisují žádné soubory. - Nakonec
git push --atomics--force-with-leaseaktualizujerefs/trunk/main, připojí jeden lineární commit dorefs/trunk/historya smaže spouštěč landingurefs/heads/land/<id>v jediné transakci.
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 vynuceny dříve, než lze commit zapsat. Hned poté, co git merge-tree --write-tree vytvoří $NEW_TREE (a dříve, než git push --atomic aktualizuje trunk), náš policy engine spustí dvě kontroly se složitostí O(delta) nad git diff-tree "$MAIN_TREE" "$NEW_TREE".
1. Čtyřúrovňová viditelnost domén
Kořenový tree je rozdělen do čtyř úrovní viditelnosti:
| Prefix 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/* | Vyjmuta (auto-schváleno) |
Pokud se veřejný balíček v //web/* pokusí importovat interní balíček z //corp/* nebo neabsolvovaný prototyp z //experimental/*, landing job se přeruší dříve, než se dotkne refs/trunk/main.
2. Hierarchické ověřování OWNERS bez serveru Gerrit
K vynucení souborů OWNERS na úrovni adresářů s pravidly dědičnosti nepotřebujete dedikovanou instanci Gerritu:
- Pro každou cestu změněnou mezi
$MAIN_TREEa$NEW_TREEověřovač prochází nahoru od adresáře souboru až k//OWNERS. - Libovolný soubor
OWNERSmůže deklarovatset noparent, čímž zablokuje dědič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í tree 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: Každá cesta pod
//experimental/<actor>/*obchází kontrolyOWNERS, aby inženýři a agenti mohli svobodně prototypovat pomocígit ws-exp <actor> <project>a později balíček převést do produkční domény pomocígit ws-graduate.
Code review bez tření: efemérní PR a diffy snapshotů
Protože refs/heads/main obsahuje pouze řídicí rovinu a refs/trunk/main je commit bez rodiče, otevření standardního GitHub Pull Requestu přímo z pracovní větve do main by se pokusilo porovnávat nesouvisející historie. Vyřešili jsme to třemi review workflow 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) Nahrání pracovního prostoru příkazemgit ws-savezpřístupnírefs/ws/<actor>/<id>přes lehkého edge workera. Tento worker převederefs/trunk/maina referenci pracovního prostoru na jejich commit SHA a přesměruje na dvoutečkový compare pohled GitHubu (/compare/<trunk-sha>..<ws-sha>). U malých patchů nebo změn generovaných agenty mohou recenzenti zkontrolovat diff a kliknout přímo ve webovém rozhraní na Land. - Efemérní GitHub Pull Requesty (
git ws-pr) Pro řádkovou revizi na GitHubugit ws-prnahraje kořenový snapshot commit ($(git rev-list --max-parents=0 HEAD)) dorefs/heads/base/<id>a vrchol pracovního prostoru dorefs/heads/review/<id>. Poté otevře GitHub Pull Request porovnávajícíreview/<id>sbase/<id>. Protože základní větev je přesný snapshot, ze kterého pracovní větev vznikla, záložka Files changed na GitHubu zobrazí pouze přesné řádky změněné v pracovním prostoru. Když recenzent klikne na Merge pull request, webhook povýší hlavní commit dorefs/heads/land/<id>, spustí landing pipeline v paměti a automaticky smažerefs/heads/base/<id>irefs/heads/review/<id>. - Pull requesty do externích zrcadel Open-source přispěvatelé se nikdy nemusí učit workflow monorepa. Forknou repozitář a otevřou pull requesty proti samostatným zrcadlovým repozitářům jako obvykle.
Synchronizace O(delta) v obou směrech a oddělené produkční větve
Mnoho organizací publikuje jednotlivé balíčky jako samostatné GitHub repozitáře, aby je externí uživatelé mohli použít příkazem go get, cargo install nebo forknout jediný projekt bez zásahu do monorepa. Některé projekty, například statické blogy nebo webové aplikace řízené vydáními, také udržují samostatnou 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) úměrném počtu souborů změněných v commitu, nikoli velikosti monorepa:
- Blobless index splicing: Při vkládání podstromu zrcadlového repozitáře do kořenového tree monorepa nebo jeho vyjmutí engine nastaví
GIT_NO_LAZY_FETCH=1a spustígit update-index --index-infonásledovanégit write-tree --missing-ok. Nezměněné podstromy a bloby se odkazují pouze svými SHA-1 hashi, aniž by byly kdy staženy na CI runner. - Delta-only blob prefetching: Před nahráním exportovaného commitu do samostatného zrcadla skript spustí
git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"a přesgit cat-file --batchpředem načte pouze staré a nové bloby změněných souborů. Git pak může sestavit tenký delta pack a během několika sekund jej nahrát do zrcadlového repozitáře. - Zachování workflow a release větví: Při odchozím exportu synchronizátor zachovává vlastní adresář
.github/workflows/každého samostatného repozitáře, aby veřejné CI runnery spouštěly testy projektu na zrcadle. Každý workflow v konfiguraci Starlark cílí na explicitní větev, napříkladmain, čímž zachovává release brány pro repozitáře nasazované z odděleně větveproduction. Landingy monorepa se synchronizují s větvímainzrcadla a automatické metadata nebo překladové commity namainse zpět synchronizují dorefs/trunk/mainpřes webhook. Povýšenímainnaproductionzůstává explicitním release krokem.
Závěrečné myšlenky
Vybudování monorepa s vysokou rychlostí vývoje nevyžaduje opuštění standardního Gitu ani provoz složité flotily zakázkových infrastrukturních serverů. Tato architektura považuje objektové úložiště Gitu za Content-Addressable Store, odděluje refs/heads/main od refs/trunk/main a refs/trunk/history a slučuje stromy v paměti pomocí git merge-tree --write-tree. Díky tomu mohou lidští inženýři i autonomní kódovací agenti naklonovat pracovní prostor za méně než sekundu a čistě škálovat napříč stovkami projektů.