---
id: 773766eac9e6ff3c46bd686d78539aed
author: Lemon Mint
title: 'Att ompröva Git-monorepot: En CAS-native arkitektur för människor och coding agents'
description: Hur man bygger en zero-daemon, CAS-native monorepo på standard Git med hjälp av parentless trunk snapshots, isolated workspace references, in-memory merge trees, hierarchical OWNERS och O(delta) mirror synchronization.
language: sv
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
---

Varje växande ingenjörsorganisation väljer så småningom mellan många repositorier och ett. Börja med dussintals små repositorier, så känns livet enkelt i början. Varje bibliotek har sin egen ärendehanterare, sin egna CI-pipeline och ett `git clone` som blir klart på några sekunder. Sedan ändrar du en struct i ett centralt nätverkspaket och tillbringar resten av eftermiddagen med att öppna versionsuppdateringspull requests i sex nedströmsrepositorier, medan du väntar på att CI-köerna töms i topologisk ordning.

Flyttar du allt till ett traditionellt Git-monorepo byter du samordning mellan repositorier mot repositoriestorlek. I takt med att committhistoriken fördjupas och binära tillgångar ackumuleras i tidigare revisioner blir `git fetch` långsammare. Arbetskataloger sväller till gigabyte. Innan länge föreslår någon att man ska installera en anpassad daemon för virtuellt filsystem, en dedikerad kodgranskningsserver eller en flotta av fjärrbyggarbetare enbart för att göra den dagliga utvecklingen uthärdlig.

När autonoma kodagenter ansluter sig till teamet förvärras båda felmoden. Polyrepos gör en agent blind för anropare i andra projekt och tvingar den att jonglera med autentiseringsuppgifter över repositorier. Traditionella monorepos överväldigar agentens containerdisk och nätverksbandbredd vid varje uppgiftsinitiering, medan samtidiga agenter som trampar på samma gren skapar oändliga rebase-kapplöpningar.

Inspirerade av arkitekturen `google3` har vi byggt atomära ändringar på en enda trunk, sparsam onboarding på under en sekund, hierarkisk `OWNERS`-styrning och fristående mirrors med öppen källkod, med standardmässig Git-infrastruktur och lagring på standardhårdvara.

Designen behöver varken någon anpassad filsystemdaemon eller någon tredjepartsgranskningsapplians, eftersom den behandlar Git som ett Merkle-träbaserat Content-Addressable Store (CAS).

## Missuppfattningen i kärnan av långsamma Git-monorepos

När utvecklare klagar på att Git inte skalar till stora monorepos stöter de sällan på en gräns i Gits objektdatabas. Inuti `.git/objects` lagrar Git tre oföränderliga primitiver, identifierade med sina SHA-1- (eller SHA-256-) innehållshashar:

1. **`blob`**: Det råa byteinnehållet i en enskild fil.
2. **`tree`**: En kataloglista som mappar filnamn och behörigheter till `blob`- eller underordnade `tree`-hashar.
3. **`commit`**: En liten textheader (vanligen 200 till 300 byte) som pekar på en enda rot-`tree`-hash, noll eller fler förälder-`commit`-hashar och författarmetadata.

Om ett monorepo innehåller 50 000 filer och du redigerar en enda rad i en fil, återanvänder den nya committen de exakta `blob`- och `tree`-hasharna för de övriga 49 999 filerna.

Ett traditionellt Git-monorepo känns tungt eftersom konventionen överbelastar en enda referens, `refs/heads/main`, med tre motstridiga uppgifter:

- Den innehåller **hela det enhetliga katalogträdet** för varje projekt.
- Den drar med sig **hela den föräldrasammanlänkade kedjan av historiska commits** tillbaka till första dagen.
- Den ligger inuti `refs/heads/*`, namnrymden som Git-servrar annonserar och förhandlar om som standard vid varje `git clone` och `git fetch`.

Varje gång en utvecklare eller CI-runner hämtar `refs/heads/main` förhandlar klienten och servern om nåbarhet i commit-grafen över tusentals historiska revisioner. Om någon av misstag committade ett 100 MB stort arkiv för tre år sedan och tog bort det dagen efter, hänger dess `blob` fortfarande kvar vid ett historiskt `tree` som är nåbart från `refs/heads/main`.

## Separering av referensnamnrymden

Gits wire-protokoll annonserar endast `refs/heads/*` och `refs/tags/*` under en standardklon. Referenser under anpassade namnrymder, såsom `refs/trunk/*` eller `refs/ws/*`, finns i exakt samma objektlager på servern, drar nytta av samma packfile-deltakomprimering och kan hämtas eller pushas explicit, men de medför ingen extra kostnad för standardkloner.

