GoSuda

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

By Lemon Mint views ...

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:

  1. blob: De ruwe bytes van de inhoud van één bestand.
  2. tree: Een directorylijst die bestandsnamen en rechten koppelt aan blob- of onderliggende tree-hashes.
  3. 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 elke git clone en git 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:

  1. git clone --filter=blob:none --sparse downloadt het control plane van 20 KB op refs/heads/main.
  2. git trunk-init configureert +refs/trunk/*:refs/remotes/origin/trunk/* en voert een blobless fetch uit. Git downloadt de tree-metadata voor refs/trunk/main (doorgaans enkele tientallen kilobytes) en nul bestandsblobs.
  3. git sparse-checkout set net/relay ipc/codec instrueert het cone-mode-indexfilter van Git om alleen de rootmap en die twee subtrees te materialiseren.
  4. git ws-new schakelt over naar een nieuwe branch gebaseerd op origin/trunk/main. Op dat exacte moment vraagt Git alleen de blob-objecten binnen net/relay en ipc/codec op 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:

  1. Onmiddellijke koppeling van de volledige boom: Een lichtgewicht lokale koppelingsdaemon leest het roottree-object van refs/trunk/main en projecteert de volledige monorepo-directoryhiërarchie in de werkmap binnen milliseconden, waardoor het uitvoeren van git sparse-checkout set overbodig wordt.
  2. Blob-faulting op aanvraag: Bestandsinhoud blijft virtueel totdat een compiler, IDE of agent een open(2)- of read(2)-systeemaanroep uitvoert. De daemon haalt vervolgens de bijbehorende SHA-1 blob op van het CAS-eindpunt en slaat die op in de lokale NVMe-opslag.
  3. Copy-on-write-overlay voor werkruimten: Lokale bewerkingen worden naar een lichtgewicht bovenste overlaylaag geschreven. Het uitvoeren van git ws-save hasht alleen de gewijzigde paden in die overlay, schrijft de bijgewerkte blob- en tree-objecten en pusht de resulterende commit naar refs/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/main vooruitgaat in //web/blog terwijl Alice //net/relay bewerkt, berekent git merge-tree --write-tree de gecombineerde roottree $NEW_TREE rechtstreeks binnen de objectopslag van Git, in milliseconden. Er worden geen bestanden naar het bestandssysteem van de runner geschreven.
  • Ten slotte werkt git push --atomic met --force-with-lease refs/trunk/main bij, voegt één lineaire commit toe aan refs/trunk/history en verwijdert de landingstrigger refs/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:

MapprefixNiveauToegestane importsVerboden importsOWNERS-poort
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*Publieke productiePublieke productie, //third_party/*//corp/*, //experimental/*Afgedwongen
//third_party/*Externe leverancierZelfstandig of //third_party/*Alle niet-third_party-domeinenAfgedwongen
//corp/*Intern privéPublieke productie, //corp/*, //third_party/*//experimental/*Afgedwongen
//experimental/<actor>/*SandboxElk 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_TREE en $NEW_TREE is gewijzigd, loopt de verifier van de map van het bestand omhoog naar //OWNERS.
  • Elk OWNERS-bestand kan set noparent declareren om geërfde goedkeuringen van bovenliggende mappen te blokkeren, of per-file *.sql=@dba-team om 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 door OWNERS in dezelfde werkruimte-commit te bewerken.
  • Sandboxvrijstelling: Elk pad onder //experimental/<actor>/* omzeilt OWNERS-controles, zodat ingenieurs en agents vrij kunnen prototypen met git ws-exp <actor> <project> en het pakket later kunnen laten doorstromen naar een productiedomein met git 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:

  1. Directe tweepuntsdiffs van de werkruimte (git ws-save + Edge Diff Router) Het pushen van een werkruimte met git ws-save maakt refs/ws/<actor>/<id> zichtbaar via een lichtgewicht edge-worker. Deze worker herleidt refs/trunk/main en 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.
  2. Kortstondige GitHub Pull Requests (git ws-pr) Voor regelgewijze review op GitHub pusht git ws-pr de root-snapshotcommit ($(git rev-list --max-parents=0 HEAD)) naar refs/heads/base/<id> en de werkruimtetop naar refs/heads/review/<id>. Vervolgens opent het een GitHub Pull Request die review/<id> vergelijkt met base/<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 naar refs/heads/land/<id>, voert de in-memory landingspipeline uit en verwijdert automatisch zowel refs/heads/base/<id> als refs/heads/review/<id>.
  3. 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=1 in en voert git update-index --index-info uit, gevolgd door git 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 via git cat-file --batch alleen 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, zoals main, wat releasepoorten behoudt voor repositories die vanuit een afzonderlijke branch production deployen. Monorepo-landingen synchroniseren met de branch main van de spiegel, en geautomatiseerde metadata- of vertaalcommits op main synchroniseren via webhook terug naar refs/trunk/main. Het promoveren van main naar production blijft 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.