GoSuda

Ny vurdering av Git-monorepoet: En CAS-nativ arkitektur for mennesker og Coding Agents

By Lemon Mint views ...

Enhver voksende ingeniørorganisasjon står til slutt overfor et valg mellom mange repositorier og ett. Man starter med titalls små repositorier, og hverdagen føles enkel i begynnelsen. Hvert bibliotek har sin egen saksliste, sin egen CI-pipeline og en git clone som fullføres på sekunder. Så endrer du en struct i en sentral nettverkspakke og bruker resten av ettermiddagen på å åpne pull requests for versjonsoppdatering i seks nedstrøms repositorier, mens du venter på at CI-køene tømmes i topologisk rekkefølge.

Flytter du alt over i et tradisjonelt Git-monorepo, bytter du koordinering på tvers av repositorier mot størrelse på repositoriet. Etter hvert som commit-historikken vokser og binære ressurser hoper seg opp i tidligere revisjoner, blir git fetch tregere. Arbeidstrærne sveller opp til flere gigabyte. Snart foreslår noen å sette opp en egendesignet daemon for virtuelt filsystem, en dedikert server for kodegjennomgang eller en flåte med eksterne byggearbeidere, bare for å gjøre den daglige utviklingen utholdelig.

Når autonome kodeagenter blir en del av teamet, forverres begge feilmodusene. Polyrepoer gjør at en agent ikke ser kallere på tvers av prosjekter, og tvinger den til å jonglere med legitimasjon på tvers av repositorier. Tradisjonelle monorepoer overbelaster agentens containerdisk og nettverksbåndbredde ved hver oppgaveinitialisering, mens samtidige agenter som trår på hverandre på samme gren skaper endeløse rebase-kappløp.

Inspirert av google3-arkitekturen har vi bygget atomiske endringer på én trunk, sparsom onboarding på under ett sekund, hierarkisk styring med OWNERS, og frittstående åpen kildekode-speilrepositorier, med standard Git-infrastruktur og objektlagring basert på standardmaskinvare.

Designet krever verken en egendesignet filsystemdaemon eller en tredjeparts apparat for gjennomgang, fordi det behandler Git som 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. I .git/objects lagrer Git tre uforanderlige primitiver, nøklet etter innholdshashen i SHA-1 (eller SHA-256):

  1. blob: Det rå byteinnholdet i én enkelt fil.
  2. tree: En katalogliste som knytter filnavn og rettigheter til hasher for blob eller underordnede tree.
  3. commit: En liten tekstheader (vanligvis 200 til 300 byte) som peker til én rot-tree-hash, null eller flere foreldre-commit-hasher og metadata om forfatteren.

Hvis et monorepo inneholder 50 000 filer og du redigerer én enkelt linje i én fil, gjenbruker den nye commit-en de nøyaktige blob- og tree-hashene for de øvrige 49 999 filene.

Et tradisjonelt Git-monorepo føles tungt fordi konvensjonen belaster én enkelt referanse, refs/heads/main, med tre motstridende oppgaver:

  • Den inneholder hele det samlede katalogtreet for alle prosjekter.
  • Den drar med seg hele den foreldrekoblede 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 klienten og serveren om tilgjengeligheten i commit-grafen på tvers av tusenvis av historiske revisjoner. Hvis noen for tre år siden ved et uhell committet et arkiv på 100 MB og slettet det dagen etter, henger fortsatt dets blob på et historisk tree som kan nås fra refs/heads/main.

Separering av referansenavnerommet

