rustforge
RustForge: the read-only, cgit-style git web host serving this site
6799 bytes · raw
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | # RustForge
A small, **read-only**, cgit-style git web host written in Rust.
Version 0.1, built to be the self-hosted home for Tyler's hobby projects
(starting with the Rustbelt kernel).
> The crate and binary are called `rustforge`; the site shows "RustForge".

## What it does
- **Repo index**: every configured repo with its description and how long since the last commit.
- **Summary**: branches, tags, the 10 latest commits, a `git clone` hint, and the README rendered from Markdown.
- **Log**: 50 commits per page with newer/older links. `?h=<branch|tag|commit>` picks where to start.
- **Commit**: author, message, parents, a diffstat and a unified diff with green/red added/removed lines.
- **Tree and blob**: browse directories. Files get syntax highlighting and clickable line numbers (`#n42`).
- **Raw**: download any file as-is.
- **Refs**: all branches and tags.
- **`git clone` over HTTP** (smart protocol, v0 and v2). Pushing is refused.
- Light and dark themes (follows your OS setting). No JavaScript anywhere.
Screenshots: [index](docs/index.png), [summary](docs/summary.png), [commit diff](docs/commit.png), [commit diff, dark theme](docs/commit-dark.png), [blob view](docs/blob.png).
## Quick start
```sh
cargo build --release
./target/release/rustforge rustforge.toml # or: cargo run --release -- rustforge.toml
# open http://127.0.0.1:8090/
git clone http://127.0.0.1:8090/rust-kernel
```
Options: `rustforge [CONFIG.toml] [--listen ADDR]`. The config path defaults to `rustforge.toml`.
Running it in the background:
```sh
nohup ./target/release/rustforge rustforge.toml > /tmp/rustforge.log 2>&1 &
```
## Configuration
`rustforge.toml` lists the repos to serve. Nothing else on disk is reachable.
```toml
[server]
listen = "127.0.0.1:8090"
site_title = "Tyler's git"
# clone_base_url = "https://git.example.com" # shown in clone hints
[[repo]]
name = "rust-kernel" # URL: /rust-kernel/
path = "/workspace/rust-kernel" # a normal checkout or a bare foo.git
description = "Rustbelt: a hobby x86_64 kernel written in Rust"
```
`git` must be installed and on `PATH`, because cloning runs `git upload-pack`.
## URLs
| URL | Page |
|-----|------|
| `/` | repo index |
| `/{repo}/` | summary |
| `/{repo}/refs` | branches and tags |
| `/{repo}/log?h=main&ofs=50` | log, page 2 |
| `/{repo}/commit/{id}` | commit and diff (full or short id) |
| `/{repo}/tree/{path}?h=v1.0` | directory |
| `/{repo}/blob/{path}` | highlighted file |
| `/{repo}/raw/{path}` | raw file |
| `/{repo}/info/refs`, `/{repo}/git-upload-pack` | used by `git clone` |
## How it works
A request goes through four steps:
1. **Routing** (`src/web.rs`). [axum](https://docs.rs/axum) matches the URL to a
handler function, for example `/{repo}/log` goes to `log()`. Each handler
first looks the repo name up in the config. Unknown names get a 404, which
is why nothing outside the config can be served.
2. **Reading git** (`src/git.rs`). The handler opens the repo with
[git2](https://docs.rs/git2) (bindings to libgit2) and collects plain
structs such as `CommitInfo` and `FileDiff`. libgit2 calls block the
thread, so they run in `tokio::task::spawn_blocking` and don't hold up other
requests. File paths from the URL are never used on the real filesystem.
They are looked up *inside git's tree objects*, after `..` and odd
characters have been rejected (`util::clean_repo_path`).
3. **Rendering HTML** (`src/pages.rs`). Pages are [maud](https://maud.lambda.xyz)
templates, which are Rust code checked at compile time. Every `(value)` is
HTML-escaped automatically. The only pre-built HTML comes from:
- `src/markdown.rs`: READMEs rendered with pulldown-cmark. Raw HTML in the
Markdown becomes text, and `javascript:` links are removed.
- `src/highlight.rs`: syntect, which emits `<span class="hl-…">` with the
text escaped. Colors come from generated CSS for a light and a dark theme.
4. **Cloning** (`src/smart_http.rs`). Rather than reimplementing git's wire
protocol, RustForge runs the real `git upload-pack --stateless-rpc`, which is
what git's own `git-http-backend` does, and pipes the HTTP request and
response through it. upload-pack only reads, so a clone can't change the repo.
Every response also carries a strict `Content-Security-Policy` (no scripts at
all) and `X-Content-Type-Options: nosniff` as a second safety net. Raw files
are never served as HTML or SVG.
## Project layout
```
src/main.rs command line, starts the server
src/lib.rs module list
src/config.rs rustforge.toml parsing and validation
src/web.rs routes + handlers
src/git.rs reading repos with libgit2
src/pages.rs HTML templates
src/markdown.rs README rendering
src/highlight.rs syntax highlighting
src/smart_http.rs git clone support
src/util.rs dates, ages, path/ref checks, URL encoding
static/style.css the stylesheet
tests/integration.rs builds temp repos, checks every page, does real clones
```
## Tests
```sh
cargo test
```
The unit tests cover config parsing, path and ref checks, date formatting,
Markdown sanitizing, and highlighting. The integration test builds a normal and
a bare repo in a temp dir and checks every page type, pagination, escaping and
path traversal attempts. It then starts a real server and runs `git clone`
(protocol v0 and v2) plus a `git push` that must be refused.
## Deployment
RustForge runs in the RustForge container behind a proxy manager. The binary is
built here and copied over (`scripts/deploy.sh`), and repos are published as bare
mirrors (`scripts/sync-mirrors.sh`), so the working repos never need a remote.
The proxy target is `http://172.16.10.10:8090`. Everything is in
[DEPLOY.md](DEPLOY.md), including the systemd unit (`deploy/rustforge.service`)
and the production config (`deploy/rustforge.toml.example`).
## Roadmap
- **Push over SSH**: `git-receive-pack` behind an SSH server with per-user keys (e.g. the `russh` crate, or `authorized_keys` with a forced command).
- **Users and auth**: accounts, private repos, per-repo permissions. Smart-HTTP push could then be added behind auth.
- **Nicer browsing**: blame, a branch/tag selector, commit search, diffs between any two refs, tarball snapshots, Atom feed of commits.
- **Speed**: cache rendered pages and the "idle" times, keyed by ref tips.
- **Public deploy**: a reverse proxy (Caddy or nginx) with TLS in front of `127.0.0.1:8090`, `clone_base_url` set, and a systemd unit.
- **Issues, later**: a minimal issue tracker, or a mailing-list style flow.
- **Self-hosting on Rustbelt**: once the kernel has a TCP stack, sockets and `fork/exec`, run RustForge (or a cut-down build) on Rustbelt itself.
|