irongit

Git hosting and a container registry in one Rust binary (axum + Astro)

45 commits
Clone ▾
HTTPS
SSH
CLI
README.md

irongit

Git hosting and a container registry in one binary, built on the astrum template (axum + Astro). Users and organizations share one namespace; repos and images are public or private independently; profiles have a contribution heatmap; ig is the CLI.

What runs where

Piece Where it lives
Accounts, permissions, metadata, sessions, tokens Postgres
Git repositories (bare) Server disk, DATA_DIR/repos/{id}.git (by id, so renames never move files)
Git LFS objects R2 lfs/, uploaded and downloaded by clients through presigned URLs
Image layers and configs R2 registry/blobs/, pulls are 307 redirects to presigned URLs
Image manifests and tags Postgres
Avatars R2 avatars/
CLI releases R2 releases/cli/
Backups (git bundles + pg_dump) R2 backups/
Logs DATA_DIR/logs/ (server.*.log, frontend.*.log, hooks.*.log)

Git transport and browsing go through the system git binary (it must be on PATH). Pushes run a pre-receive hook (irongit hook pre-receive, the same binary) that rejects files over MAX_FILE_MB and pushes over the owner's quota, pointing people at Git LFS.

Develop

Requirements: Rust, bun, git, watchexec, Postgres (local dev uses the container on 5432, database irongit).

./dev.sh          # http://localhost:7878, git SSH on 2222

Configuration is .env at the repo root; every variable is described in secrets.md (gitignored). The first account registered becomes site admin; irongit admin promote <user> makes more.

cargo test -p irongit             # unit tests
cargo run -p irongit -- admin check-storage   # verify R2 credentials

Build

./build.sh
# target/release/irongit                          server (x86_64 Linux)
# target/x86_64-unknown-linux-musl/release/ig      CLI, static

Publish a CLI build so install.sh and ig upgrade serve it:

irongit admin publish-cli target/x86_64-unknown-linux-musl/release/ig --version 0.1.0

The ig CLI

curl -fsSL http://localhost:7878/install.sh | sh    # installs ig and docker-credential-ig
ig login                       # approve in the browser; sets up git and docker helpers
ig repo create api --private
ig repo clone you/api
docker push localhost:7878/you/api:1.0
ig image visibility you/api public
ig ssh-key add                 # ~/.ssh/id_ed25519.pub by default
ig import github my-github-name  # bring a GitHub account over (uses your gh login)
ig upgrade

Full reference at /docs/cli; git, SSH and LFS at /docs/git; the registry at /docs/registry; GitHub imports at /docs/import.

Importing from GitHub

/new/import (or ig import github OWNER) copies a GitHub user's or organization's repositories: every branch and tag, Git LFS objects (into R2), description, default branch, visibility and archived state; optionally the profile; and, for your own account, your GitHub-verified emails so imported commits count on your heatmap. It runs as a background job on the server (backend/src/importer/). GitHub tokens are held in memory only and OAuth import tokens are revoked when the job ends. Sign in with GitHub and "Connect GitHub" need GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET (see secrets.md).

Language stats

Repo pages show a GitHub-style language bar and profiles a chart of every language across the account's repositories. Files are classified with GitHub Linguist's own data (backend/src/languages/linguist.json): its language table (extensions, filenames, shebang interpreters, groups, colors), its vendored, documentation and generated rules, and its content heuristics for shared extensions like .h or .sql. .gitattributes overrides (linguist-vendored, linguist-language=...) are honored. Results are cached per commit and recomputed when the default branch moves.

irongit admin languages huncholane/irongit --files   # every file and why it counts or not
irongit admin languages path/to/repo.git             # any git directory, no database
python3 scripts/gen-linguist.py v9.8.0               # update to a newer Linguist release

After regenerating the data, add a migration with delete from repo_languages; so every repository is recounted.

Tests

cargo test --workspace            # unit tests (server, CLI, shared types)
scripts/e2e/ssh-lfs.sh            # SSH, LFS end to end against a running server
scripts/e2e/backup.sh             # backup to R2 and restore every bundle

Layout

backend/
  migrations/        Postgres schema
  src/main.rs        startup, subcommands (serve, hook, admin)
  src/auth.rs        sessions, personal access tokens, extractors
  src/perm.rs        every access decision (repos, images, orgs)
  src/storage.rs     R2 over the S3 API (presigned URLs, multipart)
  src/git.rs         git CLI wrapper used by the web UI
  src/git_http.rs    smart HTTP (v0, v1, v2)
  src/transport.rs   spawning upload-pack/receive-pack with hooks and limits
  src/hook.rs        pre-receive size and quota checks
  src/push.rs        post-push: events, default branch, heatmap contributions
  src/languages/     language stats (Linguist data and rules)
  src/ssh.rs         built-in SSH server
  src/lfs.rs         Git LFS batch API
  src/registry/      OCI distribution API (docker push/pull)
  src/api/           JSON API for the CLI, device login, CLI releases
  src/backup.rs      scheduled backups to R2
  src/web/           server-rendered pages (maud) poured into the Astro shell
cli/                 the ig CLI
shared/              types shared by the API and the CLI
frontend/            Astro: landing, docs, the page shell, CSS and client JS

Deploying

Production is https://git.hygo.ai on the rybbit server (ssh rybbit), as the irongit service in /opt/hygo/docker-compose.yml: same shared Postgres (database irongit) and Caddy as the other hygo services, config in /opt/hygo/irongit.env, data in the irongit-data volume.

./deploy.sh      # build the image, docker load it on rybbit, roll irongit, wait for healthy,
                 # publish the bundled ig to R2 when cli/Cargo.toml's version changed

Pushing main to git.hygo.ai deploys automatically: .githooks/pre-push runs ./deploy.sh first and stops the push if the deploy fails (enable it per clone with git config core.hooksPath .githooks; skip once with git push --no-verify or SKIP_DEPLOY=1 git push). It refuses to deploy uncommitted or untracked files, since the image is built from the folder.

Roll back:

ssh rybbit 'cd /opt/hygo && docker tag irongit:$(cat .irongit.prev) irongit:latest && docker compose up -d --no-deps irongit'

The DNS record is DNS-only (grey cloud): the Cloudflare proxy caps request bodies at 100 MB, which breaks large docker push layers and big HTTPS git pushes. Caddy gets its own Let's Encrypt certificate. Git over SSH is published directly on port 2222 (ssh://git@git.hygo.ai:2222/owner/repo.git). Admin commands run inside the container, e.g. ssh rybbit docker exec irongit irongit admin promote <user>.