Git hosting and a container registry in one Rust binary (axum + Astro)
| 1 | # irongit |
| 2 | |
| 3 | Git hosting and a container registry in one binary, built on the astrum |
| 4 | template (axum + Astro). Users and organizations share one namespace; repos |
| 5 | and images are public or private independently; profiles have a contribution |
| 6 | heatmap; `ig` is the CLI. |
| 7 | |
| 8 | ## What runs where |
| 9 | |
| 10 | | Piece | Where it lives | |
| 11 | | --- | --- | |
| 12 | | Accounts, permissions, metadata, sessions, tokens | Postgres | |
| 13 | | Git repositories (bare) | Server disk, `DATA_DIR/repos/{id}.git` (by id, so renames never move files) | |
| 14 | | Git LFS objects | R2 `lfs/`, uploaded and downloaded by clients through presigned URLs | |
| 15 | | Image layers and configs | R2 `registry/blobs/`, pulls are 307 redirects to presigned URLs | |
| 16 | | Image manifests and tags | Postgres | |
| 17 | | Avatars | R2 `avatars/` | |
| 18 | | CLI releases | R2 `releases/cli/` | |
| 19 | | Backups (git bundles + pg_dump) | R2 `backups/` | |
| 20 | | Logs | `DATA_DIR/logs/` (`server.*.log`, `frontend.*.log`, `hooks.*.log`) | |
| 21 | |
| 22 | Git transport and browsing go through the system `git` binary (it must be on |
| 23 | PATH). Pushes run a pre-receive hook (`irongit hook pre-receive`, the same |
| 24 | binary) that rejects files over `MAX_FILE_MB` and pushes over the owner's |
| 25 | quota, pointing people at Git LFS. |
| 26 | |
| 27 | ## Develop |
| 28 | |
| 29 | Requirements: Rust, bun, git, watchexec, Postgres (local dev uses the |
| 30 | container on 5432, database `irongit`). |
| 31 | |
| 32 | ```sh |
| 33 | ./dev.sh # http://localhost:7878, git SSH on 2222 |
| 34 | ``` |
| 35 | |
| 36 | Configuration is `.env` at the repo root; every variable is described in |
| 37 | `secrets.md` (gitignored). The first account registered becomes site admin; |
| 38 | `irongit admin promote <user>` makes more. |
| 39 | |
| 40 | ```sh |
| 41 | cargo test -p irongit # unit tests |
| 42 | cargo run -p irongit -- admin check-storage # verify R2 credentials |
| 43 | ``` |
| 44 | |
| 45 | ## Build |
| 46 | |
| 47 | ```sh |
| 48 | ./build.sh |
| 49 | # target/release/irongit server (x86_64 Linux) |
| 50 | # target/x86_64-unknown-linux-musl/release/ig CLI, static |
| 51 | ``` |
| 52 | |
| 53 | Publish a CLI build so `install.sh` and `ig upgrade` serve it: |
| 54 | |
| 55 | ```sh |
| 56 | irongit admin publish-cli target/x86_64-unknown-linux-musl/release/ig --version 0.1.0 |
| 57 | ``` |
| 58 | |
| 59 | ## The ig CLI |
| 60 | |
| 61 | ```sh |
| 62 | curl -fsSL http://localhost:7878/install.sh | sh # installs ig and docker-credential-ig |
| 63 | ig login # approve in the browser; sets up git and docker helpers |
| 64 | ig repo create api --private |
| 65 | ig repo clone you/api |
| 66 | docker push localhost:7878/you/api:1.0 |
| 67 | ig image visibility you/api public |
| 68 | ig ssh-key add # ~/.ssh/id_ed25519.pub by default |
| 69 | ig import github my-github-name # bring a GitHub account over (uses your gh login) |
| 70 | ig upgrade |
| 71 | ``` |
| 72 | |
| 73 | Full reference at `/docs/cli`; git, SSH and LFS at `/docs/git`; the |
| 74 | registry at `/docs/registry`; GitHub imports at `/docs/import`. |
| 75 | |
| 76 | ## Importing from GitHub |
| 77 | |
| 78 | `/new/import` (or `ig import github OWNER`) copies a GitHub user's or |
| 79 | organization's repositories: every branch and tag, Git LFS objects (into R2), |
| 80 | description, default branch, visibility and archived state; optionally the |
| 81 | profile; and, for your own account, your GitHub-verified emails so imported |
| 82 | commits count on your heatmap. It runs as a background job on the server |
| 83 | (`backend/src/importer/`). GitHub tokens are held in memory only and OAuth |
| 84 | import tokens are revoked when the job ends. Sign in with GitHub and "Connect |
| 85 | GitHub" need `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` (see secrets.md). |
| 86 | |
| 87 | ## Language stats |
| 88 | |
| 89 | Repo pages show a GitHub-style language bar and profiles a chart of every |
| 90 | language across the account's repositories. Files are classified with |
| 91 | GitHub Linguist's own data (`backend/src/languages/linguist.json`): its |
| 92 | language table (extensions, filenames, shebang interpreters, groups, colors), |
| 93 | its vendored, documentation and generated rules, and its content heuristics |
| 94 | for shared extensions like `.h` or `.sql`. `.gitattributes` overrides |
| 95 | (`linguist-vendored`, `linguist-language=...`) are honored. Results are |
| 96 | cached per commit and recomputed when the default branch moves. |
| 97 | |
| 98 | ```sh |
| 99 | irongit admin languages huncholane/irongit --files # every file and why it counts or not |
| 100 | irongit admin languages path/to/repo.git # any git directory, no database |
| 101 | python3 scripts/gen-linguist.py v9.8.0 # update to a newer Linguist release |
| 102 | ``` |
| 103 | |
| 104 | After regenerating the data, add a migration with `delete from |
| 105 | repo_languages;` so every repository is recounted. |
| 106 | |
| 107 | ## Tests |
| 108 | |
| 109 | ```sh |
| 110 | cargo test --workspace # unit tests (server, CLI, shared types) |
| 111 | scripts/e2e/ssh-lfs.sh # SSH, LFS end to end against a running server |
| 112 | scripts/e2e/backup.sh # backup to R2 and restore every bundle |
| 113 | ``` |
| 114 | |
| 115 | ## Layout |
| 116 | |
| 117 | ``` |
| 118 | backend/ |
| 119 | migrations/ Postgres schema |
| 120 | src/main.rs startup, subcommands (serve, hook, admin) |
| 121 | src/auth.rs sessions, personal access tokens, extractors |
| 122 | src/perm.rs every access decision (repos, images, orgs) |
| 123 | src/storage.rs R2 over the S3 API (presigned URLs, multipart) |
| 124 | src/git.rs git CLI wrapper used by the web UI |
| 125 | src/git_http.rs smart HTTP (v0, v1, v2) |
| 126 | src/transport.rs spawning upload-pack/receive-pack with hooks and limits |
| 127 | src/hook.rs pre-receive size and quota checks |
| 128 | src/push.rs post-push: events, default branch, heatmap contributions |
| 129 | src/languages/ language stats (Linguist data and rules) |
| 130 | src/ssh.rs built-in SSH server |
| 131 | src/lfs.rs Git LFS batch API |
| 132 | src/registry/ OCI distribution API (docker push/pull) |
| 133 | src/api/ JSON API for the CLI, device login, CLI releases |
| 134 | src/backup.rs scheduled backups to R2 |
| 135 | src/web/ server-rendered pages (maud) poured into the Astro shell |
| 136 | cli/ the ig CLI |
| 137 | shared/ types shared by the API and the CLI |
| 138 | frontend/ Astro: landing, docs, the page shell, CSS and client JS |
| 139 | ``` |
| 140 | |
| 141 | ## Deploying |
| 142 | |
| 143 | Production is `https://git.hygo.ai` on the rybbit server (`ssh rybbit`), as |
| 144 | the `irongit` service in `/opt/hygo/docker-compose.yml`: same shared Postgres |
| 145 | (database `irongit`) and Caddy as the other hygo services, config in |
| 146 | `/opt/hygo/irongit.env`, data in the `irongit-data` volume. |
| 147 | |
| 148 | ```sh |
| 149 | ./deploy.sh # build the image, docker load it on rybbit, roll irongit, wait for healthy, |
| 150 | # publish the bundled ig to R2 when cli/Cargo.toml's version changed |
| 151 | ``` |
| 152 | |
| 153 | Pushing `main` to git.hygo.ai deploys automatically: `.githooks/pre-push` runs |
| 154 | `./deploy.sh` first and stops the push if the deploy fails (enable it per |
| 155 | clone with `git config core.hooksPath .githooks`; skip once with |
| 156 | `git push --no-verify` or `SKIP_DEPLOY=1 git push`). It refuses to deploy |
| 157 | uncommitted or untracked files, since the image is built from the folder. |
| 158 | |
| 159 | Roll back: |
| 160 | |
| 161 | ```sh |
| 162 | ssh rybbit 'cd /opt/hygo && docker tag irongit:$(cat .irongit.prev) irongit:latest && docker compose up -d --no-deps irongit' |
| 163 | ``` |
| 164 | |
| 165 | The DNS record is **DNS-only** (grey cloud): the Cloudflare proxy caps request |
| 166 | bodies at 100 MB, which breaks large `docker push` layers and big HTTPS git |
| 167 | pushes. Caddy gets its own Let's Encrypt certificate. Git over SSH is |
| 168 | published directly on port 2222 (`ssh://git@git.hygo.ai:2222/owner/repo.git`). |
| 169 | Admin commands run inside the container, e.g. |
| 170 | `ssh rybbit docker exec irongit irongit admin promote <user>`. |