GoSuda

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

By Lemon Mint views ...

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

  1. blob: Surový bajtový obsah jednoho souboru.
  2. tree: Výpis adresáře, který mapuje názvy souborů a oprávnění na hashe blob nebo podřazených tree.
  3. commit: Malá textová hlavička (typicky 200 až 300 bajtů) odkazující na jediný kořenový hash tree, nula nebo více rodičovských hashů commit a 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ém git clone a git fetch ve 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:

  1. git clone --filter=blob:none --sparse stáhne 20KB řídicí rovinu na refs/heads/main.
  2. git trunk-init nastaví +refs/trunk/*:refs/remotes/origin/trunk/* a spustí blobless fetch. Git stáhne metadata tree pro refs/trunk/main (typicky několik desítek kilobajtů) a nula souborových blobů.
  3. git sparse-checkout set net/relay ipc/codec přikazuje cone-mode indexovému filtru Gitu zmaterializovat pouze kořenový adresář a tyto dva podstromy.
  4. git ws-new přepne na novou větev zakořeněnou v origin/trunk/main. V tomto přesném okamžiku Git na vyžádání požádá pouze o objekty blob uvnitř net/relay a ipc/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:

  1. Okamžité připojení celého stromu: Lehký lokální mount démon přečte kořenový objekt tree z refs/trunk/main a promítne celou hierarchii monorepa do pracovního adresáře během milisekund, čímž odpadá nutnost spouštět git sparse-checkout set.
  2. 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) nebo read(2). Démon pak stáhne odpovídající SHA-1 blob z CAS endpointu a uloží ho do lokální cache na NVMe úložišti.
  3. Copy-on-Write overlay pro pracovní prostory: Lokální úpravy se zapisují do lehké horní overlay vrstvy. Spuštění git ws-save hashuje pouze znečištěné cesty v této vrstvě, zapíše aktualizované objekty blob a tree a nahraje výsledný commit do refs/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/main posunul v //web/blog, zatímco Alice upravovala //net/relay, git merge-tree --write-tree spočítá kombinovaný kořenový tree $NEW_TREE přímo uvnitř objektového úložiště Gitu během milisekund. Na souborový systém runneru se nezapisují žádné soubory.
  • Nakonec git push --atomic s --force-with-lease aktualizuje refs/trunk/main, připojí jeden lineární commit do refs/trunk/history a smaže spouštěč landingu refs/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é importyZakázané importyBrána OWNERS
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*Veřejná produkceVeřejná produkce, //third_party/*//corp/*, //experimental/*Vynucena
//third_party/*Externí dodavatelSamostatné nebo //third_party/*Všechny domény mimo third_partyVynucena
//corp/*Interní soukromáVeřejná produkce, //corp/*, //third_party/*//experimental/*Vynucena
//experimental/<actor>/*SandboxLibovolný 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_TREE a $NEW_TREE ověřovač prochází nahoru od adresáře souboru až k //OWNERS.
  • Libovolný soubor OWNERS může deklarovat set noparent, čímž zablokuje dědičná schválení z nadřazených adresářů, nebo per-file *.sql=@dba-team, čímž omezí schválení podle globu názvu souboru.
  • Invariant proti manipulaci: Existující soubory OWNERS se 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í úpravou OWNERS ve stejném commitu pracovního prostoru.
  • Výjimka pro sandbox: Každá cesta pod //experimental/<actor>/* obchází kontroly OWNERS, 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:

  1. Okamžité dvoutečkové diffy pracovního prostoru (git ws-save + Edge Diff Router) Nahrání pracovního prostoru příkazem git ws-save zpřístupní refs/ws/<actor>/<id> přes lehkého edge workera. Tento worker převede refs/trunk/main a 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.
  2. Efemérní GitHub Pull Requesty (git ws-pr) Pro řádkovou revizi na GitHubu git ws-pr nahraje kořenový snapshot commit ($(git rev-list --max-parents=0 HEAD)) do refs/heads/base/<id> a vrchol pracovního prostoru do refs/heads/review/<id>. Poté otevře GitHub Pull Request porovnávající review/<id> s base/<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 do refs/heads/land/<id>, spustí landing pipeline v paměti a automaticky smaže refs/heads/base/<id> i refs/heads/review/<id>.
  3. 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=1 a spustí git update-index --index-info ná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řes git cat-file --batch př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říklad main, čímž zachovává release brány pro repozitáře nasazované z odděleně větve production. Landingy monorepa se synchronizují s větví main zrcadla a automatické metadata nebo překladové commity na main se zpět synchronizují do refs/trunk/main přes webhook. Povýšení main na production zů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ů.