GoSuda

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

By Lemon Mint views ...

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:

  1. blob: De rå bytes i en enkelt fil.
  2. tree: En mappeliste, der kortlægger filnavne og rettigheder til blob- eller underordnede tree-hashes.
  3. 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 hver git clone og git 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:

  1. git clone --filter=blob:none --sparse henter 20 KB-kontrolplanet på refs/heads/main.
  2. git trunk-init konfigurerer +refs/trunk/*:refs/remotes/origin/trunk/* og kører en blobless hentning. Git henter tree-metadataene for refs/trunk/main (typisk nogle få snesevis af kilobytes) og nul fil-blobs.
  3. git sparse-checkout set net/relay ipc/codec instruerer Gits cone-mode indeksfilter om kun at materialisere rodmappen og de to undertræer.
  4. git ws-new skifter til en ny branch forankret i origin/trunk/main. Netop på dette tidspunkt anmoder Git efter behov kun om blob-objekterne inde i net/relay og ipc/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:

  1. Øjeblikkelig montering af hele træet: En letvægts lokal monteringsdæmon læser rod-tree-objektet for refs/trunk/main og projicerer hele monorepoets mappehierarki ind i arbejdsmappen på millisekunder, så det ikke længere er nødvendigt at køre git sparse-checkout set.
  2. Blob-hentning ved behov: Filindhold forbliver virtuelt, indtil en compiler, IDE eller agent udfører et open(2)- eller read(2)-systemkald. Dæmonen henter derefter den tilsvarende SHA-1 blob fra CAS-endpointet og cacher den på lokal NVMe-lagring.
  3. Copy-on-Write-overlay til arbejdsområder: Lokale redigeringer skrives til et letvægts øvre overlay-lag. Kørsel af git ws-save hashes kun de ændrede stier i det overlay, skriver de opdaterede blob- og tree-objekter og pusher den resulterende commit til refs/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/main er rykket frem i //web/blog, mens Alice redigerede //net/relay, beregner git merge-tree --write-tree det kombinerede rodtræ $NEW_TREE direkte inde i Git-objektlageret på millisekunder. Der skrives ingen filer til runnerens filsystem.
  • Til sidst opdaterer git push --atomic med --force-with-lease refs/trunk/main, tilføjer én lineær commit til refs/trunk/history og sletter landing-udløseren refs/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:

MappeprefiksNiveauTilladte importerForbudte importerOWNERS-gate
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*Offentlig produktionOffentlig produktion, //third_party/*//corp/*, //experimental/*Håndhævet
//third_party/*Ekstern leverandørSelvstændig eller //third_party/*Alle ikke-third_party-domænerHåndhævet
//corp/*Intern privatOffentlig produktion, //corp/*, //third_party/*//experimental/*Håndhævet
//experimental/<actor>/*SandkasseEnhver 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_TREE og $NEW_TREE, går verifikatoren opad fra filens mappe til //OWNERS.
  • Enhver OWNERS-fil kan erklære set noparent for at blokere nedarvede godkendelser fra overordnede mapper, eller per-file *.sql=@dba-team for 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 redigere OWNERS i samme arbejdsområdecommit.
  • Undtagelse for sandkasse: Enhver sti under //experimental/<actor>/* omgår OWNERS-kontroller, så ingeniører og agenter frit kan prototype med git ws-exp <actor> <project> og senere overføre pakken til et produktionsdomæne med git 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:

  1. Øjeblikkelige to-punkts arbejdsområde-diffs (git ws-save + Edge Diff Router) Ved at pushe et arbejdsområde med git ws-save eksponeres refs/ws/<actor>/<id> via en letvægts edge-worker. Denne worker oversætter refs/trunk/main og 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.
  2. Midlertidige GitHub Pull Requests (git ws-pr) Til linje-for-linje-gennemgang på GitHub pusher git ws-pr rodsnapshot-commit'en ($(git rev-list --max-parents=0 HEAD)) til refs/heads/base/<id> og arbejdsområdets spids til refs/heads/review/<id>. Derefter åbnes en GitHub Pull Request, der sammenligner review/<id> med base/<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 til refs/heads/land/<id>, kører landing-pipelinen i hukommelsen og sletter automatisk både refs/heads/base/<id> og refs/heads/review/<id>.
  3. 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=1 og kører git update-index --index-info efterfulgt af git 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 via git 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åsom main, hvilket bevarer releasekontrollen for repositories, der deployer fra en separat production-branch. Monorepo-landinger synkroniseres med spejlets main-branch, og automatiserede metadata- eller oversættelses-commits på main synkroniseres tilbage til refs/trunk/main via webhook. Forfremmelse af main til production forbliver 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.