---
id: 773766eac9e6ff3c46bd686d78539aed
author: Lemon Mint
title: 'Ny vurdering av Git-monorepoet: En CAS-nativ arkitektur for mennesker og Coding Agents'
description: Hvordan bygge et zero-daemon, CAS-native monorepo på standard Git ved hjelp av parentless trunk snapshots, isolated workspace references, in-memory merge trees, hierarchical OWNERS og O(delta) mirror synchronization.
language: "no"
date: 2026-10-30T01:17:15.292299Z
path: /blog/posts/rethinking-the-git-monorepo-a-cas-native-architecture-for-humans-and-coding-agents-zfd3d7931
category: lowlevel
---

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:

```
refs/heads/main               Kun kontrollplan (~20 KB: regler, .gitconfig, synkroniseringskonfigurasjon, skript)
refs/trunk/main               Foreldreløs snapshot-commit som peker til det nyeste samlede rot-treet
refs/trunk/history            Lineær, foreldrekoblet commit-kjede som deler nøyaktig rot-tree SHA-1
refs/ws/<actor>/<id>          Isolerte arbeidsområde-snapshots (changelists) per utvikler eller agent
refs/ws/cl/<change-id>/<tip>  Uforanderlige patchsets: hver tip av en endring sendt til gjennomgang
refs/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**:

```bash
# Foreldreløs snapshot-commit for umiddelbare utsjekkinger av arbeidsområder med dybde 1
SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")

# Foreldrekoblet commit for lineær git log og git blame
HIST=$(git commit-tree "$NEW_TREE" -p "$OLD_HIST" -m "$RAW_MSG")

git push --atomic origin \
  --force-with-lease="refs/trunk/main:${OLD_MAIN}" \
  --force-with-lease="refs/trunk/history:${OLD_HIST}" \
  "${SNAP}:refs/trunk/main" \
  "${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:

```bash
# 1. Klon 20 KB-kontrollplanet med blob-filtrering og sparsom modus aktivert
git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
cd monorepo

# 2. Last repositoriets aliaser og koble til refs/trunk/*
git config include.path ../.gitconfig
git trunk-init

# 3. Deklarer domenekatalogene som trengs for denne oppgaven
git sparse-checkout set net/relay ipc/codec

# 4. Opprett et isolert arbeidsområde forankret i det nyeste trunk-snapshotet
git 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:

```bash
# 1. Finn rot-snapshot-commit-en der arbeidsområdet forgrenet seg
BASE=$(git rev-list --max-parents=0 "$WORK_SHA")

BASE_TREE=$(git rev-parse "${BASE}^{tree}")
MAIN_TREE=$(git rev-parse "${OLD_MAIN}^{tree}")
WORK_TREE=$(git rev-parse "${WORK_SHA}^{tree}")

# 2. Beregn det sammenslåtte rot-treet i minnet uten utsjekking av et arbeidstre
if [ "$MAIN_TREE" = "$BASE_TREE" ]; then
  NEW_TREE="$WORK_TREE"
elif OUT=$(git merge-tree --write-tree --merge-base="$BASE_TREE" "$MAIN_TREE" "$WORK_TREE"); then
  NEW_TREE=$(head -n1 <<<"$OUT")
else
  echo "Merge conflict detected; run git ws-sync to rebase." >&2
  exit 1
fi
```

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:

| Katalogprefiks                                                                         | Nivå                 | Tillatte importer                                 | Forbudte importer              | `OWNERS`-kontroll       |
| :------------------------------------------------------------------------------------- | :------------------- | :------------------------------------------------ | :----------------------------- | :---------------------- |
| `//web/*`, `//net/*`, `//ipc/*`, `//storage/*`, `//crypto/*`, `//ai/*`, `//devtools/*` | Offentlig produksjon | Offentlig produksjon, `//third_party/*`           | `//corp/*`, `//experimental/*` | Håndheves               |
| `//third_party/*`                                                                      | Ekstern leverandør   | Selvstendig eller `//third_party/*`               | Alle domener utenom `third_party` | Håndheves            |
| `//corp/*`                                                                             | Intern privat        | Offentlig produksjon, `//corp/*`, `//third_party/*` | `//experimental/*`           | Håndheves               |
| `//experimental/<actor>/*`                                                             | Sandkasse            | Enhver 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.
