GoSuda

Gentænkning af Git Monorepo: En CAS-native arkitektur for mennesker og coding agents

By Lemon Mint views ...

Enhver voksende ingeniørorganisation står på et tidspunkt over for valget mellem mange repositories og ét. Man starter med snesevis af små repositories, og i begyndelsen føles livet enkelt. Hvert bibliotek har sin egen issue tracker, sin egen CI-pipeline og en git clone, der er færdig på få sekunder. Derefter ændrer man 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 man venter på, at CI-køerne tømmes i topologisk rækkefølge.

Flytter man alt over i et traditionelt Git-monorepo, bytter man den tværgående koordinering mellem repositories ud med repositoryets størrelse. Efterhånden som commit-historikken vokser, og binære aktiver ophobes i tidligere revisioner, bliver git fetch langsommere. Arbejdstræerne svulmer op til gigabyte. Før længe foreslår nogen at opsætte en skræddersyet virtuel filsystemdæmon, en dedikeret code review-server eller en flåde af eksterne build-workers, blot for at gøre den daglige udvikling udholdelig.

Når autonome kodningsagenter kommer med i teamet, forværres begge fejltilstande. Polyrepos gør en agent blind over for kald fra andre projekter og tvinger den til at jonglere med legitimationsoplysninger på tværs af repositories. Traditionelle monorepos overbelaster en agents containerdisk og netværksbåndbredde ved hver opgaveinitialisering, mens samtidige agenter, der arbejder på den samme branch, skaber endeløse rebase-kapløb.

Inspireret af google3-arkitekturen har vi bygget atomare ændringer på en enkelt trunk, sparsom onboarding på under et sekund, hierarkisk OWNERS-styring og selvstændige open source-spejle med standard Git-infrastruktur og standardiseret objektlagring.

Designet kræver hverken en skræddersyet filsystemdæmon eller en tredjeparts review-appliance, fordi det behandler Git som en Merkle-tree Content-Addressable Store (CAS).

Misforståelsen i kernen 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: Det rå byteindhold i en enkelt fil.
  2. tree: En mappeliste, der knytter 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.

Hvis et monorepo indeholder 50.000 filer, og du redigerer en enkelt linje i én fil, genbruger den nye commit de præcise blob- og tree-hashes for de øvrige 49.999 filer.

