gitmote / README.md · history · branches
8001 bytes · raw
Your own, personal, GitHub.
A tiny self-hosted git remote. One Go container speaks git's smart-HTTP protocol; repositories live in S3-compatible object storage and mutable metadata (refs, users, keys, access) in s3lite. Use it instead of GitHub for a handful of repos and a few invited people — it is explicitly not trying to be GitHub at scale.
The one interesting trick: git's data is split by mutability — immutable objects/packs to S3, mutable refs and forge metadata to s3lite (plain SQLite, backed to S3) — so the only genuinely hard problem, atomic ref updates, becomes a single SQL transaction.
Status: early implementation — git read/write over smart-HTTP works
(clone/fetch/push with token auth and per-repo ACLs), and gitmote hosts its own
source repo end-to-end: locally against MinIO (make e2e-local) and in
production on Scaleway Object Storage at
gitmote.atmin.net. The design lives in
docs/architecture/.
One container, one bucket. S3 is the single source of truth and the container is disposable: kill it and restart — it restores the whole forge from S3 and continues. Point it at any S3-compatible bucket with credentials:
docker run --rm -p 8080:8080 \
-e GITMOTE_S3_BUCKET=my-gitmote-bucket \
-e AWS_REGION=us-east-1 \
-e AWS_ACCESS_KEY_ID=… -e AWS_SECRET_ACCESS_KEY=… \
ghcr.io/atmin/gitmote:master
# Published tags: :master (latest on master) and :<git-sha>. There is no :latest.
# Non-AWS S3 (Scaleway, MinIO, R2): add -e GITMOTE_S3_ENDPOINT=https://s3.fr-par.scw.cloud
# Keep the local cache across restarts (optional): -v gitmote-data:/data
On the first run gitmote auto-bootstraps an admin and prints a one-time access
token to the logs behind a SAVE IT NOW banner — grab it there (no setup page).
Then sign in at http://localhost:8080/login by pasting it, and clone/push:
git clone http://admin:<token>@localhost:8080/<repo> # create the repo on the dashboard (/) first
Lost the token? It's never stored (only its hash), so re-mint one — stop the container, then:
docker run --rm -e GITMOTE_S3_BUCKET=… -e AWS_… ghcr.io/atmin/gitmote:master bootstrap -reissue
Nothing else is required: the session cookie key and CI worker secret are generated and persisted on first run. See docs/ops.md for the full env surface, CI, and the Scaleway deployment.
Because the bucket is the forge, you don't need any always-on compute. Keep the
bucket in the cloud, run gitmote locally only when you want to push or pull,
and stop it afterwards — your git remote is localhost, but the durable state
lives in S3. This is the cheapest way to self-host: you pay for object storage,
not a running server.
# Start the forge against your CLOUD bucket, then push/pull, then Ctrl-C.
docker run --rm -p 8080:8080 \
-e GITMOTE_S3_BUCKET=my-gitmote-bucket \
-e GITMOTE_S3_ENDPOINT=https://s3.fr-par.scw.cloud \ # your provider's endpoint
-e AWS_REGION=fr-par \
-e AWS_ACCESS_KEY_ID=… -e AWS_SECRET_ACCESS_KEY=… \
ghcr.io/atmin/gitmote:master
git remote add gitmote http://admin:<token>@localhost:8080/<repo>
git push gitmote main
Point at the same storage root (GITMOTE_S3_BUCKET), endpoint, and region as
before. Objects and metadata share that root, so a later instance either finds the
whole forge or, pointed at the wrong root, sees a clean empty one — never a
half-restored state.
Use your existing token. A populated bucket already has an admin; don't
re-bootstrap it — sign in with the token you saved (or bootstrap -reissue to
mint a fresh one).
Only one writer at a time — and that's enforced, not your job. gitmote takes a
single-writer lease on the storage root, so running a second instance (or a stray
cloud container) can't corrupt anything: whoever holds the lease writes, the other serves
reads and returns a retryable 503 on push. So a local on-demand forge is safe
even if you also run gitmote in the cloud — no coordination, no harm. Details in
docs/ops.md (“Single writer is a correctness invariant”).
make dev gives you a running instance in one command: it builds the binaries,
starts MinIO in a container (S3 on :9100), runs gitmote natively on :8080,
and ensures an admin + a gitmote repo (auto-bootstrap), minting a fresh
token each run and printing it. State (the metadata DB and object cache) persists
under data/, so repos and history survive restarts:
make dev # first run prints the token, clone URL, and UI URL
make dev-reset # wipe MinIO + data/ for a clean slate
Sign in to the UI at http://localhost:8080/ by pasting the token, or clone/push straight away with the printed URL. Requires Docker + Docker Compose.
CI works locally too. Push a repo with .gitmote/workflows/*.yml and gitmote
records a run and executes it on the spot — it spawns gitmote-runner as a local
process (the same runner code the cloud path runs on Scaleway, just a local
substrate) which runs the workflow with act.
Install act (brew install act); it uses the same Docker/podman daemon MinIO
does. Watch runs in the UI. gitmote reads .gitmote/workflows (GitHub-workflow
syntax, run by act) — not .github/workflows, so a repo mirrored to GitHub
chooses its forge by where the workflow lives and never double-runs.
make e2e-local brings up a gitmote container plus MinIO with
docker-compose.yml, bootstraps an admin/token/repo, then
pushes this working tree, clones it back, and — after force-recreating the
container — clones again to prove the repo survives on the object store and
persisted refs (not local disk). Requires Docker + Docker Compose.
make e2e-restore additionally exercises the litestream cold-start path (wipes
the metadata DB and restores it from S3).
make e2e-build proves the GITMOTE_CI_ALLOW_BUILDS gate: it runs act in a
privileged docker-in-docker box (a rootful daemon in a box) with the daemon socket
suppressed (a docker build step fails) then mounted (it succeeds), so the safe
default and the opt-in are both verified against a real build. Skips cleanly with
no daemon. Deployment — Scaleway Serverless Containers, single writer,
gitmote.atmin.net — lives in docs/ops.md.
An empty instance has no users, so token auth would be a chicken-and-egg — so the
server auto-bootstraps on first run: when it is the writer and no admin
exists, it creates the admin user and prints a one-time token to the logs (see
the Quickstart). No second command.
You can still bootstrap by hand against the bucket before the server is live
(gitmote bootstrap [-handle …] [-repo …]), and recover a lost token with
gitmote bootstrap -reissue — both refuse to clobber and print the token once.
Only the token's hash is ever stored, so a lost token is re-minted, never
recovered.
The bare root / is a dashboard: a viewer-scoped repo list (public repos to
anyone, private to those with access, all to an admin) with browse pages for each
repo. Sign in at /login by pasting a token (the same PAT format git uses) — any
user may sign in to see the repos they can access; the server issues a signed,
stateless session cookie keyed by an auto-generated, persisted secret (override
with GITMOTE_COOKIE_KEY).
The things you can't do over git are admin-only: create repos and mint/revoke
tokens from the top-level /users and /tokens pages, and manage a repo from its
/<repo>/settings (visibility, default branch), /<repo>/access (invite
spectators/collaborators), and /<repo>/secrets pages.
git push gitmote — reach out and touch faith. 🎶