Et nytt blikk på Git-monorepoet: En CAS-native arkitektur for mennesker og Coding Agents

Hver voksende ingeniørorganisasjon støter til slutt på det samme veiskillet. Man starter med dusinvis av små repositories, og tilværelsen føles enkel til å begynne med. Hvert bibliotek har sin egen saksliste, sin egen CI-pipeline og en git clone som er ferdig før man rekker å ta en slurk kaffe. Så kommer skatten. Du endrer en struct i en sentral nettverkspakke og bruker resten av ettermiddagen på å åpne pull requests for versjonsoppdatering i seks nedstrøms repositories, mens du venter på at CI-køene tømmes i topologisk rekkefølge.
Flytter man alt over i et tradisjonelt Git-monorepo, bytter man koordineringssmerte mot fysikksmerte. Etter hvert som commit-historikken blir dypere og binære filer hoper seg opp i tidligere revisjoner, blir git fetch tregere. Arbeidstrær vokser til gigabyte. Før lenge foreslår noen at man setter opp en tilpasset virtuell filsystemdaemon, en dedikert kodegjennomgangsserver eller en flåte med eksterne byggarbeidere bare for å gjøre den daglige utviklingen utholdelig.
Når autonome kodeagenter blir med i teamet, forverres begge feilmodusene. Polyrepoer gjør en agent blind for kall på tvers av prosjekter og tvinger den til å jonglere med legitimasjon på tvers av repositories. Tradisjonelle monorepoer drukner containerdisken og nettverksbåndbredden til agenten ved hver oppgaveinitialisering, mens samtidige agenter som trår på hverandre på samme gren skaper endeløse rebase-kappløp.
Vi ønsket et tredje alternativ. Inspirert av google3-arkitekturen stilte vi et enkelt spørsmål: Kan vi oppnå atomiske endringer på én trunk, sparse-onboarding på under ett sekund, hierarkisk OWNERS-styring og frittstående åpen kildekode-speiler ved kun å bruke standard Git-plumbing og rimelig objektlagring?
Svaret viste seg å være ja, og det krevde verken en tilpasset filsystemdaemon eller en tredjeparts gjennomgangsløsning. Det krevde bare at man brukte Git til det det faktisk er under panseret: et Merkle-tre-basert Content-Addressable Store (CAS).
Misforståelsen i kjernen av trege Git-monorepoer
Når utviklere klager over at Git ikke skalerer til store monorepoer, treffer de sjelden en grense i Gits objektdatabase. Inne i .git/objects lagrer Git tre uforanderlige primitiver indeksert etter deres SHA-1- (eller SHA-256-) innholdshash:
blob: Råbyteinnholdet i en enkelt fil.tree: En katalogliste som knytter filnavn og tillatelser tilblob- eller underordnedetree-hasher.commit: En liten tekstheader (vanligvis 200 til 300 byte) som peker til én rot-tree-hash, null eller flere forelder-commit-hasher og forfattermetadata.
Som et content-addressable store er denne designen bemerkelsesverdig effektiv. Hvis et monorepo inneholder 50 000 filer og du redigerer én linje i én fil, gjenbruker den nye commiten de eksakte blob- og tree-hashene for de andre 49 999 filene.
Hvorfor føles da et tradisjonelt Git-monorepo tungt? Fordi konvensjonen overbelaster en enkelt referanse, refs/heads/main, med tre motstridende oppgaver:
- Den inneholder hele det samlede katalogtreet for hvert prosjekt.
- Den drar med seg hele den forelderkoblede kjeden av historiske commits tilbake til første dag.
- Den ligger i
refs/heads/*, navnerommet som Git-servere annonserer og forhandler om som standard ved hvergit cloneoggit fetch.
Hver gang en utvikler eller CI-kjører henter refs/heads/main, forhandler klient og server om commit-grafens nåbarhet på tvers av tusenvis av historiske revisjoner. Hvis noen for tre år siden ved en feil committet et arkiv på 100 MB og slettet det dagen etter, henger fortsatt blob-en fast i et historisk tree som kan nås fra refs/heads/main.
Å skille referansenavnerommet i fire lag
Gits wire-protokoll annonserer kun refs/heads/* og refs/tags/* under en standard klone. Referanser under egendefinerte navnerom, som refs/trunk/* eller refs/ws/*, ligger i nøyaktig samme objektlager på serveren, drar nytte av samme packfile-deltakomprimering og kan hentes eller pushes eksplisitt, men de påfører standardkloner null overhead.
Ved å dele ansvar på tvers av fire referansenavnerom i ett repository forsvinner flaskehalsen:
1refs/heads/main Kun kontrollplan (~20 KB: regler, .gitconfig, synkroniseringskonfig, skript)
2refs/trunk/main Snapshot-commit uten forelder som peker til det nyeste samlede rottreet
3refs/trunk/history Lineær kjede av forelderkoblede commits som deler nøyaktig rottre-SHA-1
4refs/ws/<actor>/<id> Isolerte arbeidsområde-snapshots (changelists) per utvikler eller agent
5refs/heads/land/<id> Midlertidig utløser for sammenslåingskø, slettes umiddelbart etter landing
1. Kontrollplanet (refs/heads/main)
Standardgrenen inneholder ingen applikasjonskildekode. Den sporer kun repositoryets driftsregler (AGENTS.md, README.md), en delt .gitconfig med arbeidsflytaliaser, Starlark-synkroniseringskonfigurasjonen (sync.star) og sammenslåingsskript på serversiden. Kloning av standardgrenen overfører omtrent 20 KB og er ferdig på millisekunder.
2. Trunk-snapshotet uten forelder (refs/trunk/main)
refs/trunk/main peker til det komplette, samlede kildetreet for hele organisasjonen (//web/*, //net/*, //ipc/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), men commit-objektet opprettes uten et -p-forelderflagg.
Fordi refs/trunk/main har null foreldre (parents = []), krever henting av den null negotiering av commit-grafen. Git-serveren går ikke gjennom historikken; den sender den ene hode-commiten og dens nåværende katalog-tree-objekter. Historiske binærfiler eller filer slettet forrige måned er ikke nåbare fra refs/trunk/main og overføres aldri.
3. Den lineære revisjonshistorikken (refs/trunk/history)
Utviklere trenger fortsatt git blame og git log når de undersøker regresjoner. Hver gang en endring blir landet, oppretter sammenslåingsskriptet to commit-objekter som peker til den nøyaktig samme rot-$NEW_TREE-SHA-1-hashen:
1# Snapshot-commit uten forelder for øyeblikkelige depth-1-utsjekker av arbeidsområder
2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
3
4# Commit med forelder for lineær git log og 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"
Dobler det å vedlikeholde både refs/trunk/main og refs/trunk/history repositoryets lagringsfotavtrykk? Slett ikke. Fordi begge commitene refererer til den identiske $NEW_TREE-hashen, deduplikerer Gits content-addressable store 100 % av tree- og blob-objektene. Den ekstra lagringskostnaden for snapshotet uten forelder er en enkelt 250 byte stor commit-header.
4. Isolerte arbeidsområder (refs/ws/<actor>/<id>)
Arbeid under utvikling pushes til refs/ws/<actor>/<id> i stedet for refs/heads/*. Hundre kodeagenter kan lagre sjekkpunkter hvert minutt uten å forurense grenlisten eller bremse git fetch for menneskelige ingeniører.
Onboarding på fem kommandoer: Slik fungerer sparse- og blobless-utsjekk
Med denne referanseoppsettet installerer ikke en ny utvikler eller en midlertidig kodeagent-container en egen CLI eller FUSE-driver. Onboarding tar fem standard Git-kommandoer:
1# 1. Klon kontrollplanet på 20 KB med blob-filtrering og sparse-modus aktivert
2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
3cd monorepo
4
5# 2. Last inn repository-aliaser og koble til refs/trunk/*
6git config include.path ../.gitconfig
7git trunk-init
8
9# 3. Deklarer domenekatalogene som trengs for denne oppgaven
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Opprett et isolert arbeidsområde forankret i det siste trunk-snapshotet
13git ws-new alice/optimize-framing
Under panseret gjør hvert steg kun det minimale arbeidet som kreves:
git clone --filter=blob:none --sparselaster ned kontrollplanet på 20 KB pårefs/heads/main.git trunk-initkonfigurerer+refs/trunk/*:refs/remotes/origin/trunk/*og kjører en blobless fetch. Git laster nedtree-metadataene forrefs/trunk/main(typisk noen titalls kilobyte) og null filblobs.git sparse-checkout set net/relay ipc/codecinstruerer Gits cone-mode-indeksfilter om å materialisere kun rotkatalogen og de to undertrærne.git ws-newbytter til en ny gren forankret iorigin/trunk/main. Akkurat i det øyeblikket ber Git på forespørsel kun omblob-objektene inne inet/relayogipc/codec. De resterende 95 % av monorepoet lastes aldri ned til disken.
Fremtidshorisont: Montering av refs/trunk/main som et virtuelt filsystem (VFS)
Sparse-utsjekk med blobless-kloner dekker daglige arbeidsflyter uten ekstra klientverktøy. Fordi refs/trunk/main allerede er et rent Merkle-tre i et Content-Addressable Store, gir arkitekturen også et direkte grunnlag for et valgfritt virtuelt filsystem (VFS) i userspace via FUSE på Linux, FileProvider på macOS eller ProjFS på Windows:
- Umiddelbar montering av hele treet: En lett lokal monteringsdaemon leser rot-
tree-objektet tilrefs/trunk/mainog projiserer hele monorepoets katalogstruktur inn i arbeidskatalogen på millisekunder, slik at man ikke trenger å kjøregit sparse-checkout set. - Blob-henting ved behov: Filinnhold forblir virtuelt til en kompilator, IDE eller agent utfører et
open(2)- ellerread(2)-systemkall. Daemonen henter da den tilhørende SHA-1-blob-en fra CAS-endepunktet og cacher den på lokal NVMe-lagring. - Copy-on-Write-overlegg for arbeidsområder: Lokale endringer skrives til et lett øvre overleggslag. Å kjøre
git ws-savehasher kun de endrede stiene i overlegget, skriver de oppdaterteblob- ogtree-objektene og pusher den resulterende commiten tilrefs/ws/<actor>/<id>uten å skanne umodifiserte kataloger.
Changelist-modellen på ren Git: Sammenslåing i minnet på serversiden
I en tradisjonell Git-arbeidsflyt enten lager sammenslåing av en funksjonsgren en merge-commit med to foreldre (som gjør historikken til en sammenfiltret rangeerstasjon), eller så krever det at utviklere manuelt rebaser og løper om kapp med andre ingeniører som pusher til main.
Inspirert av google3-modellen for Changelist (CL) representerer hvert arbeidsområde i vår arkitektur en enkelt atomisk endring mot trunk-snapshotet. Mens man arbeider lokalt i ws/alice/optimize-framing, kan en utvikler eller agent lage ti inkrementelle commits (wip, fix tests, lint). Når endringen er klar til å landes, komprimerer serveren arbeidsområdets delta til en enkelt lineær trunk-revisjon uten å sjekke ut en arbeidskatalog på CI-kjøreren.
Slik kjører landingsskriptet (land.sh) i minnet ved hjelp av Git 2.40+-plumbing:
1# 1. Finn rot-snapshot-commiten der arbeidsområdet forgrenet seg
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. Beregn det sammenslåtte rottreet i minnet uten utsjekk av arbeidskatalog
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
Se nøye på steg 2:
- Uansett hvor mange lokale commits som er gjort på arbeidsgrenen, sammenligner skriptet kun tre tre-hasher: det opprinnelige baseshapshotet (
$BASE_TREE), det nåværende trunk-snapshotet ($MAIN_TREE) og det endelige arbeidsområdetreet ($WORK_TREE). - Hvis
refs/trunk/maingikk videre i//web/blogmens Alice redigerte//net/relay, beregnergit merge-tree --write-treedet kombinerte rot-treet$NEW_TREEdirekte inne i Git-objektlageret på millisekunder. Ingen filer skrives til kjørerens filsystem. - Til slutt oppdaterer
git push --atomicmed--force-with-leaserefs/trunk/main, legger til én lineær commit irefs/trunk/historyog sletter landingsutløserenrefs/heads/land/<id>i én transaksjon.
Håndheving av arkitekturgrenser: Synlighet i fire nivåer og hierarkisk OWNERS
Å samle kode i ett repository fungerer kun når grensene mellom domener håndheves mekanisk før en commit kan landes. Rett etter at git merge-tree --write-tree har produsert $NEW_TREE (og før git push --atomic oppdaterer trunk), kjører vår policymotor to O(delta)-kontroller over git diff-tree "$MAIN_TREE" "$NEW_TREE".
1. Synlighet for domener i fire nivåer
Rottreet er delt inn i fire synlighetsnivåer:
| Katalogprefiks | Nivå | Tillatte importer | Forbudte importer | OWNERS-kontroll |
|---|---|---|---|---|
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/* | Offentlig produksjon | Offentlig produksjon, //third_party/* | //corp/*, //experimental/* | Håndheves |
//third_party/* | Ekstern leverandør | Selvstendig eller //third_party/* | Alle domener utenom third_party | Håndheves |
//corp/* | Intern privat | Offentlig produksjon, //corp/*, //third_party/* | //experimental/* | Håndheves |
//experimental/<actor>/* | Sandkasse | Enhver pakke utenom corp | //corp/* | Unntatt (automatisk godkjent) |
Hvis en offentlig pakke i //web/* forsøker å importere en intern pakke fra //corp/* eller en uferdig prototype fra //experimental/*, avbryter landingsjobben før den rører refs/trunk/main.
2. Hierarkisk OWNERS-verifisering uten Gerrit-server
Du trenger ikke en dedikert Gerrit-instans for å håndheve katalognivå-OWNERS-filer med arvingsregler:
- For hver sti som er endret mellom
$MAIN_TREEog$NEW_TREE, går verifisereren oppover fra filens katalog til//OWNERS. - Enhver
OWNERS-fil kan deklarereset noparentfor å blokkere godkjenninger arvet fra overordnede kataloger, ellerper-file *.sql=@dba-teamfor å avgrense godkjenninger etter filnavnmønster (glob). - Invariant mot manipulering: Eksisterende
OWNERS-filer leses alltid fra$MAIN_TREE(det nåværende trunk-treet), aldri fra$NEW_TREE. En uautorisert forfatter kan ikke gi seg selv tillatelse ved å redigereOWNERSi samme arbeidsområde-commit. - Unntak for sandkasse: Enhver sti under
//experimental/<actor>/*omgårOWNERS-kontrollene, slik at ingeniører og agenter kan prototype fritt medgit ws-exp <actor> <project>og senere gradere pakken til et produksjonsdomene medgit ws-graduate.
Kodegjennomgang uten friksjon: Midlertidige PR-er og snapshot-differ
Fordi refs/heads/main kun inneholder kontrollplanet og refs/trunk/main er en commit uten forelder, ville det å åpne en standard GitHub Pull Request direkte fra en arbeidsområdegren inn i main forsøke å sammenligne urelaterte historikker. Vi løste dette med tre gjennomgangsarbeidsflyter tilpasset ulike endringer:
- Umiddelbare to-punkts arbeidsområde-differ (
git ws-save+ Edge Diff Router) Å pushe et arbeidsområde medgit ws-saveeksponererrefs/ws/<actor>/<id>gjennom en lett edge-worker. Denne workeren løserrefs/trunk/mainog arbeidsområdereferansen til deres commit-SHA-er og videresender til GitHubs to-punkts sammenligningsvisning (/compare/<trunk-sha>..<ws-sha>). For små patcher eller agentgenererte endringer kan reviewere inspisere diffen og klikke Land direkte i webgrensesnittet. - Midlertidige GitHub Pull Requests (
git ws-pr) For linje-for-linje-gjennomgang på GitHub pushergit ws-prsnapshot-commiten uten forelder ($(git rev-list --max-parents=0 HEAD)) tilrefs/heads/base/<id>og arbeidsområdespissen tilrefs/heads/review/<id>. Deretter åpnes en GitHub Pull Request som sammenlignerreview/<id>medbase/<id>. Fordi basisgrenen er nøyaktig snapshotet arbeidsområdet forgrenet seg fra, viser fanen Files changed på GitHub kun de eksakte linjene som er endret i arbeidsområdet. Når reviewer klikker Merge pull request, forfremmer en webhook hode-commiten tilrefs/heads/land/<id>, kjører den sammenslåtte landingspipelinen i minnet og sletter automatisk båderefs/heads/base/<id>ogrefs/heads/review/<id>. - Eksterne speil-pull requests Open-source-bidragsytere trenger aldri å lære monorepo-arbeidsflyten. De forker og åpner pull requests mot frittstående speilrepositories som vanlig.
Toveis synkronisering i O(delta) og separate produksjonsgrener
Mange organisasjoner publiserer enkeltpakker som frittstående GitHub-repositories slik at eksterne brukere kan kjøre go get, cargo install eller forke et enkelt prosjekt uten å berøre monorepoet. Noen prosjekter, som statiske blogger eller nettapper med utgivelsesporting, vedlikeholder også en egen production-gren ved siden av main.
Vår Starlark-synkroniseringsmotor håndterer både utgående eksporter og innkommende bidrag i O(delta)-tid, proporsjonalt med filene som er endret i en commit, ikke med størrelsen på monorepoet:
- Blobless indeks-splicing: Når et speilrepositorys undertre settes inn i eller fjernes fra monorepoets rottre, setter motoren
GIT_NO_LAZY_FETCH=1og kjørergit update-index --index-infoetterfulgt avgit write-tree --missing-ok. Uendrede undertrær og blobs refereres utelukkende via sine SHA-1-hasher uten å bli lastet ned til CI-kjøreren. - Forhenting av kun delta-blobs: Før en eksportert commit pushes til et frittstående speil, kjører skriptet
git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"og forhenter kun de gamle og nye blobs for de endrede filene viagit cat-file --batch. Git kan da beregne en tynn deltapakke og pushe til speilrepositoryet på sekunder. - Bevaring av workflow- og utgivelsesgrener: Under eksport bevarer synkroniseringsverktøyet hvert frittstående repositorys egen
.github/workflows/-katalog, slik at offentlige CI-kjørere kjører prosjekttester på speilet. Hver workflow i Starlark-konfigurasjonen retter seg mot en eksplisitt gren, sommain, og dette bevarer utgivelsesporter for repositories som deployer fra en separatproduction-gren. Monorepo-landinger synkroniseres med speiletsmain-gren, og automatiske metadata- eller oversettelsescommits påmainsynkroniseres tilbake tilrefs/trunk/mainvia webhook. Å forfremmemaintilproductioner fortsatt et eksplisitt utgivelsessteg.
Avsluttende refleksjoner
Å bygge et monorepo med høy hastighet krever ikke at man forlater standard Git eller drifter en kompleks flåte med skreddersydd infrastrukturserver. Denne arkitekturen behandler Gits objektlager som et Content-Addressable Store, skiller refs/heads/main fra refs/trunk/main og refs/trunk/history, og sammenslår trær i minnet med git merge-tree --write-tree. Som et resultat kan både menneskelige ingeniører og autonome kodeagenter klone et arbeidsområde på under ett sekund og skalere rent på tvers av hundrevis av prosjekter.