---
id: 773766eac9e6ff3c46bd686d78539aed
author: Lemon Mint
title: 'Gentænkning af Git Monorepo: En CAS-native arkitektur for mennesker og coding agents'
description: Sådan bygger du en zero-daemon, CAS-native monorepo på standard Git ved hjælp af parentless trunk snapshots, isolerede workspace references, in-memory merge trees, hierarkiske OWNERS og O(delta) mirror synchronization.
language: da
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ø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:

```
refs/heads/main               Kun kontrolplan (~20 KB: regler, .gitconfig, synkroniseringskonfiguration, scripts)
refs/trunk/main               Forældreløs snapshot-commit, der peger på den seneste samlede rod-tree
refs/trunk/history            Lineær forældrelinket commit-kæde, der deler den præcise rod-tree SHA-1
refs/ws/<actor>/<id>          Isolerede arbejdsområde-snapshots (changelists) pr. udvikler eller agent
refs/ws/cl/<change-id>/<tip>  Uforanderlige patchsets: hver tip af en ændring, der er sendt til review
refs/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**:

```bash
# Forældreløs snapshot-commit til øjeblikkelige depth-1 checkouts af arbejdsområder
SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")

# Forældrelinket commit til 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"
```

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:

```bash
# 1. Klon 20 KB kontrolplanet med blobfiltrering og sparsom tilstand aktiveret
git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
cd monorepo

# 2. Indlæs repositoryets aliaser og tilknyt refs/trunk/*
git config include.path ../.gitconfig
git trunk-init

# 3. Angiv de domænemapper, som denne opgave kræver
git sparse-checkout set net/relay ipc/codec

# 4. Opret et isoleret arbejdsområde baseret på det seneste trunk-snapshot
git 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:

```bash
# 1. Identificer rod-snapshot-commit, hvor arbejdsområdet blev forgrenet
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 sammenlagte rod-tree i hukommelsen uden checkout af et arbejdstræ
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 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æfiks                                                                           | Niveau              | Tilladte imports                                 | Forbudte imports               | `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 (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.
