# forge 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). > `forge` is a placeholder name. To rename it, change `name` in `Cargo.toml` > and the `forge::` imports in `src/main.rs` and `tests/integration.rs`. ![repo summary](docs/summary.png) ## 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=` 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). ## Quick start ```sh cargo build --release ./target/release/forge forge.toml # or: cargo run --release -- forge.toml # open http://127.0.0.1:8090/ git clone http://127.0.0.1:8090/rust-kernel ``` Options: `forge [CONFIG.toml] [--listen ADDR]`. The config path defaults to `forge.toml`. Running it in the background: ```sh nohup ./target/release/forge forge.toml > /tmp/githost.log 2>&1 & ``` ## Configuration `forge.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 `` 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, forge 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 forge.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. ## 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 forge (or a cut-down build) on Rustbelt itself.