gitmote

gitmote / docs / architecture / storage.md  ·  history  ·  branches

5341 bytes · raw

Components & storage

Part of the gitmote architecture.

Components

┌──────────────────────────────────────────────────────────────┐
│  gitmote container (Go, single writer)                       │
│                                                              │
│   HTTP  ─┬─ smart-HTTP handler ── spawns ── git http-backend │
│          │                                    (stock git)    │
│          ├─ web UI (dashboard, repos, tokens, ACLs, CI)      │
│          └─ embedded s3lite (*sql.DB)                        │
│                                                              │
│   ephemeral disk: working bare repos (a CACHE, disposable)   │
└───────────┬─────────────────────────────────┬────────────────┘
            │ objects / packs                 │ WAL replication (litestream)
            ▼                                 ▼
      ┌───────────┐                     ┌───────────┐
      │    S3     │                     │  S3 (WAL) │  ← s3lite's backup target
      │ objects/  │                     └───────────┘
      │ lfs/      │
      └───────────┘

Storage layout

S3 (immutable — content before pointer)

Mirrors the on-disk bare repo's immutable directories, under the storage root (the whole bucket, or a bucket/base sub-path — see ops.md). Refs are deliberately excluded — they live in s3lite (also under the root, at meta/, so objects and metadata share fate). Keys below are relative to the root.

Key Contents
{repo}/objects/… Loose git objects (git's own ab/cdef… fan-out)
{repo}/objects/pack/pack-*.pack .idx Packfiles + indexes
ci/{repo}/{run}/{job}.log CI run logs (append-only)
lfs/{repo}/{oid} Large-file blobs (deferred — see open questions)

Sync is done with the S3 SDK directly (single static binary, no external dependency); rclone is a zero-code fallback. New objects/packs are PUT after receive-pack; on fetch, missing objects are pulled on demand.

s3lite schema (mutable — the reason s3lite is here)

The schema is defined once, in internal/meta/schema.sql — the single source of truth, applied idempotently (CREATE ... IF NOT EXISTS) on every Open, with a guarded ALTER migration in internal/meta/meta.go for additive columns the version-less store can't express in schema.sql alone. It is self-documenting (every column is commented); this doc keeps only the cross-cutting rules the DDL can't state. The tables, grouped: repos / refs (repos and their mutable pointers — refs are the whole reason this DB exists); users / tokens / ssh_keys / acls (identity and access); ci_runs / ci_jobs / ci_secrets (CI); and server_secrets (auto-provisioned cookie/worker keys). The rules that live here, not in the DDL:

HEAD is not a row — it derives from repos.default_branch. Every refs row holds a concrete object id; symbolic refs beyond HEAD are not stored.

users.is_admin is the global administrator flag — the role that may create users and repos and manage ACLs (the entry point is gitmote bootstrap, which mints the first admin). It is distinct from acls.perm='admin', which is per-repo; day-to-day repo access is always governed by acls.

A PAT is a selector.secret pair. The selector is a non-secret, unique, indexed lookup key; only the verifier — SHA-256 of the secret half — is stored. Verification looks the row up by selector, then compares the verifier in constant time, so neither a timing side-channel on the lookup nor a database leak yields a usable token. The raw selector.secret is shown to the user exactly once at mint time and never persisted.