Att dela upp dessa uppgifter över separata referenser i ett enda repository eliminerar flaskhalsen:

```
refs/heads/main               Enbart kontrollplan (~20 KB: regler, .gitconfig, synkkonfiguration, skript)
refs/trunk/main               Commit utan förälder som pekar på det senaste enhetliga rotträdet
refs/trunk/history            Linjär föräldrasammanlänkad commit-kedja som delar exakt rot-SHA-1 för trädet
refs/ws/<actor>/<id>          Isolerade arbetsytesnapshots (changelists) per utvecklare eller agent
refs/ws/cl/<change-id>/<tip>  Oföränderliga patchsets: varje tip för en ändring som skickats för granskning
refs/heads/land/<id>          Kortlivad merge-kö-trigger som raderas omedelbart vid landning
```

### 1. Kontrollplanet (`refs/heads/main`)

Standardgrenen innehåller ingen applikationskällkod. Den spårar endast reglerna för driften av repositoryt (`AGENTS.md`, `README.md`), en delad `.gitconfig` med arbetsflödesaliaser, Starlark-synkroniseringskonfigurationen (`sync.star`) och serversidiga merge-skript. Att klona standardgrenen överför ungefär 20 KB och är klart på millisekunder.

### 2. Den föräldralösa trunk-snapshoten (`refs/trunk/main`)

`refs/trunk/main` pekar på hela det enhetliga källträdet för hela organisationen (`//web/*`, `//net/*`, `//ipc/*`, `//storage/*`, `//crypto/*`, `//ai/*`, `//devtools/*`, `//corp/*`, `//third_party/*`, `//experimental/*`), men dess commit-objekt skapas **utan en `-p`-förälderflagga**.

Eftersom `refs/trunk/main` har noll föräldrar (`parents = []`) kräver hämtning av den ingen förhandling om commit-grafen. Git-servern går inte igenom historiken; den skickar det enda head-commit-objektet och dess aktuella katalog-`tree`-objekt. Historiska binärer eller filer som raderades för en månad sedan är inte nåbara från `refs/trunk/main` och överförs aldrig.

### 3. Den linjära granskningshistoriken (`refs/trunk/history`)

Utvecklare behöver fortfarande `git blame` och `git log` när de undersöker regressioner. Varje gång en ändring landar skapar merge-skriptet två commit-objekt som pekar på exakt samma `$NEW_TREE`-SHA-1-hash:

```bash
# Snapshot-commit utan förälder för omedelbara depth-1-utcheckningar av arbetsytor
SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")

# Förälderlänkad commit för linjär git log och 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"
```

Att behålla både `refs/trunk/main` och `refs/trunk/history` fördubblar inte lagringsutrymmet. Eftersom båda commits refererar till samma `$NEW_TREE`-hash deduplicerar Gits content-addressable store 100 % av `tree`- och `blob`-objekten. Den extra lagringskostnaden för snapshot-committen utan förälder är endast en 250 byte stor commit-header.

### 4. Isolerade arbetsytor (`refs/ws/<actor>/<id>`)

Pågående arbetsgrenar pushas till `refs/ws/<actor>/<id>` i stället för `refs/heads/*`. Hundra kodagenter kan spara checkpoints varje minut utan att förorena grenlistorna eller bromsa `git fetch` för mänskliga ingenjörer.

## Onboarding i fyra steg: så fungerar sparsam och blobfri utcheckning

Med denna referensuppdelning installerar en ny utvecklare eller en tillfällig kodagentcontainer inte något anpassat CLI eller någon FUSE-drivrutin. Onboarding tar fyra steg med standard-Git och repositoryts aliases:

```bash
# 1. Klona 20 KB-kontrollplanet med blobfiltrering och sparsamt läge aktiverat
git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
cd monorepo

# 2. Läs in repositoryts aliases och koppla refs/trunk/*
git config include.path ../.gitconfig
git trunk-init

# 3. Deklarera de domänkataloger som behövs för denna uppgift
git sparse-checkout set net/relay ipc/codec

# 4. Skapa en isolerad arbetsyta med rot i den senaste trunk-snapshoten
git ws-new alice/optimize-framing
```

Varje steg hämtar endast det som behövs:

