Gentænkning af Git-monorepoen: En CAS-native arkitektur til mennesker og kodningsagenter

Enhver voksende ingeniørorganisation rammer til sidst det samme vejskel. Start med snesevis af små repositories, og livet føles enkelt i starten. Hvert bibliotek har sin egen issue tracker, sin egen CI-pipeline og en git clone, der er færdig, før du når at tage en slurk kaffe. Så indfinder skatten sig. Du ændrer en struct i en central netværkspakke og bruger resten af eftermiddagen på at åbne version-bump pull requests i seks nedstrøms repositories, mens du venter på, at CI-køerne tømmes i topologisk rækkefølge.
Flytter man alt ind i et traditionelt Git-monorepo, bytter man koordineringssmerter ud med fysiksmerter. Efterhånden som commit-historikken vokser, og binære aktiver ophobes i tidligere revisioner, bliver git fetch langsommere. Arbejdstræerne svulmer op til gigabytes. Før længe foreslår nogen at opsætte en tilpasset virtuel filsystemdæmon, en dedikeret kodegennemgangsserver eller en flåde af eksterne build-workers blot for at holde den daglige udvikling til at fungere.
Når autonome kodeagenter kommer ind i teamet, forværres begge fejltilstande. Polyrepos blænder en agent for callers på tværs af projekter og tvinger den til at jonglere med legitimationsoplysninger på tværs af repositories. Traditionelle monorepos drukner agentens containerdisk og netværksbåndbredde ved hver opgaveinitialisering, mens samtidige agenter, der træder på den samme branch, skaber endeløse rebase-kapløb.
Vi ønskede en tredje mulighed. Inspireret af google3-arkitekturen stillede vi et enkelt spørgsmål: Kan vi opnå atomiske ændringer på en enkelt trunk, sparsom onboarding på under et sekund, hierarkisk OWNERS-styring og selvstændige open source-spejle ved kun at bruge standard Git-infrastruktur og billig objektlagring?
Svaret viste sig at være ja, og det krævede hverken et tilpasset filsystemdæmon eller en tredjepartsgennemgangsappliance. Det krævede blot, at man brugte Git til det, det faktisk er under motorhjelmen: et Merkle-træ-baseret Content-Addressable Store (CAS).
Misforståelsen i hjertet af langsomme Git-monorepos
Når udviklere klager over, at Git ikke skalerer til store monorepos, rammer de sjældent en grænse i Gits objektdatabase. Inde i .git/objects gemmer Git tre uforanderlige primitiver, nøglet efter deres SHA-1 (eller SHA-256) indholdshash:
blob: De rå bytes i en enkelt fil.tree: En mappeliste, der kortlægger filnavne og rettigheder tilblob- eller underordnedetree-hashes.commit: En lille tekstheader (typisk 200 til 300 bytes), der peger på en enkelt rod-tree-hash, nul eller flere forældre-commit-hashes og forfattermetadata.
Som et content-addressable store er dette design bemærkelsesværdigt effektivt. Hvis et monorepo indeholder 50.000 filer, og du redigerer en enkelt linje i én fil, genbruger den nye commit de nøjagtige blob- og tree-hashes for de øvrige 49.999 filer.
Hvorfor føles et traditionelt Git-monorepo så tungt? Fordi konventionen overbelaster en enkelt reference, refs/heads/main, med tre modstridende opgaver:
- Den rummer hele det samlede mappetræ for hvert projekt.
- Den trækker hele den forældrelinkede kæde af historiske commits tilbage til første dag.
- Den ligger inde i
refs/heads/*, navnerummet som Git-servere annoncerer og forhandler om som standard ved hvergit cloneoggit fetch.
Hver gang en udvikler eller CI-runner henter refs/heads/main, forhandler klienten og serveren om commit-grafens tilgængelighed på tværs af tusindvis af historiske revisioner. Hvis nogen ved en fejl committede et 100 MB arkiv for tre år siden og slettede det dagen efter, hænger dets blob stadig på et historisk tree, der kan nås fra refs/heads/main.
Opdeling af referencenavnerummet i fire lag
Gits wire-protokol annoncerer kun refs/heads/* og refs/tags/* under en standardkloning. Referencer under tilpassede navnerum, såsom refs/trunk/* eller refs/ws/*, ligger i præcis samme objektlager på serveren, drager fordel af den samme packfile-deltakomprimering og kan hentes eller pushes eksplicit, men de pålægger ingen overhead på standardkloninger.
Ved at opdele ansvar på tværs af fire referencenavnerum inde i et enkelt repository forsvinder flaskehalsen:
1refs/heads/main Kun kontrolplan (~20 KB: regler, .gitconfig, synkroniseringskonfiguration, scripts)
2refs/trunk/main Parentless snapshot-commit, der peger på det seneste samlede rodtræ
3refs/trunk/history Lineær, forældrelinket commit-kæde, der deler det nøjagtige rodtræ-SHA-1
4refs/ws/<actor>/<id> Isolerede arbejdsområde-snapshots (changelists) pr. udvikler eller agent
5refs/heads/land/<id> Kortvarig merge-queue-udløser, slettet umiddelbart efter landing
1. Kontrolplanet (refs/heads/main)
Standardbranchen indeholder ingen applikationskildekode. Den indeholder kun repositoryets driftsregler (AGENTS.md, README.md), en delt .gitconfig med workflow-aliaser, Starlark-synkroniseringskonfigurationen (sync.star) og serverside merge-scripts. En kloning af standardbranchen overfører cirka 20 KB og er færdig på millisekunder.
2. Det forældreløse trunk-snapshot (refs/trunk/main)
refs/trunk/main peger på det komplette, samlede kildetræ for hele organisationen (//web/*, //net/*, //ipc/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), men dets commit-objekt oprettes uden et -p-forældreflag.
Fordi refs/trunk/main har nul forældre (parents = []), kræver hentning af den ingen forhandling af commit-grafen. Git-serveren gennemløber ikke historikken; den sender kun den enkelte head-commit og dens aktuelle mappe-tree-objekter. Historiske binærfiler eller filer slettet for en måned siden kan ikke nås fra refs/trunk/main og overføres aldrig.
3. Den lineære revisionshistorik (refs/trunk/history)
Udviklere har stadig brug for git blame og git log, når de undersøger regressioner. Hver gang en ændring lander, opretter merge-scriptet to commit-objekter, der peger på den nøjagtigt samme rod-$NEW_TREE-SHA-1-hash:
1# Parentless snapshot-commit til øjeblikkelige depth-1 checkouts af arbejdsområder
2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
3
4# Forældrelinket commit til 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"
Fordobler vedligeholdelsen af både refs/trunk/main og refs/trunk/history repositoryets lagerfodaftryk? Slet ikke. Fordi begge commits refererer til den identiske $NEW_TREE-hash, deduplikerer Gits content-addressable store 100 % af tree- og blob-objekterne. Den ekstra lagerudgift for det forældreløse snapshot er blot en enkelt commit-header på 250 bytes.
4. Isolerede arbejdsområder (refs/ws/<actor>/<id>)
Arbejdsbranches under udvikling pushes til refs/ws/<actor>/<id> frem for refs/heads/*. Hundrede kodeagenter kan gemme checkpoints hvert minut uden at forurene branch-dropdown-menuerne eller bremse git fetch for menneskelige ingeniører.
Onboarding på fem kommandoer: Sådan fungerer sparsom checkout med blobless
Med denne referencelayout installerer en ny udvikler eller en midlertidig kodeagent-container ikke en tilpasset CLI eller FUSE-driver. Onboarding består af fem standard Git-kommandoer:
1# 1. Klon 20 KB-kontrolplanet med blob-filtrering og sparsom tilstand aktiveret
2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
3cd monorepo
4
5# 2. Indlæs repositoryets aliaser og tilknyt refs/trunk/*
6git config include.path ../.gitconfig
7git trunk-init
8
9# 3. Angiv de domænemapper, der er nødvendige for denne opgave
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Opret et isoleret arbejdsområde forankret i det seneste trunk-snapshot
13git ws-new alice/optimize-framing
Under motorhjelmen udfører hvert trin kun det minimale nødvendige arbejde:
git clone --filter=blob:none --sparsehenter 20 KB-kontrolplanet pårefs/heads/main.git trunk-initkonfigurerer+refs/trunk/*:refs/remotes/origin/trunk/*og kører en blobless hentning. Git hentertree-metadataene forrefs/trunk/main(typisk nogle få snesevis af kilobytes) og nul fil-blobs.git sparse-checkout set net/relay ipc/codecinstruerer Gits cone-mode indeksfilter om kun at materialisere rodmappen og de to undertræer.git ws-newskifter til en ny branch forankret iorigin/trunk/main. Netop på dette tidspunkt anmoder Git efter behov kun omblob-objekterne inde inet/relayogipc/codec. De resterende 95 % af monorepoet hentes aldrig til disken.
Fremtidshorisont: Montering af refs/trunk/main som et virtuelt filsystem (VFS)
Sparsom checkout med blobless-kloninger dækker de daglige arbejdsgange uden ekstra klientværktøj. Fordi refs/trunk/main allerede er et rent Merkle-træ i et Content-Addressable Store, giver arkitekturen også et direkte grundlag for et valgfrit userspace virtuelt filsystem (VFS) via FUSE på Linux, FileProvider på macOS eller ProjFS på Windows:
- Øjeblikkelig montering af hele træet: En letvægts lokal monteringsdæmon læser rod-
tree-objektet forrefs/trunk/mainog projicerer hele monorepoets mappehierarki ind i arbejdsmappen på millisekunder, så det ikke længere er nødvendigt at køregit sparse-checkout set. - Blob-hentning ved behov: Filindhold forbliver virtuelt, indtil en compiler, IDE eller agent udfører et
open(2)- ellerread(2)-systemkald. Dæmonen henter derefter den tilsvarende SHA-1blobfra CAS-endpointet og cacher den på lokal NVMe-lagring. - Copy-on-Write-overlay til arbejdsområder: Lokale redigeringer skrives til et letvægts øvre overlay-lag. Kørsel af
git ws-savehashes kun de ændrede stier i det overlay, skriver de opdateredeblob- ogtree-objekter og pusher den resulterende commit tilrefs/ws/<actor>/<id>uden at scanne umodificerede mapper.
Changelist-modellen på ren Git: Landing i hukommelsen på serversiden
I en traditionel Git-arbejdsgang opretter sammenfletning af en feature-branch enten en merge-commit med to forældre (hvilket gør historikken til et rodet rangerbanegård) eller kræver, at udviklere manuelt rebaser og kapløber med andre ingeniører, der pusher til main.
Inspireret af google3-modellen for Changelists (CL) repræsenterer hvert arbejdsområde i vores arkitektur en enkelt atomisk ændring i forhold til trunk-snapshottet. Mens en udvikler eller agent arbejder lokalt i ws/alice/optimize-framing, kan der laves ti inkrementelle commits (wip, fix tests, lint). Når ændringen er klar til at lande, kollapser serveren arbejdsområdets delta til én lineær trunk-revision uden nogensinde at checke et arbejdstræ ud på CI-runneren.
Sådan udføres landing-scriptet (land.sh) i hukommelsen ved hjælp af Git 2.40+ plumbing:
1# 1. Identificér rodsnapshot-commit'en, hvor arbejdsområdet forgrenede sig
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 sammenflettede rodtræ i hukommelsen uden checkout af et arbejdstræ
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ærmere på trin 2:
- Uanset hvor mange lokale commits der er lavet på arbejdsområdets branch, sammenligner scriptet kun tre træ-hashes: det oprindelige basissnapshot (
$BASE_TREE), det aktuelle trunk-snapshot ($MAIN_TREE) og det endelige arbejdsområdetræ ($WORK_TREE). - Hvis
refs/trunk/mainer rykket frem i//web/blog, mens Alice redigerede//net/relay, beregnergit merge-tree --write-treedet kombinerede rodtræ$NEW_TREEdirekte inde i Git-objektlageret på millisekunder. Der skrives ingen filer til runnerens filsystem. - Til sidst opdaterer
git push --atomicmed--force-with-leaserefs/trunk/main, tilføjer én lineær commit tilrefs/trunk/historyog sletter landing-udløserenrefs/heads/land/<id>i én transaktion.
Håndhævelse af arkitekturgrænser: Firelags-synlighed og hierarkiske OWNERS
Konsolidering af kode i ét repository fungerer kun, hvis grænserne mellem domæner håndhæves mekanisk, før en commit kan lande. Straks efter at git merge-tree --write-tree har produceret $NEW_TREE (og før git push --atomic opdaterer trunk), kører vores policy-motor to O(delta)-kontroller over git diff-tree "$MAIN_TREE" "$NEW_TREE".
1. Firelags-domænesynlighed
Rodtræet er opdelt i fire synlighedsniveauer:
| Mappeprefiks | Niveau | Tilladte importer | Forbudte importer | OWNERS-gate |
|---|---|---|---|---|
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/* | Offentlig produktion | Offentlig produktion, //third_party/* | //corp/*, //experimental/* | Håndhævet |
//third_party/* | Ekstern leverandør | Selvstændig eller //third_party/* | Alle ikke-third_party-domæner | Håndhævet |
//corp/* | Intern privat | Offentlig produktion, //corp/*, //third_party/* | //experimental/* | Håndhævet |
//experimental/<actor>/* | Sandkasse | Enhver ikke-corp-pakke | //corp/* | Undtaget (automatisk godkendt) |
Hvis en offentlig pakke i //web/* forsøger at importere en intern pakke fra //corp/* eller en ikke-færdiggjort prototype fra //experimental/*, afbrydes landing-jobbet, før refs/trunk/main berøres.
2. Hierarkisk OWNERS-verifikation uden en Gerrit-server
Du behøver ikke en dedikeret Gerrit-instans for at håndhæve mappeniveau-OWNERS-filer med arvereglar:
- For hver sti, der er ændret mellem
$MAIN_TREEog$NEW_TREE, går verifikatoren opad fra filens mappe til//OWNERS. - Enhver
OWNERS-fil kan erklæreset noparentfor at blokere nedarvede godkendelser fra overordnede mapper, ellerper-file *.sql=@dba-teamfor at afgrænse godkendelser efter filnavnsmønster. - Anti-manipulationsinvariant: Eksisterende
OWNERS-filer læses altid fra$MAIN_TREE(det aktuelle trunk-træ), aldrig fra$NEW_TREE. En uautoriseret forfatter kan ikke give sig selv tilladelse ved at redigereOWNERSi samme arbejdsområdecommit. - Undtagelse for sandkasse: Enhver sti under
//experimental/<actor>/*omgårOWNERS-kontroller, så ingeniører og agenter frit kan prototype medgit ws-exp <actor> <project>og senere overføre pakken til et produktionsdomæne medgit ws-graduate.
Kodegennemgang uden friktion: Midlertidige PR'er og snapshot-diffs
Fordi refs/heads/main kun rummer kontrolplanet, og refs/trunk/main er en forældreløs commit, ville det at åbne en standard GitHub Pull Request direkte fra en arbejdsområdebranch til main forsøge at sammenligne ikke-relaterede historikker. Vi løste dette med tre gennemgangsarbejdsgange, skræddersyet til forskellige ændringer:
- Øjeblikkelige to-punkts arbejdsområde-diffs (
git ws-save+ Edge Diff Router) Ved at pushe et arbejdsområde medgit ws-saveeksponeresrefs/ws/<actor>/<id>via en letvægts edge-worker. Denne worker oversætterrefs/trunk/mainog arbejdsområdereferencen til deres commit-SHA'er og viderestiller til GitHubs to-punkts sammenligningsvisning (/compare/<trunk-sha>..<ws-sha>). Ved små rettelser eller agentgenererede ændringer kan reviewere inspicere diffen og klikke på Land direkte i webgrænsefladen. - Midlertidige GitHub Pull Requests (
git ws-pr) Til linje-for-linje-gennemgang på GitHub pushergit ws-prrodsnapshot-commit'en ($(git rev-list --max-parents=0 HEAD)) tilrefs/heads/base/<id>og arbejdsområdets spids tilrefs/heads/review/<id>. Derefter åbnes en GitHub Pull Request, der sammenlignerreview/<id>medbase/<id>. Fordi basisbranchen er præcis det snapshot, arbejdsområdet forgrenede sig fra, viser GitHubs fane Files changed kun de nøjagtige linjer, der er ændret i arbejdsområdet. Når revieweren klikker på Merge pull request, forfremmer en webhook head-commit'en tilrefs/heads/land/<id>, kører landing-pipelinen i hukommelsen og sletter automatisk båderefs/heads/base/<id>ogrefs/heads/review/<id>. - Eksterne spejl-Pull Requests Open source-bidragydere behøver aldrig at lære monorepo-arbejdsgangen. De forker og åbner pull requests mod selvstændige spejl-repositories som vanligt.
O(delta) tovejs-synkronisering og separate produktionsbranches
Mange organisationer udgiver enkelte pakker som selvstændige GitHub-repositories, så eksterne brugere kan go get, cargo install eller forke et enkelt projekt uden at røre monorepoet. Nogle projekter, såsom statiske blogs eller webapps med releasekontrol, vedligeholder også en separat production-branch ved siden af main.
Vores Starlark-synkroniseringsmotor håndterer både udgående eksporter og indgående bidrag i O(delta)-tid, proportional med de filer, der er ændret i en commit, snarere end med monorepoets størrelse:
- Blobless indeksskarøvning: Når et spejl-repositorys undertræ skarøves ind i eller ud af monorepoets rodtræ, sætter motoren
GIT_NO_LAZY_FETCH=1og kørergit update-index --index-infoefterfulgt afgit write-tree --missing-ok. Uændrede undertræer og blobs refereres udelukkende via deres SHA-1-hashes uden nogensinde at blive hentet til CI-runneren. - Forhentning af kun delta-blobs: Før en eksporteret commit pushes til et selvstændigt spejl, kører scriptet
git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"og forhenter kun de gamle og nye blobs for de ændrede filer viagit cat-file --batch. Git kan derefter beregne en tynd delta-pack og pushe til spejl-repositoryet på få sekunder. - Bevarelse af workflow- og releasebranches: Under udgående eksport bevarer synkronisatoren hvert selvstændigt repositorys egen
.github/workflows/-mappe, så offentlige CI-runners kører projekttests på spejlet. Hver workflow i Starlark-konfigurationen peger på en eksplicit branch, såsommain, hvilket bevarer releasekontrollen for repositories, der deployer fra en separatproduction-branch. Monorepo-landinger synkroniseres med spejletsmain-branch, og automatiserede metadata- eller oversættelses-commits påmainsynkroniseres tilbage tilrefs/trunk/mainvia webhook. Forfremmelse afmaintilproductionforbliver et eksplicit releasetrin.
Afsluttende bemærkninger
At bygge et monorepo med høj hastighed kræver ikke, at man opgiver standard Git eller driver en kompleks flåde af specialbyggede infrastrukturservere. Denne arkitektur behandler Gits objektlager som et Content-Addressable Store, adskiller refs/heads/main fra refs/trunk/main og refs/trunk/history og sammenfletter træer i hukommelsen med git merge-tree --write-tree. Resultatet er, at både menneskelige ingeniører og autonome kodeagenter kan klone et arbejdsområde på under et sekund og skalere pænt på tværs af hundredvis af projekter.