Et traditionelt Git-monorepo føles tungt, fordi konventionen belaster en enkelt reference, refs/heads/main, med tre modstridende opgaver:

  • Den indeholder hele det samlede mappetræ for alle projekter.
  • Den trækker hele den forældrelinkede kæde af historiske commits tilbage til første dag.
  • Den ligger i refs/heads/*, det navnerum 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.

Adskillelse af referencenavnerummet

Gits wire-protokol annoncerer kun refs/heads/* og refs/tags/* under en standard-clone. Referencer under brugerdefinerede 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å standard-clones.

Ved at fordele disse opgaver på separate referencer i ét repository fjernes flaskehalsen:

1refs/heads/main               Kun kontrolplan (~20 KB: regler, .gitconfig, synkroniseringskonfiguration, scripts)
2refs/trunk/main               Forældreløs snapshot-commit, der peger på den seneste samlede rod-tree
3refs/trunk/history            Lineær forældrelinket commit-kæde, der deler den præcise rod-tree SHA-1
4refs/ws/<actor>/<id>          Isolerede arbejdsområde-snapshots (changelists) pr. udvikler eller agent
5refs/ws/cl/<change-id>/<tip>  Uforanderlige patchsets: hver tip af en ændring, der er sendt til review
6refs/heads/land/<id>          Kortlivet merge-kø-trigger, slettet straks ved landing

1. Kontrolplanet (refs/heads/main)

Standardgrenen indeholder ingen applikationskildekode. Den holder kun styr på repositoryets driftsregler (AGENTS.md, README.md), en fælles .gitconfig med workflow-aliaser, Starlark-synkroniseringskonfigurationen (sync.star) og server-side merge-scripts. Clone af standardgrenen 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/*, //storage/*, //crypto/*, //ai/*, //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 om commit-grafen. Git-serveren gennemløber ikke historikken; den sender den enkelte hoved-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 præcis samme rod-$NEW_TREE-SHA-1-hash:

 1# Forældreløs 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"

At beholde både refs/trunk/main og refs/trunk/history fordobler ikke lagerpladsforbruget. Fordi begge commits refererer til den identiske $NEW_TREE-hash, deduplikerer Gits content-addressable store 100 % af tree- og blob-objekterne. Den ekstra lagerpris for det forældreløse snapshot er blot en 250 bytes stor commit-header.

4. Isolerede arbejdsområder (refs/ws/<actor>/<id>)

Arbejdsgrene under udarbejdelse pushes til refs/ws/<actor>/<id> i stedet for refs/heads/*. Hundrede kodningsagenter kan gemme checkpoints hvert minut uden at forurene grenlisterne eller bremse git fetch for menneskelige ingeniører.

Onboarding i fire trin: Sådan fungerer sparsom checkout med blobless-hentning

Med denne referencelayout installerer en ny udvikler eller en midlertidig kodningsagent-container hverken en skræddersyet CLI eller en FUSE-driver. Onboarding består af fire trin med standard Git og repositoryets aliaser:

 1# 1. Klon 20 KB kontrolplanet med blobfiltrering 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, som denne opgave kræver
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Opret et isoleret arbejdsområde baseret på det seneste trunk-snapshot
13git ws-new alice/optimize-framing

Hvert trin henter kun det, det har brug for:

  1. git clone --filter=blob:none --sparse downloader de 20 KB kontrolplan fra refs/heads/main.
  2. git trunk-init konfigurerer +refs/trunk/*:refs/remotes/origin/trunk/* og kører en blobless-hentning. Git downloader tree-metadataene for refs/trunk/main (typisk nogle snesevis af kilobyte) og nul filblobs.
  3. git sparse-checkout set net/relay ipc/codec instruerer Gits indeksfilter i cone-tilstand om kun at materialisere rodmappen og de to undertræer.
  4. git ws-new skifter til en ny gren med rod i origin/trunk/main. Git anmoder derefter efter behov kun om de blob-objekter, der findes i net/relay og ipc/codec. De resterende 95 % af monorepoet downloades aldrig til disken.

Fremtidshorisont: Montering af refs/trunk/main som et virtuelt filsystem (VFS)

Sparsom checkout med blobless-klonede repositories dækker de daglige arbejdsgange uden ekstra klientværktøj. Fordi refs/trunk/main allerede er et rent Merkle-tree i en Content-Addressable Store, giver arkitekturen også et direkte grundlag for et valgfrit brugerrumsbaseret 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 mount-dæmon læser rod-tree-objektet i refs/trunk/main og projicerer hele monorepoets mappehierarki ind i arbejdsmappen på millisekunder, hvilket fjerner behovet for at køre git sparse-checkout set.
  2. Blob-hentning efter behov: Filindhold forbliver virtuelt, indtil en compiler, et IDE eller en agent udsteder et open(2)- eller read(2)-systemkald. Dæmonen henter derefter den tilsvarende SHA-1-blob fra CAS-endepunktet og cacher den på lokal NVMe-lagring.
  3. Copy-on-Write-overlay til arbejdsområder: Lokale redigeringer skrives til et letvægts øvre overlagslag. Kørsel af git ws-save hasher kun de ændrede stier i overlaget, skriver de opdaterede blob- og tree-objekter og pusher den resulterende commit til refs/ws/<actor>/<id> uden at scanne uændrede mapper.

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

I en traditionel Git-arbejdsgang skaber sammenlægning af en feature-branch enten en merge-commit med to forældre, hvilket gør historikken ikke-lineær, eller det kræver, at udviklerne manuelt rebaserer og kapløber mod samtidige pushes til main.

Inspireret af google3-modellen med Changelists (CL) repræsenterer hvert arbejdsområde i vores arkitektur en enkelt atomar ændring mod trunk-snapshottet. Mens en udvikler eller agent arbejder lokalt i ws/alice/optimize-framing, kan der laves ti trinvise 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 at der nogensinde checkes et arbejdstræ ud på CI-runneren.

Sådan udføres landing-scriptet (land.sh) i hukommelsen ved hjælp af Git 2.40+-infrastruktur:

 1# 1. Identificer rod-snapshot-commit, hvor arbejdsområdet blev forgrenet
 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 sammenlagte rod-tree 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

I trin 2:

  • Uanset hvor mange lokale commits der er lavet på arbejdsområdets branch, sammenligner scriptet kun tre tree-hashes: det oprindelige basis-snapshot ($BASE_TREE), det aktuelle trunk-snapshot ($MAIN_TREE) og det endelige arbejdsområde-tree ($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 rod-tree $NEW_TREE direkte i Gits objektlager på millisekunder, uden at skrive filer til runnerens filsystem.
  • Efter at de nedenstående politikkontroller er bestået, opdaterer ét git push --atomic med --force-with-lease refs/trunk/main, tilføjer én lineær commit til refs/trunk/history, og landing-triggeren refs/heads/land/<id> slettes.

Håndhævelse af arkitekturgrænser: Synlighed i fire niveauer og hierarkiske OWNERS

Konsolidering af kode i ét repository virker kun, hvis grænserne mellem domænerne håndhæves mekanisk, før en commit kan lande. Umiddelbart efter at git merge-tree --write-tree har produceret $NEW_TREE (og før git push --atomic opdaterer trunken), kører vores politikmotor to O(delta)-kontroller over git diff-tree "$MAIN_TREE" "$NEW_TREE".

1. Synlighed på fire domæneniveauer

Rod-tree'et er opdelt i fire synlighedsniveauer:

MappepræfiksNiveauTilladte importsForbudte importsOWNERS-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 (auto-godkendt)

Hvis en offentlig pakke i //web/* forsøger at importere en intern pakke fra //corp/* eller en ikke-færdiggjort prototype fra //experimental/*, afbryder landing-jobbet, før refs/trunk/main berøres.

2. Hierarkisk OWNERS-verifikation uden en Gerrit-server

Verifikatoren håndhæver 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 filnavnemønster.
  • Invariant mod manipulation: Eksisterende OWNERS-filer læses altid fra $MAIN_TREE (det aktuelle trunk-tree), aldrig fra $NEW_TREE. En uautoriseret forfatter kan ikke give sig selv tilladelse ved at redigere OWNERS i samme arbejdsområde-commit.
  • Undtagelse for sandkasse: Enhver sti under //experimental/<actor>/* omgår OWNERS-kontrollerne, 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.

Code review uden gnidninger: Midlertidige PR'er og snapshot-diffs

Fordi refs/heads/main kun indeholder 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 ind i main forsøge at sammenligne ikke-relaterede historikker. Vi løste dette med tre review-arbejdsgange, skræddersyet til forskellige typer ændringer:

  1. Øjeblikkelige to-punktsdiffs for arbejdsområder (git ws-save + edge-diff-router) Ved at pushe et arbejdsområde med git ws-save eksponeres refs/ws/<actor>/<id> gennem en letvægts edge-worker. Denne worker opløser refs/trunk/main og arbejdsområdereferencen til deres commit-SHA'er og omdirigerer til GitHubs to-punkts compare-visning (/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-review på GitHub pusher git ws-pr rod-snapshot-commit'en ($(git rev-list --max-parents=0 HEAD)) til refs/heads/base/<id> og arbejdsområdets tip til refs/heads/review/<id>. Derefter åbnes en GitHub Pull Request, der sammenligner review/<id> med base/<id>. Fordi basisgrenen er det snapshot, arbejdsområdet blev forgrenet fra, viser GitHubs fane Files changed kun arbejdsområdets delta. Når revieweren klikker på Merge pull request, forfremmer en webhook head-commit'en til refs/heads/land/<id>, kører den lineære landing-pipeline i hukommelsen og sletter automatisk både refs/heads/base/<id> og refs/heads/review/<id>.
  3. Pull requests til eksterne spejle Open source-bidragydere behøver aldrig at lære monorepo-arbejdsgangen. De forker og åbner pull requests mod selvstændige spejl-repositories, som de plejer.

Ændringer, patchsets og videreførsel af godkendelser

Et branchnavn kan ikke identificere en ændring. En agent kan omdøbe sin branch, ændre tip'en eller rebasere på en nyere trunk flere gange, inden den lander. Ligesom i Gerrit identificerer arbejdsgangen en ændring ved en Change-Id: I<40 hex>-trailer i tip-commitbeskeden. git ws-save, git ws-pr og git ws-land tilføjer trailer'en, når tip'en mangler den, og bevarer den ellers, så id'et overlever ændringer, rebaser og omdøbninger. Den landede trunk-commit beholder trailer'en og registrerer nummeret på dens review-pull request, så git trunk-cl <number|Change-Id> finder den i refs/trunk/history.

Hver tip, der sendes til review, bliver et patchset. git ws-pr pusher den til den uforanderlige reference refs/ws/cl/<change-id>/<tip> ved siden af review-grenene. Kørsel af git ws-pr igen efter en ændring eller en git ws-sync tilføjer et patchset til samme pull request. git ws-ps lister patchsets for den aktuelle ændring, og git ws-psdiff <a> <b> udskriver interdiffen mellem to af dem, så en reviewer kun læser det, der er ændret siden den seneste runde.

GitHub binder en godkendelse til en enkelt commit, så hver rebase normalt ville kræve en ny review. Landing-pipelinen accepterer også en godkendelse givet til et andet patchset af samme ændring, når begge patchsets laver den samme patch mod deres eget basis-snapshot. Den sammenligner git patch-id --verbatim over den fulde binære diff. En ren rebase på en nyere trunk lander uden endnu en review, mens ethvert indholdsmæssigt ændringsforsøg, herunder en ændring af mellemrum eller filtilstande, kræver en ny godkendelse.

O(delta) tovejssynkronisering og separate produktionsgrene

Mange organisationer publicerer enkeltpakker som selvstændige GitHub-repositories, så eksterne brugere kan køre go get, cargo install eller forke et enkelt projekt uden at røre monorepoet. Nogle projekter, såsom statiske blogs eller webapplikationer, der er underlagt udgivelsesporte, 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 antallet af filer ændret i en commit og ikke med monorepoets størrelse:

  • Blobless indeksindsætning: Når et spejl-repositorys undertræ indsættes i eller fjernes fra monorepoets rod-tree, 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 forbliver refereret via SHA-1 og downloades aldrig til CI-runneren.
  • Hentning af blobs kun for delta: Inden en eksporteret commit pushes til et selvstændigt spejl, kører scriptet git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree" og henter 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 udgivelsesgrene: Under udgående eksport bevarer synkronisatoren hvert selvstændigt repositorys egen .github/workflows/-mappe, så offentlige CI-runnere kører projekttests på spejlet. Hver workflow i Starlark-konfigurationen peger på en eksplicit branch, såsom main, hvilket bevarer udgivelsesporte for repositories, der udrulles fra en separat production-branch. Monorepo-landinger synkroniseres med spejlets main-branch, og automatiserede metadata- eller oversættelsescommits på main synkroniseres tilbage til refs/trunk/main via webhook. Forfremmelse af main til production forbliver et eksplicit udgivelsestrin.

Afsluttende bemærkninger

Denne arkitektur behandler Gits objektlager som en Content-Addressable Store, adskiller refs/heads/main fra refs/trunk/main og refs/trunk/history, identificerer hver ændring med et Change-Id og uforanderlige patchsets og sammenfletter trees i hukommelsen med git merge-tree --write-tree. Onboardingomkostningen vokser med de mapper, en opgave har brug for, og omkostningen ved landing og spejlsynkronisering vokser med størrelsen på en ændring, ikke med størrelsen på repositoryet.