1. `git clone --filter=blob:none --sparse` laddar ner 20 KB-kontrollplanet på `refs/heads/main`.
2. `git trunk-init` konfigurerar `+refs/trunk/*:refs/remotes/origin/trunk/*` och kör en blobfri hämtning. Git laddar ner `tree`-metadata för `refs/trunk/main` (vanligen några tiotal kilobyte) och **noll filblobbar**.
3. `git sparse-checkout set net/relay ipc/codec` instruerar Gits filter för indexet i cone-läge att endast materialisera rotkatalogen och dessa två underträd.
4. `git ws-new` växlar till en ny gren med rot i `origin/trunk/main`. Git begär därefter vid behov endast `blob`-objekten inuti `net/relay` och `ipc/codec`. Resterande 95 % av monorepot laddas aldrig ner till disk.

### Framtidshorisont: montering av `refs/trunk/main` som ett virtuellt filsystem (VFS)

Sparsam utcheckning med blobfria kloner täcker de dagliga arbetsflödena utan extra klientverktyg. Eftersom `refs/trunk/main` redan är ett rent Merkle-träd i ett Content-Addressable Store, ger arkitekturen även en direkt grund för ett valfritt användarlandsbaserat virtuellt filsystem (VFS) via FUSE på Linux, FileProvider på macOS eller ProjFS på Windows:

1. **Omedelbar montering av hela trädet**: En lätt lokal monteringsdaemon läser rotobjektet `tree` för `refs/trunk/main` och projicerar hela monorepots katalogstruktur i arbetskatalogen på millisekunder, vilket gör det onödigt att köra `git sparse-checkout set`.
2. **Blobladdning på begäran**: Filinnehåll förblir virtuellt tills en kompilator, IDE eller agent anropar systemanropet `open(2)` eller `read(2)`. Daemonen hämtar då motsvarande SHA-1-`blob` från CAS-slutpunkten och cachar den på lokal NVMe-lagring.
3. **Copy-on-write-overlay för arbetsytor**: Lokala redigeringar skrivs till ett lätt övre overlay-lager. Att köra `git ws-save` hashar endast de smutsiga sökvägarna i det överlagret, skriver de uppdaterade `blob`- och `tree`-objekten och pushar den resulterande committen till `refs/ws/<actor>/<id>` utan att skanna oförändrade kataloger.

## Changelist-modellen i ren Git: sammanslagning i minnet på servern

I ett traditionellt Git-arbetsflöde skapar sammanslagning av en funktionsgren antingen en merge-commit med två föräldrar, vilket gör historiken icke-linjär, eller kräver att utvecklare manuellt rebasear och tävlar mot samtidiga pushar till `main`.

Inspirerad av modellen Changelist (CL) i `google3` representerar varje arbetsyta i vår arkitektur en **enda atomär ändring** mot trunk-snapshoten. Medan en utvecklare eller agent arbetar lokalt i `ws/alice/optimize-framing` kan den göra tio inkrementella commits (`wip`, `fix tests`, `lint`). När ändringen är redo att landa kollapsar servern arbetsytans delta till en enda linjär trunk-revision utan att någonsin checka ut en arbetskatalog på CI-runnern.

Så här kör landningsskriptet (`land.sh`) i minnet med Git 2.40+-plumbing:

```bash
# 1. Identifiera rotsnapshot-committen där arbetsytan grenades av
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. Beräkna det sammanslagna rotträdet i minnet utan utcheckning av arbetskatalog
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 gäller följande:

- Oavsett hur många lokala commits som gjorts på arbetsytans gren jämför skriptet endast tre trädhashar: den ursprungliga basnapshoten (`$BASE_TREE`), den aktuella trunk-snapshoten (`$MAIN_TREE`) och det slutliga arbetsyteträdet (`$WORK_TREE`).
- Om `refs/trunk/main` avancerade i `//web/blog` medan Alice redigerade `//net/relay`, beräknar `git merge-tree --write-tree` det kombinerade rotträdet `$NEW_TREE` direkt i Gits objektlager på millisekunder, utan att skriva filer till runnerns filsystem.
- När de policykontroller som beskrivs nedan har godkänts uppdaterar ett enda `git push --atomic` med `--force-with-lease` `refs/trunk/main`, lägger till en linjär commit i `refs/trunk/history`, och landningstriggern `refs/heads/land/<id>` raderas.

## Att tillämpa arkitekturgränser: fyranivåsynlighet och hierarkiska `OWNERS`

