GoSuda

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

By Lemon Mint views ...

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:

  1. blob: Råbyteinnholdet i en enkelt fil.
  2. tree: En katalogliste som knytter filnavn og tillatelser til blob- eller underordnede tree-hasher.
  3. 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 hver git clone og git 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:

  1. git clone --filter=blob:none --sparse laster ned kontrollplanet på 20 KB på refs/heads/main.
  2. git trunk-init konfigurerer +refs/trunk/*:refs/remotes/origin/trunk/* og kjører en blobless fetch. Git laster ned tree-metadataene for refs/trunk/main (typisk noen titalls kilobyte) og null filblobs.
  3. git sparse-checkout set net/relay ipc/codec instruerer Gits cone-mode-indeksfilter om å materialisere kun rotkatalogen og de to undertrærne.
  4. git ws-new bytter til en ny gren forankret i origin/trunk/main. Akkurat i det øyeblikket ber Git på forespørsel kun om blob-objektene inne i net/relay og ipc/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:

  1. Umiddelbar montering av hele treet: En lett lokal monteringsdaemon leser rot-tree-objektet til refs/trunk/main og projiserer hele monorepoets katalogstruktur inn i arbeidskatalogen på millisekunder, slik at man ikke trenger å kjøre git sparse-checkout set.
  2. Blob-henting ved behov: Filinnhold forblir virtuelt til en kompilator, IDE eller agent utfører et open(2)- eller read(2)-systemkall. Daemonen henter da den tilhørende SHA-1-blob-en fra CAS-endepunktet og cacher den på lokal NVMe-lagring.
  3. Copy-on-Write-overlegg for arbeidsområder: Lokale endringer skrives til et lett øvre overleggslag. Å kjøre git ws-save hasher kun de endrede stiene i overlegget, skriver de oppdaterte blob- og tree-objektene og pusher den resulterende commiten til refs/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/main gikk videre i //web/blog mens Alice redigerte //net/relay, beregner git merge-tree --write-tree det kombinerte rot-treet $NEW_TREE direkte inne i Git-objektlageret på millisekunder. Ingen filer skrives til kjørerens filsystem.
  • Til slutt oppdaterer git push --atomic med --force-with-lease refs/trunk/main, legger til én lineær commit i refs/trunk/history og sletter landingsutløseren refs/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:

KatalogprefiksNivåTillatte importerForbudte importerOWNERS-kontroll
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*Offentlig produksjonOffentlig produksjon, //third_party/*//corp/*, //experimental/*Håndheves
//third_party/*Ekstern leverandørSelvstendig eller //third_party/*Alle domener utenom third_partyHåndheves
//corp/*Intern privatOffentlig produksjon, //corp/*, //third_party/*//experimental/*Håndheves
//experimental/<actor>/*SandkasseEnhver 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_TREE og $NEW_TREE, går verifisereren oppover fra filens katalog til //OWNERS.
  • Enhver OWNERS-fil kan deklarere set noparent for å blokkere godkjenninger arvet fra overordnede kataloger, eller per-file *.sql=@dba-team for å 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 å redigere OWNERS i samme arbeidsområde-commit.
  • Unntak for sandkasse: Enhver sti under //experimental/<actor>/* omgår OWNERS-kontrollene, slik at ingeniører og agenter kan prototype fritt med git ws-exp <actor> <project> og senere gradere pakken til et produksjonsdomene med git 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:

  1. Umiddelbare to-punkts arbeidsområde-differ (git ws-save + Edge Diff Router) Å pushe et arbeidsområde med git ws-save eksponerer refs/ws/<actor>/<id> gjennom en lett edge-worker. Denne workeren løser refs/trunk/main og 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.
  2. Midlertidige GitHub Pull Requests (git ws-pr) For linje-for-linje-gjennomgang på GitHub pusher git ws-pr snapshot-commiten uten forelder ($(git rev-list --max-parents=0 HEAD)) til refs/heads/base/<id> og arbeidsområdespissen til refs/heads/review/<id>. Deretter åpnes en GitHub Pull Request som sammenligner review/<id> med base/<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 til refs/heads/land/<id>, kjører den sammenslåtte landingspipelinen i minnet og sletter automatisk både refs/heads/base/<id> og refs/heads/review/<id>.
  3. 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=1 og kjører git update-index --index-info etterfulgt av git 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 via git 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, som main, og dette bevarer utgivelsesporter for repositories som deployer fra en separat production-gren. Monorepo-landinger synkroniseres med speilets main-gren, og automatiske metadata- eller oversettelsescommits på main synkroniseres tilbake til refs/trunk/main via webhook. Å forfremme main til production er 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.