walgit — een git-server die één binary is vóór een object store
Snel aan de slag
1. Een bucket en configuratie
Gebruik een S3-compatibele store of GCS en maak een configuratiebestand aan:
cat > walgit.toml <<'EOF'
[server]
listen = "0.0.0.0:8080"
public_url = "https://git.example.com"
auto_create_on_push = true
[server.auth]
mode = "token"
anonymous_read = false
tokens = [{ principal = "me", token_env = "WALGIT_TOKEN_ME", write = true }]
[store]
backend = "s3"
bucket = "my-walgit"
[store.s3]
endpoint = "https://s3.us-east-1.amazonaws.com"
region = "us-east-1"
EOF
2. Uitvoeren
WALGIT_TOKEN_ME=$(openssl rand -hex 24) walgit serve --config walgit.toml
3. Gebruiken
Een push naar een nieuwe naam maakt automatisch de repository aan:
git -c http.extraHeader="Authorization: Bearer $WALGIT_TOKEN_ME" push https://git.example.com/acme/app.git main
Dit is de volledige deployment. Voeg meer machines toe die naar dezelfde bucket wijzen en zij serveren dezelfde repositories consistent, zonder dat er coördinatie nodig is. Als u ze allemaal stopt, verliest u enkel de cache ("warmth"), maar geen data.
walgit is een Rust-implementatie van de architectuur die Cursor beschrijft in Git at any scale (het systeem dat zij Continuity noemen), met de nodige aanpassingen om het te kunnen draaien op machines die kleiner zijn dan de repository zelf.
Waarom deze architectuur?
Git is gedistribueerd, maar het hosten ervan is lastig vanwege packfiles. Alles in een repository is gecomprimeerd in grote binaire packs die zo zijn ingericht dat ze klein zijn, niet dat ze sequentieel gelezen kunnen worden. Elke git-operatie is in feite een "random walk" over gigabytes aan data. Dit werkt prima op een laptop met bestanden in de page cache, maar is rampzalig over een netwerkbestandssysteem (NFS).
De traditionele oplossing (zoals GitHub's Spokes) houdt echte repositories op lokale NVMe-schijven zodat de upstream git het werk doet, en repliceert op packfile-niveau met strikte consistentie. Dit vereist echter een three-phase commit over een vaste set replica's, een database die elke repository aan machines koppelt, en een vloot van zorgvuldig beheerde servers ("pets").
De visie van Continuity verandert de economie: maak een write-ahead log (WAL) in object storage de "source of truth" en maak elke on-disk repository een cache. Een push wordt opgeslagen als een onveranderlijk (immutable) object in de bucket en wordt pas zichtbaar wanneer een klein manifest wordt herschreven met een compare-and-swap (CAS). Deze CAS is de consensus — er is geen verkiezing, geen quorum en geen primary node nodig. Elke instantie kan een push accepteren; twee gelijktijdige instanties kunnen niet beiden winnen. Een replica die een repository nog nooit heeft gezien, leest de log en bezit deze vervolgens.
Lezingen zijn consistent zonder coördinatie omdat elke read eerst aan de store vraagt of er iets is gewijzigd (een conditional GET, meestal resulterend in een 304). Compactie wordt één keer uitgevoerd door degene die een lease houdt en wordt in de log gepubliceerd, zodat replica's gecompacteerde packs downloaden in plaats van ze zelf opnieuw te packen. Omdat de WAL de waarheid is, is er volledige provenance: elke push en elke repack is terugspeelbaar naar elk gewenst punt.
walgit neemt dit over en voegt toe wat een monorepo op kleine machines nodig heeft:
- Het serveren van refs en webpagina's voor een repository waarvan de packs nooit op de instantie passen (via een remote reader over HTTP range requests).
- Het lokaal houden van commits en trees terwijl blobs in de bucket blijven (de history pack).
- Het volledig uitbesteden van clone-bytes aan de server (via
bundle-uri: nieuwe clones en catch-ups zijn statische bestanden die de bucket of een CDN uit handen geeft).
Functionaliteiten
Git
- Smart HTTP v0/v2: ls-refs met prefixes, fetch met filter/shallow/deepen/sideband-all, receive-pack (atomair, deletes, tags, push options, report-status-v2),
<owner>/<repo>namespaces, en ondersteuning voor sha1 en sha256 repositories. Upstream git doet upload-pack/repack/bundle; walgit doet receive-pack, de WAL en de plumbing. - bundle-uri: Bundles worden gesneden op kalenderslots (wekelijkse full, geketende dailies, hourlies) als een pure functie van de WAL. Een nieuwe clone downloadt de nieuwste full plus de chain erboven uit de bucket en vraagt de server alleen om het restant. Een catch-up downloadt precies de slots die gemist zijn. Er zijn twee lijsten per repo:
bundles/listvoor clones enbundles/catchupvoor fetches. Blobless families zijn beschikbaar voor--filter=blob:none.
LFS
Batch API + basisoverdracht, objecten in de bucket, optionele read-through van een upstream LFS-server voor geïmporteerde repositories.
Web UI + API
Een React UI (tree, blob, commits, diffs, en een health-pagina van de WAL) op een grotendeels leesbare JSON API onder /{owner}/{repo}/api/*. Antwoorden op basis van sha-adressering zijn immutable en overal gecached; lange antwoorden streamen voortgang via SSE. repos.js is een dependency-vrije SDK voor pagina's, agents en scripts.
Policy
Push-regels per repository (policy.json): protected refs, groepen, fast-forward only, bypass-lijsten. Zie docs/POLICY.md.
Settings
Per-repository configuratie (bundle schedules, compactie, upstream follow) gepubliceerd in de WAL met historie.
Events
Een kleine bridge volgt de WAL en verstuurt ref-events via een POST naar een webhook, exact één keer per (repo, seq, ref) met een duurzame cursor. Zie docs/EVENTS. alleen.
Maintenance
Checkpoints, bundle builds, geometrische compactie, base rebuilds, connectivity audits en repairs. Eén loop berekent elke pass de gewenste staat vanuit (config, WAL) en voert één begrensd onderdeel uit van het belangrijkste ontbrekende werk. Zelfhelend door constructie: een outage laat geen gaten achter; een verwijderd artefact is "missing" en wordt identiek herbouwd.
Auth
- none: (loopback) iedereen is anoniem met schrijfrechten.
- token: statische tokens in de configuratie (
token_envleest het geheim uit de omgeving). - oidc: elke OpenID Connect issuer (Google, Entra, Okta, Auth0, Keycloak, Dex, GitLab, etc.): browser sign-in, ID tokens, en door walgit uitgegeven access tokens voor git.
De developer setup kan met één idempotent commando: sh -c "$(curl -fsSL 'https://git.example.com/services/public/install.sh')".
Stores
S3 en S3-compatibel (AWS, MinIO, rustfs, R2, Ceph, …) en GCS zijn first-class ondersteund. Er is een in-memory store voor tests.
Hoe het werkt (beknopt)
De repository is een WAL in de bucket. Onder repos/<owner>/<repo>/ bevindt zich:
manifest.pb: (klein, CAS-herschreven) head sequence, de live pack set, checkpoint pointer, settings — dit is het linearisatiepunt.log/<seq>.pb: (immutable entries) PUSH, COMPACT, CHECKPOINT, SETTINGS.wal/<checksum>.pack|.idx|.rev|.bitmap|.commit-graph: (immutable, content-addressed packs met hun side-files).checkpoints/<seq>/: gevouwen ref snapshot + pack inventory voor een koude start.bundles/,leases/(CAS met TTL — de enige cross-instance mutex),policy.json,lfs/objects/,events/cursor.json.
Een push: Onze receive-pack indexeert de pack (via git index-pack --fix-thin --rev-index in een scratch dir), controleert connectiviteit en policy, uploadt de pack ∥ idx ∥ log entry, en voert vervolgens de CAS uit op het manifest. Bij een 412-fout wordt alles opnieuw gelezen, worden de oude waarden van elke ref gevalideerd en volgt een retry. Gelijktijdige pushes naar één repository op één instantie worden group-committed in één CAS. De client ziet pas "ok" nadat de bucket dit heeft bevestigd.
Een read: Eén conditional GET van het manifest. Bij een 304 wordt geserveerd vanuit de lokale kopie, bij een 200 worden de nieuwe entries toegepast. Wat "toepassen" betekent hangt af van het verzoek:
- Refs: (snapshot + log → packed-refs, geen packs: advertisements, de API, bundle lijsten).
- Serve: De pack set zoals deze machine die kan houden: kleine packs en de history pack lokaal, een te grote base wordt via range requests gelezen.
- Full: Alles lokaal, voor repacks.
- Objects: De remote reader, voor de UI op een repository die niet op de machine past.
Pack-downloads draaien op hun eigen runtime en blokkeren nooit een refs-verzoek.
Placement is configuratie. [placement] serve / maintain globs bepalen welke repositories een host object-werk voor doet; refs-level reads werken overal. Eén box: gebruik de defaults. Meerdere boxen: zet de monorepo op de host met de SSD (cache.mode = "disk"), de rest op de kleinere machines, en routeer via /<owner>/<repo> ervoor.
Niets wacht in stilte. Alles wat traag is, is een taak met een id, een log en een progress stream — verteld aan git op sideband 2 (remote: * …) en aan de browser via SSE.
Voor de volledige architectuur en handleiding, inclusief constraints en het kostenmodel, zie AGENTS.md.
Uitvoeren
Build
Vereist Rust (volgens rust-toolchain.toml), protoc, node 24 + pnpm voor de web UI.
just web-build && cargo build --release -p walgit-cli
# Of: nix build .#walgit of: podman build -t walgit -f Containerfile .
Starten (Eén machine)
Voor TLS via walgit zelf en een lokale S3 store (rustfs in een container):
just dev-store
./target/release/walgit-server --config walgit.standalone.toml
open https://walgit.localhost:8080/
Configuratiebestanden:
walgit.standalone.toml: Voor één machine (self-signed TLS, rustfs, alle rollen).walgit.example.toml: Bevat elke key met defaults en commentaar.Containerfile,flake.nix: OCI image en Nix package/devshell.deploy/nginx.conf.example: Optionele nginx ervoor voor publieke TLS, auth_request per credential, en byte offload viaX-Accel-Redirect.
Rollen (server.roles):
serve: git, API, UI, bundles, LFS.maintain: checkpoints, bundles, compaction, fsck/repair.events: de webhook bridge.- (Leeg = alle rollen). Elk aantal
servehosts mag naar één bucket wijzen; geef elke repository éénmaintainer(via placement globs).
Authenticatie
| Mode | Toegang | Git Authenticatie |
|---|---|---|
| none | Iedereen is anoniem met schrijfrechten | Geen |
| token | Statische tokens in de config (token_env leest geheim uit env) | Authorization: Bearer <token>, of token als HTTP Basic password |
| oidc | Elke OpenID Connect issuer (Google, Entra, Okta, etc.) | Een walgit access token (via browser sign-in op /auth/tokens). Stateless (HMAC met sessionsecret). ID tokens van de issuer werken ook. |
Ontwikkelen
Testen
just test: Snelle hermetische tier (< 1 min): unit + quick integration, in-memory store, echte git.just e2e: Echte git tegen de server (~20 s).just warnings: Geen rustc warnings over alle targets.just ci: Alle bovenstaande.cargo test -p walgit-server --test sim: Fault-injection simulatie (crashes, partitions, stale reads).just test-s3: Store contract tegen lokale rustfs.
Code Map
crates/walgit-proto: Protobuf schema (wal.proto), log framing, store keys.crates/walgit-store:ObjectStoretrait (CAS versions, conditional GET, range, compose); backends s3, gcs, memory; leases.crates/walgit-git: Bare repos op disk, receive-pack, pack ingest, refs $\leftrightarrow$ packed-refs, advertisements, upload-pack drivers.crates/walgit-wal:RepoHandle: sync levels, publish (group commit + CAS), checkpoints, log reader, remote reader, tasks.crates/walgit-bundle:bundle-uri: slots en chains, building, header $\circ$ pack composition, lists, retention.crates/walgit-server: Axum: smart HTTP, LFS, bundles, auth, de maintainer loop, upstream follow, web/ (API, UI, SDK routes, SSE), setup.rs, events bridge.crates/walgit-config:walgit.toml(+WALGIT__env overrides), per-repo settings merge, fail-closed validation.crates/walgit-cli: CLI tools (serve|import|compact|bundle|wal|mirror|synth|config|repo).web/: React SPA (Vite) +sdk/repos.ts.docs/: Documentatie over BUNDLEURIDESIGN, ROUNDTRIPS (kostenmodel), POLICY, LFS, INTEGRITY, EVENTS, CONTRACT.
Belangrijke Invarianten
- De manifest CAS is het enige commit-punt; alles daarvóór is onzichtbaar, alles daarna is idempotent en replayable.
- Onveranderlijke (immutable) objecten zijn content-addressed; niets wordt overschreven behalve het manifest, de bundle-lijst en leases.
- Elke read valideert eerst tegen de bucket; er bestaat geen "eventually consistent".
- Lokale schijf is een cache. Geheugen is een cache. De bucket is de repository.
- Placement is geconfigureerd, nooit afgeleid; refs-level reads werken overal, object-werk alleen waar geplaatst.
- De output van de maintainer is een pure functie van (config, WAL); wat ontbreekt is simpelweg "nog niet gebouwd".
- Kosten mogen niet schalen met het aantal refs op een hot path, noch met de pack-grootte op een machine die te klein is voor de pack.
- Langdurig werk is een taak: ontdekbaar, koppelbaar en voorzien van narratie.
- "Correct" is niet voldoende: elke protocolwijziging wordt beoordeeld op het aantal round trips naar de bucket (zie
docs/ROUNDTRIPS.md).
Licentie
MIT — zie LICENSE.
Groetjes,