Att konsolidera kod i ett enda repository fungerar endast om gränserna mellan domäner upprätthålls mekaniskt innan en commit kan landa. Direkt efter att `git merge-tree --write-tree` har producerat `$NEW_TREE` (och innan `git push --atomic` uppdaterar trunk) kör vår policymotor två O(delta)-kontroller över `git diff-tree "$MAIN_TREE" "$NEW_TREE"`.

### 1. Fyranivåig domänsynlighet

Rotträdet är indelat i fyra synlighetsnivåer:

| Katalogprefix | Nivå | Tillåtna importer | Förbjudna importer | `OWNERS`-grind |
| :--- | :--- | :--- | :--- | :--- |
| `//web/*`, `//net/*`, `//ipc/*`, `//storage/*`, `//crypto/*`, `//ai/*`, `//devtools/*` | Publik produktion | Publik produktion, `//third_party/*` | `//corp/*`, `//experimental/*` | Tillämpas |
| `//third_party/*` | Extern leverantör | Självständig eller `//third_party/*` | Alla domäner utom `third_party` | Tillämpas |
| `//corp/*` | Intern privat | Publik produktion, `//corp/*`, `//third_party/*` | `//experimental/*` | Tillämpas |
| `//experimental/<actor>/*` | Sandlåda | Valfritt paket utom `corp` | `//corp/*` | Undantagen (automatiskt godkänd) |

Om ett publikt paket i `//web/*` försöker importera ett internt paket från `//corp/*` eller en oavslutad prototyp från `//experimental/*`, avbryts landningsjobbet innan `refs/trunk/main` berörs.

### 2. Hierarkisk `OWNERS`-verifiering utan en Gerrit-server

Verifieraren tillämpar katalognivåns `OWNERS`-filer med arvsregler:

- För varje sökväg som ändrats mellan `$MAIN_TREE` och `$NEW_TREE` går verifieraren uppåt från filens katalog till `//OWNERS`.
- Vilken `OWNERS`-fil som helst kan deklarera `set noparent` för att blockera godkännanden som ärvs från överordnade kataloger, eller `per-file *.sql=@dba-team` för att avgränsa godkännanden efter filnamnsmönster.
- **Invariant mot manipulation**: Befintliga `OWNERS`-filer läses alltid från `$MAIN_TREE` (det aktuella trunk-trädet), aldrig från `$NEW_TREE`. En ogodkänd författare kan inte ge sig själv behörighet genom att redigera `OWNERS` i samma arbetsytecommit.
- **Sandlådeundantag**: Varje sökväg under `//experimental/<actor>/*` kringgår `OWNERS`-kontroller, så att ingenjörer och agenter fritt kan prototypa med `git ws-exp <actor> <project>` och senare befordra paketet till en produktionsdomän med `git ws-graduate`.

## Kodgranskning utan friktion: tillfälliga PR:er och snapshot-diffar

Eftersom `refs/heads/main` endast innehåller kontrollplanet och `refs/trunk/main` är en commit utan förälder, skulle en vanlig GitHub Pull Request direkt från en arbetsytegren till `main` försöka jämföra orelaterade historiker. Vi löste detta med tre granskningsarbetsflöden anpassade efter olika ändringar:

1. **Omedelbara tvåpunktsdiffar för arbetsytor (`git ws-save` + Edge Diff Router)**
   Att pusha en arbetsyta med `git ws-save` exponerar `refs/ws/<actor>/<id>` via en lätt edge-worker. Workern löser `refs/trunk/main` och arbetsytereferensen till deras commit-SHA:er och omdirigerar till GitHubs tvåpunktsjämförelsevy (`/compare/<trunk-sha>..<ws-sha>`). För små patchar eller agentgenererade ändringar kan granskare inspektera diffen och klicka på **Land** direkt i webbgränssnittet.
2. **Tillfälliga GitHub Pull Requests (`git ws-pr`)**
   För rad-för-rad-granskning på GitHub pushar `git ws-pr` rotsnapshot-committen (`$(git rev-list --max-parents=0 HEAD)`) till `refs/heads/base/<id>` och arbetsytans spets till `refs/heads/review/<id>`. Därefter öppnar den en GitHub Pull Request som jämför `review/<id>` mot `base/<id>`. Eftersom basgrenen är snapshoten som arbetsytan grenades från visar GitHubs flik **Files changed** endast arbetsytans delta. När granskaren klickar på **Merge pull request** befordrar en webhook head-committen till `refs/heads/land/<id>`, kör landningspipelinen i minnet och raderar automatiskt både `refs/heads/base/<id>` och `refs/heads/review/<id>`.
