gitmote

gitmote  Β·  history  Β·  branches

NameModeSize
πŸ“„ .dockerignore 100644101
πŸ“„ .env.example 1006441041
πŸ“ .github/ 040000
πŸ“„ .gitignore 10064433
πŸ“ .gitmote/ 040000
πŸ“„ .golangci.yml 100644535
πŸ“„ CLAUDE.md 100644763
πŸ“„ CONTRIBUTING.md 1006443711
πŸ“„ Dockerfile 1006441857
πŸ“„ Dockerfile.runner 1006441609
πŸ“„ LICENSE 10064411357
πŸ“„ Makefile 1006445271
πŸ“„ README.md 1006448001
πŸ“ cmd/ 040000
πŸ“„ docker-compose.dev.yml 1006441367
πŸ“„ docker-compose.selfdeploy.yml 1006441202
πŸ“„ docker-compose.yml 1006441739
πŸ“ docs/ 040000
πŸ“„ go.mod 1006443615
πŸ“„ go.sum 10064433948
πŸ“ internal/ 040000
πŸ“ scripts/ 040000
πŸ“ tasks/ 040000

gitmote

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/.

Quickstart β€” run it anywhere

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.

Remote bucket, local on-demand forge

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”).

Develop locally

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.

Run it locally

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.

Bootstrap

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.

Web UI

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. 🎢