Het Git Monorepo heroverwegen: een CAS-native architectuur voor mensen en coding agents

Elke groeiende engineeringorganisatie komt uiteindelijk voor dezelfde splitsing te staan. Begin met tientallen kleine repositories en het leven lijkt aanvankelijk eenvoudig. Elke bibliotheek heeft een eigen issuetracker, een eigen CI-pipeline en een git clone die al klaar is voordat je een slokje koffie hebt genomen. Dan komt de belasting. Je wijzigt een struct in een kernnetwerkpakket en besteedt de rest van de middag aan het openen van pull requests voor versieverhoging in zes afhankelijke repositories, terwijl je wacht tot de CI-wachtrijen in topologische volgorde zijn leeggelopen.
Verplaats alles naar een traditionele Git-monorepo en je ruilt coördinatiepijn in voor fysieke pijn. Naarmate de commitgeschiedenis groeit en binaire bestanden zich in eerdere revisies opstapelen, vertraagt git fetch. Werkmappen zwellen op tot gigabytes. Al snel wordt voorgesteld om een op maat gemaakte virtuele bestandssysteemdaemon, een apart code-reviewserver of een vloot externe buildworkers op te zetten, uitsluitend om de dagelijkse ontwikkeling draaglijk te houden.
Wanneer autonome codeeragenten deel gaan uitmaken van het team, worden beide faalwijzen erger. Polyrepos ontnemen een agent het zicht op aanroepers in andere projecten en dwingen hem tot het jongleren met credentials over repositories heen. Traditionele monorepos verzwelgen bij elke taakinitialisatie de schijfruimte van de container van de agent en de netwerkbandbreedte, terwijl gelijktijdige agents die dezelfde branch bewerken eindeloze rebase-races veroorzaken.
Wij wilden een derde optie. Geïnspireerd door de google3-architectuur stelden wij een eenvoudige vraag: Kunnen we atomaire wijzigingen op één trunk, sparse onboarding binnen een seconde, hiërarchische OWNERS-governance en zelfstandige open-sourcespiegels realiseren met uitsluitend standaard Git-plumbing en goedkope objectopslag?
Het antwoord bleek ja, en het vereiste geen op maat gemaakte bestandssysteemdaemon of een reviewapplicatie van een derde partij. Het vereiste alleen dat Git werd gebruikt voor wat het in wezen is: een Merkle-tree Content-Addressable Store (CAS).
Het misverstand achter trage Git-monorepos
Wanneer ontwikkelaars klagen dat Git niet schaalt naar grote monorepos, lopen zij zelden tegen een beperking van de objectdatabase van Git aan. Binnen .git/objects slaat Git drie onveranderlijke primitieven op, gesleuteld op hun SHA-1 (of SHA-256) contenthash:
blob: De ruwe bytes van de inhoud van één bestand.tree: Een directorylijst die bestandsnamen en rechten koppelt aanblob- of onderliggendetree-hashes.commit: Een kleine tekstheader (doorgaans 200 tot 300 bytes) die verwijst naar één roottree-hash, nul of meer bovenliggende commit-hashes en auteursmetadata.
Als content-addressable store is dit ontwerp opmerkelijk efficiënt. Als een monorepo 50.000 bestanden bevat en je wijzigt één regel in één bestand, dan hergebruikt de nieuwe commit de exacte blob- en tree-hashes van de overige 49.999 bestanden.
Waarom voelt een traditionele Git-monorepo dan toch zwaar aan? Omdat de conventie één referentie, refs/heads/main, belast met drie conflicterende taken:
- Het bevat de volledige, verenigde directorystructuur van elk project.
- Het sleept de volledige, aan elkaar gekoppelde keten van historische commits terug tot de eerste dag mee.
- Het bevindt zich in
refs/heads/*, de namespace die Git-servers standaard adverteren en onderhandelen bij elkegit cloneengit fetch.
Telkens wanneer een ontwikkelaar of CI-runner refs/heads/main ophaalt, onderhandelen client en server over het bereikbaarheidsgraaf van commits over duizenden historische revisies. Als iemand drie jaar geleden per ongeluk een archief van 100 MB heeft gecommit en het de volgende dag heeft verwijderd, hangt de blob daarvan nog steeds aan een historische tree die bereikbaar is vanaf refs/heads/main.
De referentienamespace opdelen in vier lagen
Het Git-wireprotocol adverteert tijdens een standaard clone alleen refs/heads/* en refs/tags/*. Referenties onder aangepaste namespaces, zoals refs/trunk/* of refs/ws/*, bevinden zich op de server in exact dezelfde objectopslag, profiteren van dezelfde packfile-deltacompressie en kunnen expliciet worden opgehaald of gepusht. Toch veroorzaken ze geen extra overhead bij standaard clones.
Door verantwoordelijkheden over vier referentienamespaces binnen één repository te verdelen, verdwijnt het knelpunt:
1refs/heads/main Alleen control plane (~20 KB: regels, .gitconfig, synchronisatieconfig, scripts)
2refs/trunk/main Commit-snapshot zonder ouders die verwijst naar de nieuwste verenigde roottree
3refs/trunk/history Lineaire, aan elkaar gekoppelde commitketen die exact dezelfde roottree-SHA-1 deelt
4refs/ws/<actor>/<id> Geïsoleerde werkruimte-snapshots (changelists) per ontwikkelaar of agent
5refs/heads/land/<id> Kortstondige merge-queue-trigger die direct na landing wordt verwijderd
1. Het control plane (refs/heads/main)
De standaardbranch bevat geen applicatiebroncode. Hij bevat alleen de regels voor bedrijfsvoering van de repository (AGENTS.md, README.md), een gedeeld .gitconfig met workflowaliassen, de Starlark-synchronisatieconfiguratie (sync.star) en server-side mergescripts. Het clonen van de standaardbranch transfereert ongeveer 20 KB en is in milliseconden klaar.
2. De trunk-snapshot zonder ouders (refs/trunk/main)
refs/trunk/main verwijst naar de volledige, verenigde broncodeboom van de hele organisatie (//web/*, //net/*, //ipc/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), maar het commitobject wordt aangemaakt zonder een -p-ouderparameter.
Omdat refs/trunk/main nul ouders heeft (parents = []), vereist het ophalen ervan geen onderhandeling over het commitgraaf. De Git-server doorloopt de geschiedenis niet; hij stuurt alleen de enkele headcommit en de huidige directory-tree-objecten. Historische binaire bestanden of bestanden die vorige maand zijn verwijderd, zijn niet bereikbaar vanaf refs/trunk/main en worden nooit overgedragen.
3. De lineaire auditgeschiedenis (refs/trunk/history)
Ontwikkelaars hebben nog steeds git blame en git log nodig bij het onderzoeken van regressies. Telkens wanneer een wijziging wordt gelanded, maakt het mergescript twee commitobjecten aan die naar exact dezelfde root $NEW_TREE SHA-1-hash verwijzen:
1# Snapshotcommit zonder ouders voor directe depth-1 werkruimte-checkouts
2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
3
4# Commit met ouderlink voor lineaire git log en 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"
Verdubbelt het onderhouden van zowel refs/trunk/main als refs/trunk/history de opslagruimte van de repository? Helemaal niet. Omdat beide commits naar exact dezelfde $NEW_TREE-hash verwijzen, dedupliceert de content-addressable store van Git 100% van de tree- en blob-objecten. De extra opslagkosten van de snapshot zonder ouders bestaan uit één commitheader van 250 bytes.
4. Geïsoleerde werkruimten (refs/ws/<actor>/<id>)
Werk in uitvoering wordt gepusht naar refs/ws/<actor>/<id> in plaats van naar refs/heads/*. Honderd codeeragenten kunnen elke minuut checkpoints opslaan zonder de branchselectie te vervuilen of git fetch voor menselijke ingenieurs te vertragen.
Onboarding in vijf commando's: hoe sparse + blobless checkout werkt
Met deze referentie-indeling heeft een nieuwe ontwikkelaar of een kortstondige codeeragent-container geen aangepaste CLI of FUSE-driver nodig. Onboarding vereist vijf standaard Git-commando's:
1# 1. Kloon het control plane van 20 KB met blobfiltering en sparse-modus ingeschakeld
2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
3cd monorepo
4
5# 2. Laad repositoryaliassen en koppel refs/trunk/*
6git config include.path ../.gitconfig
7git trunk-init
8
9# 3. Declareer de domeinmappen die nodig zijn voor deze taak
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Maak een geïsoleerde werkruimte gebaseerd op de meest recente trunk-snapshot
13git ws-new alice/optimize-framing
Onder de motorkap voert elke stap alleen het minimaal noodzakelijke werk uit:
git clone --filter=blob:none --sparsedownloadt het control plane van 20 KB oprefs/heads/main.git trunk-initconfigureert+refs/trunk/*:refs/remotes/origin/trunk/*en voert een blobless fetch uit. Git downloadt detree-metadata voorrefs/trunk/main(doorgaans enkele tientallen kilobytes) en nul bestandsblobs.git sparse-checkout set net/relay ipc/codecinstrueert het cone-mode-indexfilter van Git om alleen de rootmap en die twee subtrees te materialiseren.git ws-newschakelt over naar een nieuwe branch gebaseerd oporigin/trunk/main. Op dat exacte moment vraagt Git alleen deblob-objecten binnennet/relayenipc/codecop aanvraag op. De overige 95% van de monorepo wordt nooit naar de schijf gedownload.
Toekomstperspectief: refs/trunk/main als virtueel bestandssysteem (VFS) koppelen
Sparse checkout met blobless clones dekt de dagelijkse workflows zonder extra clienttooling. Omdat refs/trunk/main al een zuivere Merkle-tree in een content-addressable store is, biedt de architectuur ook een directe basis voor een optioneel userspace virtueel bestandssysteem (VFS) via FUSE op Linux, FileProvider op macOS of ProjFS op Windows:
- Onmiddellijke koppeling van de volledige boom: Een lichtgewicht lokale koppelingsdaemon leest het roottree-object van
refs/trunk/mainen projecteert de volledige monorepo-directoryhiërarchie in de werkmap binnen milliseconden, waardoor het uitvoeren vangit sparse-checkout setoverbodig wordt. - Blob-faulting op aanvraag: Bestandsinhoud blijft virtueel totdat een compiler, IDE of agent een
open(2)- ofread(2)-systeemaanroep uitvoert. De daemon haalt vervolgens de bijbehorende SHA-1blobop van het CAS-eindpunt en slaat die op in de lokale NVMe-opslag. - Copy-on-write-overlay voor werkruimten: Lokale bewerkingen worden naar een lichtgewicht bovenste overlaylaag geschreven. Het uitvoeren van
git ws-savehasht alleen de gewijzigde paden in die overlay, schrijft de bijgewerkteblob- entree-objecten en pusht de resulterende commit naarrefs/ws/<actor>/<id>, zonder ongewijzigde mappen te scannen.
Het changelistmodel op pure Git: landing in het geheugen aan de serverzijde
In een traditionele Git-workflow creëert het samenvoegen van een featurebranch ofwel een merge-commit met twee ouders (waardoor de geschiedenis verandert in een verward rangeerterrein), ofwel moeten ontwikkelaars handmatig rebasen en concurreren met andere ingenieurs die naar main pushen.
Geïnspireerd door het changelistmodel (CL) van google3 vertegenwoordigt elke werkruimte in onze architectuur één atomaire wijziging ten opzichte van de trunk-snapshot. Terwijl een ontwikkelaar of agent lokaal in ws/alice/optimize-framing werkt, kan hij tien incrementele commits maken (wip, fix tests, lint). Wanneer de wijziging klaar is om te worden gelanded, reduceert de server het werkruimte-delta tot één lineaire trunkrevisie, zonder ooit een werkmap op de CI-runner uit te checken.
Hier is hoe het landingsscript (land.sh) in het geheugen wordt uitgevoerd met Git 2.40+ plumbing:
1# 1. Bepaal de root-snapshotcommit waarop de werkruimte is afgetakt
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. Bereken de samengevoegde roottree in het geheugen zonder een working tree-checkout
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
Kijk goed naar stap 2:
- Ongeacht het aantal lokale commits op de werkruimtebranch, vergelijkt het script slechts drie tree-hashes: de oorspronkelijke basis-snapshot (
$BASE_TREE), de huidige trunk-snapshot ($MAIN_TREE) en de uiteindelijke werkruimteboom ($WORK_TREE). - Als
refs/trunk/mainvooruitgaat in//web/blogterwijl Alice//net/relaybewerkt, berekentgit merge-tree --write-treede gecombineerde roottree$NEW_TREErechtstreeks binnen de objectopslag van Git, in milliseconden. Er worden geen bestanden naar het bestandssysteem van de runner geschreven. - Ten slotte werkt
git push --atomicmet--force-with-leaserefs/trunk/mainbij, voegt één lineaire commit toe aanrefs/trunk/historyen verwijdert de landingstriggerrefs/heads/land/<id>in één transactie.
Architectuurgrenzen afdwingen: vierlaagse zichtbaarheid en hiërarchische OWNERS
Het consolideren van code in één repository werkt alleen als de grenzen tussen domeinen mechanisch worden afgedwongen voordat een commit kan landen. Direct nadat git merge-tree --write-tree $NEW_TREE heeft geproduceerd (en voordat git push --atomic de trunk bijwerkt), voert onze beleidsengine twee controles uit met O(delta)-complexiteit over git diff-tree "$MAIN_TREE" "$NEW_TREE".
1. Vierlaagse domeinzichtbaarheid
De roottree is verdeeld in vier zichtbaarheidsniveaus:
| Mapprefix | Niveau | Toegestane imports | Verboden imports | OWNERS-poort |
|---|---|---|---|---|
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/* | Publieke productie | Publieke productie, //third_party/* | //corp/*, //experimental/* | Afgedwongen |
//third_party/* | Externe leverancier | Zelfstandig of //third_party/* | Alle niet-third_party-domeinen | Afgedwongen |
//corp/* | Intern privé | Publieke productie, //corp/*, //third_party/* | //experimental/* | Afgedwongen |
//experimental/<actor>/* | Sandbox | Elk niet-corp-pakket | //corp/* | Vrijgesteld (automatisch goedgekeurd) |
Als een publiek pakket in //web/* probeert een intern pakket uit //corp/* of een niet-afgestudeerd prototype uit //experimental/* te importeren, breekt de landingsjob af voordat refs/trunk/main wordt aangeraakt.
2. Hiërarchische OWNERS-verificatie zonder Gerrit-server
Een aparte Gerrit-instantie is niet nodig om directorybrede OWNERS-bestanden met overervingsregels af te dwingen:
- Voor elk pad dat tussen
$MAIN_TREEen$NEW_TREEis gewijzigd, loopt de verifier van de map van het bestand omhoog naar//OWNERS. - Elk
OWNERS-bestand kanset noparentdeclareren om geërfde goedkeuringen van bovenliggende mappen te blokkeren, ofper-file *.sql=@dba-teamom goedkeuringen te beperken op basis van een bestandsnaampatroon. - Anti-manipulatie-invariant: Bestaande
OWNERS-bestanden worden altijd gelezen uit$MAIN_TREE(de huidige trunkboom), nooit uit$NEW_TREE. Een niet-goedgekeurde auteur kan zichzelf geen toestemming verlenen doorOWNERSin dezelfde werkruimte-commit te bewerken. - Sandboxvrijstelling: Elk pad onder
//experimental/<actor>/*omzeiltOWNERS-controles, zodat ingenieurs en agents vrij kunnen prototypen metgit ws-exp <actor> <project>en het pakket later kunnen laten doorstromen naar een productiedomein metgit ws-graduate.
Code-review zonder wrijving: kortstondige PR's en snapshotdiffs
Omdat refs/heads/main alleen het control plane bevat en refs/trunk/main een commit zonder ouders is, zou het openen van een standaard GitHub Pull Request rechtstreeks vanaf een werkruimtebranch naar main ongerelateerde geschiedenissen met elkaar proberen te vergelijken. Wij hebben dit opgelost met drie reviewworkflows, afgestemd op verschillende wijzigingen:
- Directe tweepuntsdiffs van de werkruimte (
git ws-save+ Edge Diff Router) Het pushen van een werkruimte metgit ws-savemaaktrefs/ws/<actor>/<id>zichtbaar via een lichtgewicht edge-worker. Deze worker herleidtrefs/trunk/mainen de werkruimtereferentie naar hun commit-SHA's en stuurt door naar de tweepuntsvergelijkingsweergave van GitHub (/compare/<trunk-sha>..<ws-sha>). Bij kleine patches of door agents gegenereerde wijzigingen kunnen reviewers de diff inspecteren en direct in de webinterface op Land klikken. - Kortstondige GitHub Pull Requests (
git ws-pr) Voor regelgewijze review op GitHub pushtgit ws-prde root-snapshotcommit ($(git rev-list --max-parents=0 HEAD)) naarrefs/heads/base/<id>en de werkruimtetop naarrefs/heads/review/<id>. Vervolgens opent het een GitHub Pull Request diereview/<id>vergelijkt metbase/<id>. Omdat de basisbranch exact de snapshot is waarop de werkruimte is afgetakt, toont het tabblad Files changed van GitHub alleen de exacte regels die in de werkruimte zijn gewijzigd. Wanneer de reviewer op Merge pull request klikt, promoveert een webhook de headcommit naarrefs/heads/land/<id>, voert de in-memory landingspipeline uit en verwijdert automatisch zowelrefs/heads/base/<id>alsrefs/heads/review/<id>. - Pull requests via externe spiegels Open-sourcebijdragers hoeven nooit de monorepo-workflow te leren. Zij forken en openen pull requests tegen zelfstandige spiegelrepositories, zoals gebruikelijk.
Bidirectionele synchronisatie met O(delta)-complexiteit en afzonderlijke productiebranches
Veel organisaties publiceren afzonderlijke pakketten als zelfstandige GitHub-repositories, zodat externe gebruikers een enkel project kunnen go get'en, cargo install'en of forken zonder de monorepo aan te raken. Sommige projecten, zoals statische blogs of webapplicaties met releasepoorten, onderhouden ook een afzonderlijke branch production naast main.
Onze Starlark-synchronisatie-engine verwerkt zowel uitgaande exports als inkomende bijdragen in O(delta)-tijd, evenredig aan de bestanden die in een commit zijn gewijzigd en niet aan de omvang van de monorepo:
- Blobless indexsplitsing: Bij het invoegen of verwijderen van een spiegelrepositorysubtree in of uit de roottree van de monorepo, stelt de engine
GIT_NO_LAZY_FETCH=1in en voertgit update-index --index-infouit, gevolgd doorgit write-tree --missing-ok. Ongewijzigde subtrees en blobs worden uitsluitend via hun SHA-1-hashes gerefereerd, zonder ooit naar de CI-runner te worden gedownload. - Prefetching van uitsluitend delta-blobs: Voordat een geëxporteerde commit naar een zelfstandige spiegel wordt gepusht, voert het script
git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"uit en haalt het viagit cat-file --batchalleen de oude en nieuwe blobs van de gewijzigde bestanden vooraf op. Git kan vervolgens een dunne deltapack berekenen en binnen enkele seconden naar de spiegelrepository pushen. - Behoud van workflow- en releasebranches: Tijdens de uitgaande export behoudt de synchronisator de eigen map
.github/workflows/van elke zelfstandige repository, zodat publieke CI-runners de projecttests op de spiegel uitvoeren. Elke workflow in de Starlark-configuratie richt zich op een expliciete branch, zoalsmain, wat releasepoorten behoudt voor repositories die vanuit een afzonderlijke branchproductiondeployen. Monorepo-landingen synchroniseren met de branchmainvan de spiegel, en geautomatiseerde metadata- of vertaalcommits opmainsynchroniseren via webhook terug naarrefs/trunk/main. Het promoveren vanmainnaarproductionblijft een expliciete releasestap.
Afsluitende gedachten
Het bouwen van een monorepo met hoge snelheid vereist niet dat men standaard Git laat varen of een complexe vloot op maat gemaakte infrastructuurservers exploiteert. Deze architectuur behandelt de objectopslag van Git als een Content-Addressable Store, scheidt refs/heads/main van refs/trunk/main en refs/trunk/history, en voegt bomen samen in het geheugen met git merge-tree --write-tree. Het resultaat is dat zowel menselijke ingenieurs als autonome codeeragents een werkruimte binnen één seconde kunnen klonen en zich schoon kunnen laten uitschalen over honderden projecten.