3. **Pull Requests mot externa speglar**
   Bidragsgivare till projekt med öppen källkod behöver aldrig lära sig monorepo-arbetsflödet. De forkar och öppnar pull requests mot fristående spegelrepositorier på vanligt sätt.

### Ändringar, patchsets och överföring av godkännanden

Ett grennamn kan inte identifiera en ändring. En agent kan byta namn på sin gren, ändra spetsen eller rebasa mot en nyare trunk flera gånger innan landning. Liksom i Gerrit identifierar arbetsflödet en ändring med en `Change-Id: I<40 hex>`-trailer i spetscommitens meddelande. `git ws-save`, `git ws-pr` och `git ws-land` lägger till trailern när spetsen saknar den och behåller den annars, så att identifieraren överlever ändringar, rebaser och namnbyten. Den landade trunk-committen behåller trailern och registrerar numret på sin granskningspull request, så att `git trunk-cl <number|Change-Id>` hittar den i `refs/trunk/history`.

Varje spets som skickas för granskning blir ett patchset. `git ws-pr` pushar den till den oföränderliga referensen `refs/ws/cl/<change-id>/<tip>` bredvid granskningsgrenarna. Att köra `git ws-pr` igen efter en ändring eller en `git ws-sync` lägger till ett patchset till samma pull request. `git ws-ps` listar patchseten för den aktuella ändringen, och `git ws-psdiff <a> <b>` skriver ut interdiffen mellan två av dem, så att en granskare endast läser det som ändrats sedan föregående omgång.

GitHub binder ett godkännande till en enskild commit, så varje rebase skulle normalt kräva en ny granskning. Landningspipelinen accepterar också ett godkännande som givits till ett annat patchset av samma ändring, när båda patchseten gör samma patch mot sin egen basnapshot. Den jämför `git patch-id --verbatim` över den fullständiga binära diffen. En ren rebase mot en nyare trunk landar utan ytterligare granskning, medan varje innehållsändring, inklusive en ändring av blanksteg eller filläge, kräver ett nytt godkännande.

## O(delta) dubbelriktad synkronisering och separata produktionsgrenar

Många organisationer publicerar enskilda paket som fristående GitHub-repositorier, så att externa användare kan köra `go get`, `cargo install` eller forka ett enskilt projekt utan att röra monorepot. Vissa projekt, till exempel statiska bloggar eller webbappar med releasespärrar, underhåller också en separat `production`-gren vid sidan av `main`.

Vår Starlark-synkroniseringsmotor hanterar både utgående exporter och inkommande bidrag i **O(delta)**-tid, proportionell mot de filer som ändrats i en commit snarare än mot monorepots storlek:

- **Blobfri indexinfogning**: När ett speglingsrepositorys underträd infogas i eller tas ut ur monorepots rotträd sätter motorn `GIT_NO_LAZY_FETCH=1` och kör `git update-index --index-info` följt av `git write-tree --missing-ok`. Oförändrade underträd och blobbar refereras fortsatt via SHA-1 och laddas aldrig ner till CI-runnern.
- **Prefetch av endast delta-blobbar**: Innan en exporterad commit pushas till en fristående spegel kör skriptet `git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"` och prefetchar endast de gamla och nya blobbarna för de ändrade filerna via `git cat-file --batch`. Git kan då beräkna ett tunt deltapaket och pusha till speglingsrepositoryt på några sekunder.
- **Bevarande av arbetsflöden och releasegrenar**: Vid utgående export bevarar synkroniseraren varje fristående repositorys egna `.github/workflows/`-katalog, så att publika CI-runners kör projekttester på spegeln. Varje arbetsflöde i Starlark-konfigurationen riktar sig mot en explicit gren, till exempel `main`, vilket bevarar releasespärrar för repositorier som deployar från en separat `production`-gren. Monorepo-landningar synkroniseras med speglens `main`-gren, och automatiserade metadata- eller översättningscommits på `main` synkroniseras tillbaka till `refs/trunk/main` via webhook. Att befordra `main` till `production` förblir ett explicit releasesteg.

## Avslutande reflektioner

Denna arkitektur behandlar Gits objektlager som ett Content-Addressable Store, separerar `refs/heads/main` från `refs/trunk/main` och `refs/trunk/history`, identifierar varje ändring med ett `Change-Id` och oföränderliga patchset, och sammanfogar träd i minnet med `git merge-tree --write-tree`. Introduktionskostnaden växer med de kataloger som en uppgift behöver, och kostnaden för landning och spegelsynkronisering växer med storleken på en ändring, inte med storleken på repositoryt.
