Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,9 @@ machines whose "disk" is 20 GiB of tmpfs, next to a long tail of small repositor
| `fsck.pb` | Last connectivity audit (`FsckReport`), written by the maintainer's `fsck` unit, consumed by `repair` (`docs/INTEGRITY.md`). |
| `events/cursor.json` | Durable acknowledged WAL sequence of the events bridge; advanced only after the webhook acknowledged (D32). |
| `lfs/objects/<aa>/<bb>/<oid>` | LFS objects (sha256-addressed, immutable). Missing ones can be read through from `upstream.lfs` and persisted (`docs/LFS.md`). |
Schema `crates/walgit-proto/proto/walgit/v1/wal.proto`; GCS over gRPC, S3 (AWS SDK) and in-memory stores share
one contract suite (`crates/walgit-store/tests/contract.rs`, incl. compose).
Schema `crates/walgit-proto/proto/walgit/v1/wal.proto`; GCS over gRPC, S3 (AWS SDK), Azure Blob
(`azure_storage_blob`) and in-memory stores share one contract suite
(`crates/walgit-store/tests/contract.rs`, incl. compose on the backends that support it).

### 2.2 Write path
receive-pack (ours, `walgit-git/src/receive.rs`) → index the pack locally (`git index-pack --stdin --fix-thin
Expand Down Expand Up @@ -275,9 +276,11 @@ decision in §4 — or the PR is; never "fix later".
table per chunk per thread (60 M entries × 44 threads ⇒ 178 GB RSS) and is the only place gix writes an object id
into a pack (a mid-pack refresh once paired offsets with another pack's table ⇒ a wrong id). The gix pack source
is a frozen snapshot (`frozen_pack_source`). Reproducer: `walgit-git/tests/upload_gix_scale.rs`.
- **D3** `ObjectStore` trait with CAS version tokens, conditional GET, range, compose; gcs/s3/memory backends.
`compose` is native on GCS and a multipart `UploadPartCopy` on S3 (`compose_is_native` tells callers which);
`accel_target` gives an edge a URL (+ bearer on GCS, presigned on S3) to fetch an object itself.
- **D3** `ObjectStore` trait with CAS version tokens, conditional GET, range, compose; gcs/s3/azure/memory
backends. `compose` is native on GCS and a multipart `UploadPartCopy` on S3 (`compose_is_native` tells callers
which; azure declines compose — `supports_compose = false` — and callers fall back to the byte path);
`accel_target` gives an edge a URL (+ bearer on GCS, presigned on S3) to fetch an object itself (azure has no
`accel_target`; it signs user-delegation SAS URLs for `signed_get_url` only).
- **D4** protobuf on the wire and in the bucket; schema versioned, append-only.
- **D5** Repo identity `<owner>/<repo>[.git]`, prefix `repos/<o>/<r>/`, creation = CAS create of the manifest.
- **D6** Manifest CAS is the only commit point. **D7** No node identity, no elections; leases for exclusivity.
Expand Down Expand Up @@ -424,7 +427,9 @@ Decision identifiers are stable; gaps in the numbering are intentional.
streamed by walgit). Anything an edge takes over is announced per request in `X-Walgit-Capabilities`; never
infer an edge from config, never hardcode a hostname in `crates/` or `web/`.
- **S3 and GCS are both first class.** Every store feature has both implementations and runs in the contract
suite (`just test-s3` against rustfs, `just test-gcs <bucket>`); "GCS only" is a bug.
suite (`just test-s3` against rustfs, `just test-gcs <bucket>`); "GCS only" is a bug. Azure Blob is the third
backend and runs the same suite (`just test-azure <account>`), minus the optional capabilities it declines
(`compose`, `accel_target`).
- **Use the rig before prod** (`just dev-store` → `walgit-server --config walgit.standalone.toml`). Per-repo
settings (D24) with minute-scale slots compress a week of bundle behaviour into 30 minutes.
- No new auth paths (§1.3). No LIST on hot paths. No unbounded buffering of packs in memory. No full
Expand Down
171 changes: 171 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ google-cloud-auth = "1"
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1"
aws-smithy-types = "1"
# Azure Blob. `azure_storage_blob` is pinned exactly: it is a beta whose client and
# option types still move between patch releases. `azure_core` is pulled in directly
# so the transport is rustls (never openssl) and so `hmac_rust` is available for
# user-delegation SAS signing; `xml` is what the blob list/commit payloads need.
azure_storage_blob = { version = "=1.1.0-beta.2", default-features = false, features = ["tokio"] }
azure_identity = "1.0.0"
azure_core = { version = "1", default-features = false, features = ["reqwest_rustls", "hmac_rust", "xml"] }
clap = { version = "4", features = ["derive", "env"] }
rand = "0.9"
sha1 = "0.10"
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# walgit — a git server that is one binary in front of an object store

walgit hosts git repositories with **no database, no leader and no local state that matters**. You run a
single binary, point it at an S3 or GCS bucket, and you have: smart HTTP (v0/v2) fetch and push, `bundle-uri`
clones served as static files, Git LFS, a browsing web UI, a JSON API with an SDK, per-repository push policy,
webhooks — and a server that scales to repositories **larger than the machine it runs on**. Every machine that
runs walgit is a disposable cache; the bucket is the repository.
single binary, point it at an S3, GCS or Azure Blob bucket, and you have: smart HTTP (v0/v2) fetch and
push, `bundle-uri` clones served as static files, Git LFS, a browsing web UI, a JSON API with an SDK,
per-repository push policy, webhooks — and a server that scales to repositories **larger than the machine it
runs on**. Every machine that runs walgit is a disposable cache; the bucket is the repository.

```sh
# 1. a bucket (any S3-compatible store or GCS) and a config
# 1. a bucket (any S3-compatible store, GCS, or an Azure Blob container) and a config
cat > walgit.toml <<'EOF'
[server]
listen = "0.0.0.0:8080"
Expand Down Expand Up @@ -79,7 +79,7 @@ server entirely (**bundle-uri**: fresh clones and catch-ups are static files the
| **events** | A small bridge tails the WAL and POSTs ref events to a webhook, exactly-once per (repo, seq, ref) with a durable cursor. `docs/EVENTS.md`. |
| **maintenance** | Checkpoints, bundle builds, geometric compaction, base rebuilds, connectivity audits and repairs — one loop that computes the desired state from (config, WAL) every pass and does one bounded unit of the most important missing work. Self-healing by construction: an outage leaves no holes; a deleted artefact is "missing" and rebuilt identically. |
| **auth** | `none` (loopback), `token` (static tokens), `oidc` (any OpenID Connect issuer: browser sign-in, ID tokens, and walgit-issued access tokens for git). `/services/public/install.sh` sets a developer's machine up in one idempotent command. |
| **stores** | S3 and S3-compatible (AWS, MinIO, rustfs, R2, Ceph, …) and GCS, first class; an in-memory store for tests. |
| **stores** | S3 and S3-compatible (AWS, MinIO, rustfs, R2, Ceph, …) and GCS, first class; Azure Blob (Entra credentials, user-delegation SAS URLs); an in-memory store for tests. |

## How it works, briefly

Expand Down Expand Up @@ -165,7 +165,7 @@ Code map:
```
crates/
walgit-proto protobuf schema (wal.proto), log framing, store keys
walgit-store ObjectStore trait (CAS versions, conditional GET, range, compose); backends s3, gcs, memory; leases
walgit-store ObjectStore trait (CAS versions, conditional GET, range, compose); backends s3, gcs, azure, memory; leases
walgit-git bare repos on disk, receive-pack, pack ingest, refs ↔ packed-refs, advertisements, upload-pack drivers
walgit-wal RepoHandle: sync levels, publish (group commit + CAS), checkpoints, log reader, remote reader, tasks
walgit-bundle bundle-uri: slots and chains, building, header ∘ pack composition, lists, retention
Expand Down
6 changes: 6 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ allow-expect-in-tests = true
allow-panic-in-tests = true
allow-indexing-slicing-in-tests = true
allow-dbg-in-tests = true

# The MSRV clippy must respect. `[workspace.package] rust-version` is not inherited by
# the member crates, so clippy would otherwise assume the current toolchain and suggest
# APIs newer than 1.90 (`Duration::from_hours`, `from_mins`, ...) — the lint gate is
# `-D warnings`, so those suggestions would push code past the MSRV we declare.
msrv = "1.90"
55 changes: 55 additions & 0 deletions crates/walgit-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ pub struct StoreConfig {
pub prefix: String,
pub gcs: GcsConfig,
pub s3: S3Config,
pub azure: AzureConfig,
pub max_retries: u32,
/// Objects larger than this use resumable/multipart upload.
pub multipart_threshold: ByteSize,
Expand All @@ -240,6 +241,7 @@ pub enum StoreBackend {
#[default]
Gcs,
S3,
Azure,
/// Tests only.
Memory,
}
Expand Down Expand Up @@ -280,6 +282,31 @@ pub struct S3Config {
pub force_path_style: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AzureCredentialKind {
/// az CLI / azd login (dev machines).
#[default]
DeveloperTools,
/// `AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` env vars.
ClientSecret,
ManagedIdentity,
WorkloadIdentity,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct AzureConfig {
/// Storage account name (required; `bucket` is the container).
pub account: String,
/// "" = `https://<account>.blob.core.windows.net`; override for a sovereign
/// cloud or a custom domain. Must be `https`: the SDK rejects a plain-http
/// endpoint whenever a credential is attached, and this backend always
/// attaches one — so an http emulator such as Azurite cannot be reached.
pub endpoint: String,
pub credential: AzureCredentialKind,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct CacheConfig {
Expand Down Expand Up @@ -1078,6 +1105,7 @@ impl Default for StoreConfig {
prefix: String::new(),
gcs: GcsConfig::default(),
s3: S3Config::default(),
azure: AzureConfig::default(),
max_retries: 8,
multipart_threshold: ByteSize::mib(64),
multipart_part_size: ByteSize::mib(32),
Expand Down Expand Up @@ -1730,6 +1758,33 @@ mod tests {
assert_eq!(c.server.listen.port(), 9090);
}

#[test]
fn azure_store_config_parses() {
let toml = r#"
[store]
backend = "azure"
bucket = "walgit"
[store.azure]
account = "myacct"
credential = "client_secret"
"#;
let c: Config = toml::from_str(toml).expect("parse");
assert_eq!(c.store.backend, StoreBackend::Azure);
assert_eq!(c.store.azure.account, "myacct");
assert_eq!(c.store.azure.endpoint, "");
assert_eq!(c.store.azure.credential, AzureCredentialKind::ClientSecret);
}

#[test]
fn azure_config_defaults() {
let c = Config::default();
assert_eq!(c.store.azure.account, "");
assert_eq!(
c.store.azure.credential,
AzureCredentialKind::DeveloperTools
);
}

#[test]
fn port_rewrites_loopback_public_url_only() {
let mut c = Config::default();
Expand Down
Loading