Gits wire-protokoll annonserer kun refs/heads/* og refs/tags/* under en standard kloning. 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 medfører null overhead ved standardkloninger.

Å dele disse oppgavene på separate referanser i ett enkelt repositorium fjerner flaskehalsen:

1refs/heads/main               Kun kontrollplan (~20 KB: regler, .gitconfig, synkroniseringskonfigurasjon, skript)
2refs/trunk/main               Foreldreløs snapshot-commit som peker til det nyeste samlede rot-treet
3refs/trunk/history            Lineær, foreldrekoblet commit-kjede som deler nøyaktig rot-tree SHA-1
4refs/ws/<actor>/<id>          Isolerte arbeidsområde-snapshots (changelists) per utvikler eller agent
5refs/ws/cl/<change-id>/<tip>  Uforanderlige patchsets: hver tip av en endring sendt til gjennomgang
6refs/heads/land/<id>          Midlertidig utløser for merge-kø, slettes umiddelbart etter landing

1. Kontrollplanet (refs/heads/main)

Standardgrenen inneholder ingen applikasjonskildekode. Den inneholder kun driftsreglene for repositoriet (AGENTS.md, README.md), en delt .gitconfig med aliaser for arbeidsflyter, Starlark-synkroniseringskonfigurasjonen (sync.star) og skript for sammenslåing på serversiden. Kloning av standardgrenen overfører omtrent 20 KB og fullføres på millisekunder.

2. Det foreldreløse trunk-snapshotet (refs/trunk/main)

refs/trunk/main peker til det komplette, samlede kildetreet for hele organisasjonen (//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), men commit-objektet opprettes uten et -p-foreldreflagg.

Siden refs/trunk/main har null foreldre (parents = []), krever henting av den ingen forhandling om commit-grafen. Git-serveren går ikke gjennom historikken; den sender den ene hodecommit-en og dens nåværende katalog-tree-objekter. Historiske binærfiler eller filer slettet forrige måned kan ikke nås fra refs/trunk/main, og de overføres aldri.

3. Den lineære revisjonshistorikken (refs/trunk/history)

Utviklere trenger likevel git blame og git log når de undersøker regresjoner. Hver gang en endring landes, oppretter sammenslåingsskriptet to commit-objekter som peker til nøyaktig samme rot-$NEW_TREE-SHA-1-hash:

 1# Foreldreløs snapshot-commit for umiddelbare utsjekkinger av arbeidsområder med dybde 1
 2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
 3
 4# Foreldrekoblet commit 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"

Å beholde både refs/trunk/main og refs/trunk/history fordobler ikke lagringsbehovet. Fordi begge commit-ene refererer til den identiske $NEW_TREE-hashen, dedupliserer Gits content-addressable store 100 % av tree- og blob-objektene. Den ekstra lagringskostnaden for det foreldreløse snapshotet er en enkelt commit-header på 250 byte.

4. Isolerte arbeidsområder (refs/ws/<actor>/<id>)

Grener under arbeid pushes til refs/ws/<actor>/<id> i stedet for refs/heads/*. Hundre kodeagenter kan lagre sjekkpunkter hvert minutt uten å forurense nedtrekkslistene for grener eller gjøre git fetch tregere for menneskelige ingeniører.

Onboarding i fire steg: Slik fungerer sparsom utsjekking uten blobs

Med denne referanseoppsettet trenger ikke en ny utvikler eller en midlertidig kodeagent-container å installere et egendefinert CLI eller en FUSE-driver. Onboarding tar fire steg med standard Git og repositoriets aliaser:

 1# 1. Klon 20 KB-kontrollplanet med blob-filtrering og sparsom modus aktivert
 2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
 3cd monorepo
 4
 5# 2. Last repositoriets 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 nyeste trunk-snapshotet
13git ws-new alice/optimize-framing

Hvert steg henter kun det det trenger:

  1. git clone --filter=blob:none --sparse laster ned 20 KB-kontrollplanet på refs/heads/main.
  2. git trunk-init konfigurerer +refs/trunk/*:refs/remotes/origin/trunk/* og kjører en henting uten blobs. Git laster ned tree-metadataene for refs/trunk/main (vanligvis noen titalls kilobyte) og null filblobs.
  3. git sparse-checkout set net/relay ipc/codec instruerer Gits indeksfilter i cone-modus om kun å materialisere rotkatalogen og de to undertrærne.
  4. git ws-new bytter til en ny gren forankret i origin/trunk/main. Git ber deretter kun på forespørsel om blob-objektene inne i net/relay og ipc/codec. De resterende 95 % av monorepoet lastes aldri ned til disken.

Fremtidig horisont: Montering av refs/trunk/main som et virtuelt filsystem (VFS)

Sparsom utsjekking med kloner uten blobs 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 brukerrommet 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 katalogstrukturen i monorepoet inn i arbeidskatalogen på millisekunder, slik at det ikke er nødvendig å kjøre git sparse-checkout set.
  2. Blob-henting på forespørsel: Filinnhold forblir virtuelt inntil en kompilator, IDE eller agent utfører et open(2)- eller read(2)-systemkall. Daemonen henter deretter den tilsvarende SHA-1-blob-en fra CAS-endepunktet og cacher den på lokal NVMe-lagring.
  3. Copy-on-Write-overlay for arbeidsområder: Lokale endringer skrives til et lett øvre overlay-lag. Kjøring av git ws-save hasher kun de endrede stiene i dette overlay-laget, skriver de oppdaterte blob- og tree-objektene, og pusher den resulterende commit-en til refs/ws/<actor>/<id> uten å skanne uendrede kataloger.

Changelist-modellen på ren Git: Landing i minnet på serversiden

I en tradisjonell Git-arbeidsflyt skaper sammenslåing av en funksjonsgren enten en sammenslåingscommit med to foreldre, noe som gjør historikken ikke-lineær, eller utviklere må rebase manuelt og kappes med samtidige pushes til main.

Inspirert av google3-modellen for Changelist (CL) representerer hvert arbeidsområde i arkitekturen vår en enkelt atomisk endring mot trunk-snapshotet. Mens en utvikler eller agent arbeider lokalt i ws/alice/optimize-framing, kan den lage ti inkrementelle commits (wip, fix tests, lint). Når endringen er klar til å landes, komprimerer serveren arbeidsområdets delta til én enkelt lineær trunk-revisjon uten å sjekke ut en arbeidskatalog på CI-kjøreren.

Slik utføres landingsskriptet (land.sh) i minnet ved hjelp av Git 2.40+ infrastruktur:

 1# 1. Finn rot-snapshot-commit-en 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 rot-treet i minnet uten utsjekking av et arbeidstre
 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

I steg 2:

  • Uansett hvor mange lokale commits som er gjort på arbeidsområdegrenen, sammenligner skriptet kun tre tre-hasher: det opprinnelige basissnapshotet ($BASE_TREE), det nåværende trunk-snapshotet ($MAIN_TREE) og det endelige arbeidsområdetreet ($WORK_TREE).
  • Hvis refs/trunk/main ble oppdatert i //web/blog mens Alice redigerte //net/relay, beregner git merge-tree --write-tree det kombinerte rot-treet $NEW_TREE direkte i Git-objektlageret på millisekunder, uten å skrive filer til kjøreren filsystem.
  • Etter at policykontrollene nedenfor er bestått, oppdaterer ett git push --atomic med --force-with-lease refs/trunk/main og legger til én lineær commit i refs/trunk/history, og utløseren for landing refs/heads/land/<id> slettes.

Håndheving av arkitekturgrenser: Synlighet i fire nivåer og hierarkiske OWNERS

Å samle kode i ett repositorium fungerer kun dersom 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. Domenesynlighet i fire nivåer

Rot-treet 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 (godkjennes automatisk)

Hvis en offentlig pakke i //web/* forsøker å importere en intern pakke fra //corp/* eller en ikke-uteksaminert prototype fra //experimental/*, avbrytes landingsjobben før refs/trunk/main berøres.

2. Hierarkisk verifisering av OWNERS uten en Gerrit-server

Verifikatoren håndhever OWNERS-filer på katalognivå med arvingsregler:

  • For hver sti som er endret mellom $MAIN_TREE og $NEW_TREE, går verifikatoren oppover fra filens katalog til //OWNERS.
  • Enhver OWNERS-fil kan deklarere set noparent for å blokkere arvede godkjenninger fra overordnede kataloger, eller per-file *.sql=@dba-team for å avgrense godkjenninger etter filnavnmønster.
  • 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 sandkassen: Enhver sti under //experimental/<actor>/* omgår OWNERS-kontrollene, slik at ingeniører og agenter fritt kan lage prototyper med git ws-exp <actor> <project> og senere uteksaminere 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 foreldreløs commit, ville åpning av 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ådediffer (git ws-save + Edge Diff Router) Ved å pushe et arbeidsområde med git ws-save gjøres refs/ws/<actor>/<id> tilgjengelig 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 gjennomgåere inspisere diffen og klikke på Land direkte i webgrensesnittet.
  2. Midlertidige GitHub Pull Requests (git ws-pr) For gjennomgang linje for linje på GitHub pusher git ws-pr rot-snapshot-commit-en ($(git rev-list --max-parents=0 HEAD)) til refs/heads/base/<id> og arbeidsområdetippen til refs/heads/review/<id>. Deretter åpnes en GitHub Pull Request som sammenligner review/<id> med base/<id>. Fordi basegrenen er snapshotet som arbeidsområdet forgrenet seg fra, viser GitHubs fane Files changed kun arbeidsområdets delta. Når gjennomgåeren klikker Merge pull request, flytter en webhook hodecommit-en til refs/heads/land/<id>, kjører landingspipelinen i minnet, og sletter automatisk både refs/heads/base/<id> og refs/heads/review/<id>.
  3. Pull requests til eksterne speil Bidragsytere til åpen kildekode trenger aldri å lære monorepo-arbeidsflyten. De forker og åpner pull requests mot frittstående speilrepositorier, slik de vanligvis gjør.

Endringer, patchsets og videreføring av godkjenninger

Et grennavn kan ikke identifisere en endring. En agent kan gi nytt navn til grenen sin, endre tippen, eller rebase på en nyere trunk flere ganger før landing. Som i Gerrit identifiserer arbeidsflyten en endring med en Change-Id: I<40 hex>-trailer i tippens commit-melding. git ws-save, git ws-pr og git ws-land legger til traileren når tippen mangler den, og beholder den ellers, slik at id-en overlever amend, rebase og omdøping. Den landede trunk-commit-en beholder traileren og registrerer nummeret på sin gjennomgangs-pull-request, slik at git trunk-cl <number|Change-Id> finner den i refs/trunk/history.

Hver tip som sendes til gjennomgang blir et patchset. git ws-pr pusher den til den uforanderlige referansen refs/ws/cl/<change-id>/<tip> ved siden av gjennomgangsgrenene. Å kjøre git ws-pr på nytt etter en amend eller en git ws-sync legger til et patchset i samme pull request. git ws-ps lister patchsettene til den nåværende endringen, og git ws-psdiff <a> <b> skriver ut interdiffen mellom to av dem, slik at en gjennomgåer kun leser det som har endret seg siden forrige runde.

GitHub knytter en godkjenning til én enkelt commit, så hver rebase ville normalt kreve en ny gjennomgang. Landingspipelinen aksepterer også en godkjenning gitt til et annet patchset i samme endring når begge patchsettene gjør samme patch mot sitt eget basissnapshot. Den sammenligner git patch-id --verbatim over hele den binære diffen. En ren rebase på en nyere trunk lander uten ny gjennomgang, mens enhver innholdsendring, inkludert en endring av mellomrom eller filmodus, krever en ny godkjenning.

Toveis synkronisering i O(delta) og separate produksjonsgrener

Mange organisasjoner publiserer enkeltpakker som frittstående GitHub-repositorier, slik at eksterne brukere kan go get, cargo install eller forke ett enkelt prosjekt uten å berøre monorepoet. Noen prosjekter, for eksempel statiske blogger eller webapplikasjoner med utgivelsesporter, 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, proporsjonal med filene som er endret i en commit i stedet for størrelsen på monorepoet:

  • Indeksskjøting uten blobs: Når motoren skjøter et speilrepositorys undertre inn i eller ut av monorepoets rot-tre, setter den 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 forblir referert med SHA-1 og lastes aldri ned til CI-kjøreren.
  • Forhenting av blobs kun for delta: 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 henter forhånd kun de gamle og nye blobs for de endrede filene via git cat-file --batch. Git kan deretter beregne en tynn deltapakke og pushe til speilrepositoriet på sekunder.
  • Bevaring av arbeidsflyter og utgivelsesgrener: Under utgående eksport bevarer synkroniseringsverktøyet hvert frittstående repositorys egen .github/workflows/-katalog, slik at offentlige CI-kjørere kjører prosjekttestene på speilet. Hver arbeidsflyt i Starlark-konfigurasjonen retter seg mot en eksplisitt gren, for eksempel main, noe som bevarer utgivelsesportene for repositorier som deployer fra en egen production-gren. Landinger i monorepoet synkroniseres med speilets main-gren, og automatiserte metadata- eller oversettelses-commits på main synkroniseres tilbake til refs/trunk/main via webhook. Å flytte main til production forblir et eksplisitt utgivelsessteg.

Avsluttende refleksjoner

Denne arkitekturen behandler Gits objektlager som et Content-Addressable Store, skiller refs/heads/main fra refs/trunk/main og refs/trunk/history, identifiserer hver endring med en Change-Id og uforanderlige patchsets, og slår sammen trær i minnet med git merge-tree --write-tree. Onboardingkostnaden øker med katalogene en oppgave trenger, og kostnaden for landing og speilsynkronisering øker med størrelsen på en endring, ikke med størrelsen på repositoriet.