RustForge

rustforge

RustForge: the read-only, cgit-style git web host serving this site

authorTyler Fowler <tyler@localhost>2026-10-31 18:19:33 -0700
commitaee55b07f0cb2997663861e939d9bd7f7f097f38
parent0cdbd22f2e0592b42ddbd472d3b22edf1d4c16db
treebrowse files at this commit
Rename forge to RustForge

- crate and binary: rustforge; display name RustForge in page titles
  ("<page> - <repo> - RustForge"), the footer, error pages and the
  default site_title
- config: forge.toml -> rustforge.toml; server paths /opt/rustforge,
  /etc/rustforge/rustforge.toml, systemd unit rustforge.service, system
  user rustforge (/srv/git unchanged)
- scripts: RUSTFORGE_* env vars, mirrors in /workspace/.rustforge-mirrors,
  the served repo is now 'rustforge' (was rust-githost)
- README and DEPLOY.md updated, with a note on the migration
MCargo.lock+19 -19
MCargo.toml+1 -1
MDEPLOY.md+32 -21
MREADME.md+12 -13
Rdeploy/forge.service → deploy/rustforge.service+9 -9
Rdeploy/forge.toml.example → deploy/rustforge.toml.example+4 -4
Rforge.toml → rustforge.toml+2 -2
Mscripts/deploy.sh+16 -16
Mscripts/sync-mirrors.sh+10 -10
Msrc/config.rs+2 -1
Msrc/lib.rs+1 -1
Msrc/main.rs+8 -8
Msrc/pages.rs+3 -2
Msrc/web.rs+1 -1
Mstatic/style.css+1 -1
Mtests/integration.rs+10 -6

16 files changed, 131 insertions, 115 deletions

@@ -207,25 +207,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"  [[package]]-name = "forge"-version = "0.1.0"-dependencies = [- "axum",- "flate2",- "git2",- "http-body-util",- "maud",- "pulldown-cmark",- "serde",- "syntect",- "tempfile",- "tokio",- "tokio-util",- "toml",- "tower",-]--[[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -637,6 +618,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"  [[package]]+name = "rustforge"+version = "0.1.0"+dependencies = [+ "axum",+ "flate2",+ "git2",+ "http-body-util",+ "maud",+ "pulldown-cmark",+ "serde",+ "syntect",+ "tempfile",+ "tokio",+ "tokio-util",+ "toml",+ "tower",+]++[[package]] name = "rustix" version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1,5 +1,5 @@ [package]-name = "forge"+name = "rustforge" version = "0.1.0" edition = "2021" description = "A small, read-only, cgit-style git web host"
@@ -1,8 +1,8 @@-# Deploying forge to RustForge+# Deploying RustForge -RustForge is a Debian 13 (trixie) Proxmox LXC on Hetzner:+The RustForge container is a Debian 13 (trixie) Proxmox LXC on Hetzner: `ssh -p 2222 rust@88.99.212.146`, private IP **172.16.10.10**.-forge listens on `0.0.0.0:8090` inside the container. TLS and the public ports+RustForge listens on `0.0.0.0:8090` inside the container. TLS and the public ports 80/443 belong to a separate proxy-manager container (e.g. Nginx Proxy Manager).  ## Proxy manager setting@@ -15,25 +15,25 @@ http://172.16.10.10:8090  Scheme `http`, forward host `172.16.10.10`, port `8090`. Websockets and caching are not needed. Leave request buffering off if you can, so large clones-stream. Once the domain exists, set `clone_base_url` in `/etc/forge/forge.toml`-(it's a TODO there) and run `sudo systemctl restart forge`.+stream. Once the domain exists, set `clone_base_url` in `/etc/rustforge/rustforge.toml`+(it's a TODO there) and run `sudo systemctl restart rustforge`.  ## Layout on the server  | Path | What | Owner | |------|------|-------|-| `/opt/forge/bin/forge` | the binary, built on the dev box (CSS is compiled in, no static files needed) | root, 755 |-| `/opt/forge/README.md` | docs | root |-| `/etc/forge/forge.toml` | config (from `deploy/forge.toml.example`) | root:forge, 640 |-| `/etc/systemd/system/forge.service` | unit (from `deploy/forge.service`) | root |-| `/srv/git/<name>.git` | bare mirrors that forge serves | forge:forge |+| `/opt/rustforge/bin/rustforge` | the binary, built on the dev box (CSS is compiled in, no static files needed) | root, 755 |+| `/opt/rustforge/README.md` | docs | root |+| `/etc/rustforge/rustforge.toml` | config (from `deploy/rustforge.toml.example`) | root:rustforge, 640 |+| `/etc/systemd/system/rustforge.service` | unit (from `deploy/rustforge.service`) | root |+| `/srv/git/<name>.git` | bare mirrors that RustForge serves | rustforge:rustforge | -The service runs as the system user `forge` (no login shell, home `/srv/git`).+The service runs as the system user `rustforge` (no login shell, home `/srv/git`). It has `Restart=on-failure` and a read-only view of the whole filesystem (`ProtectSystem=strict`, `NoNewPrivileges`, `PrivateTmp`, no capabilities).-Logs go to `journalctl -u forge`.+Logs go to `journalctl -u rustforge`. -Packages installed on the server: `git` (forge runs `git upload-pack` for+Packages installed on the server: `git` (RustForge runs `git upload-pack` for clones), `ca-certificates`, `rsync` (receiving end of the mirror sync), `curl` (for checks), and `unattended-upgrades` (Debian security updates). @@ -41,17 +41,17 @@ clones), `ca-certificates`, `rsync` (receiving end of the mirror sync), `curl`  ```sh sudo apt-get update && sudo apt-get install -y --no-install-recommends git ca-certificates rsync curl unattended-upgrades-sudo useradd --system --user-group --home-dir /srv/git --no-create-home --shell /usr/sbin/nologin forge-sudo install -d -m 755 -o forge -g forge /srv/git+sudo useradd --system --user-group --home-dir /srv/git --no-create-home --shell /usr/sbin/nologin rustforge+sudo install -d -m 755 -o rustforge -g rustforge /srv/git ``` -## Updating forge+## Updating RustForge  From the dev box (it never builds on the server):  ```sh scripts/deploy.sh            # build --release, copy binary + unit, restart-scripts/deploy.sh --config   # same, and overwrite /etc/forge/forge.toml from the example+scripts/deploy.sh --config   # same, and overwrite /etc/rustforge/rustforge.toml from the example ```  ## Syncing the repos@@ -60,13 +60,13 @@ The working repos in `/workspace` must never get a git remote or be pushed from (other agents work in them). `scripts/sync-mirrors.sh` publishes them anyway: -1. It keeps a private bare mirror per repo in `/workspace/.forge-mirrors/<name>.git`+1. It keeps a private bare mirror per repo in `/workspace/.rustforge-mirrors/<name>.git`    (`git clone --mirror` once, then `git remote update --prune` *inside the mirror*).    Only branches and tags are published. 2. It rsyncs the mirror over ssh to `/srv/git/<name>.git`, objects first and refs    second, so a half-finished sync never points at missing objects. The    server keeps its own `config` and `hooks`.-3. It runs `chown -R forge:forge` on the server.+3. It runs `chown -R rustforge:rustforge` on the server.  ```sh scripts/sync-mirrors.sh              # all repos@@ -75,7 +75,7 @@ scripts/sync-mirrors.sh rust-kernel  # just one  It is idempotent, so run it whenever you want the site updated (or from cron). To add a repo, add `name=/path` to `REPOS` in the script and a `[[repo]]` entry-to `/etc/forge/forge.toml`, then restart forge. `/workspace/rustbelt-os` is+to `/etc/rustforge/rustforge.toml`, then restart rustforge. `/workspace/rustbelt-os` is already in the list and is skipped until it exists.  ## Checks@@ -83,5 +83,16 @@ already in the list and is skipped until it exists. ```sh curl -s -o /dev/null -w '%{http_code}\n' localhost:8090/ git clone http://127.0.0.1:8090/rust-kernel /tmp/x-systemctl status forge+systemctl status rustforge ```++## History: rename from `forge` (Nov 2026)++The project started as `forge`. The rename to RustForge moved the following on the server:+`forge.service` → `rustforge.service`, `/opt/forge` → `/opt/rustforge`,+`/etc/forge/forge.toml` → `/etc/rustforge/rustforge.toml`, system user+`forge` → `rustforge`, and the served repo `rust-githost` → `rustforge`. The+old unit, binary, config, user and `/srv/git/rust-githost.git` were removed.+On the dev box the repo moved from `/workspace/rust-githost` (now a symlink)+to `/workspace/rustforge`, and the mirrors moved from `/workspace/.forge-mirrors`+to `/workspace/.rustforge-mirrors`.
@@ -1,11 +1,10 @@-# forge+# 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). -> `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`.+> The crate and binary are called `rustforge`; the site shows "RustForge".  ![repo summary](docs/summary.png) @@ -27,22 +26,22 @@ Screenshots: [index](docs/index.png), [summary](docs/summary.png), [commit diff]  ```sh cargo build --release-./target/release/forge forge.toml            # or: cargo run --release -- forge.toml+./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: `forge [CONFIG.toml] [--listen ADDR]`. The config path defaults to `forge.toml`.+Options: `rustforge [CONFIG.toml] [--listen ADDR]`. The config path defaults to `rustforge.toml`.  Running it in the background:  ```sh-nohup ./target/release/forge forge.toml > /tmp/githost.log 2>&1 &+nohup ./target/release/rustforge rustforge.toml > /tmp/rustforge.log 2>&1 & ```  ## Configuration -`forge.toml` lists the repos to serve. Nothing else on disk is reachable.+`rustforge.toml` lists the repos to serve. Nothing else on disk is reachable.  ```toml [server]@@ -95,7 +94,7 @@ A request goes through four steps:    - `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, forge runs the real `git upload-pack --stateless-rpc`, which is+   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. @@ -108,7 +107,7 @@ are never served as HTML or SVG. ``` src/main.rs        command line, starts the server src/lib.rs         module list-src/config.rs      forge.toml parsing and validation+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@@ -134,12 +133,12 @@ path traversal attempts. It then starts a real server and runs `git clone`  ## Deployment -forge runs on the RustForge container behind a proxy manager. The binary is+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/forge.service`)-and the production config (`deploy/forge.toml.example`).+[DEPLOY.md](DEPLOY.md), including the systemd unit (`deploy/rustforge.service`)+and the production config (`deploy/rustforge.toml.example`).  ## Roadmap @@ -149,4 +148,4 @@ and the production config (`deploy/forge.toml.example`). - **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.+- **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.
@@ -1,27 +1,27 @@-# systemd unit for forge. Install to /etc/systemd/system/forge.service, then:-#   sudo systemctl daemon-reload && sudo systemctl enable --now forge+# systemd unit for RustForge. Install to /etc/systemd/system/rustforge.service, then:+#   sudo systemctl daemon-reload && sudo systemctl enable --now rustforge [Unit]-Description=forge - read-only cgit-style git web host-Documentation=file:///opt/forge/README.md+Description=RustForge - read-only cgit-style git web host+Documentation=file:///opt/rustforge/README.md After=network-online.target Wants=network-online.target  [Service] Type=simple-User=forge-Group=forge-ExecStart=/opt/forge/bin/forge /etc/forge/forge.toml+User=rustforge+Group=rustforge+ExecStart=/opt/rustforge/bin/rustforge /etc/rustforge/rustforge.toml WorkingDirectory=/srv/git Environment=HOME=/srv/git Restart=on-failure RestartSec=2s  # --- hardening --------------------------------------------------------------# forge only *reads* repos (git upload-pack never writes), so the whole+# RustForge only *reads* repos (git upload-pack never writes), so the whole # filesystem is read-only to it; /tmp is a private, empty tmpfs. NoNewPrivileges=true ProtectSystem=strict-ReadOnlyPaths=/srv/git /etc/forge /opt/forge+ReadOnlyPaths=/srv/git /etc/rustforge /opt/rustforge ProtectHome=true PrivateTmp=true PrivateDevices=true
@@ -1,4 +1,4 @@-# Production config for the RustForge container (copied to /etc/forge/forge.toml).+# Production config for the RustForge container (copied to /etc/rustforge/rustforge.toml). # Repos are bare mirrors kept up to date by scripts/sync-mirrors.sh.  [server]@@ -18,6 +18,6 @@ path = "/srv/git/rust-kernel.git" description = "Rustbelt: a hobby x86_64 kernel written in Rust"  [[repo]]-name = "rust-githost"-path = "/srv/git/rust-githost.git"-description = "forge: the read-only, cgit-style git web host serving this site"+name = "rustforge"+path = "/srv/git/rustforge.git"+description = "RustForge: the read-only, cgit-style git web host serving this site"
@@ -1,10 +1,10 @@-# forge configuration.+# RustForge configuration. # Only the repositories listed here are served. Paths may point at a bare repo # (foo.git) or a normal checkout (foo/ containing .git/).  [server] listen = "127.0.0.1:8090"-site_title = "Tyler's git"+site_title = "RustForge" # Base URL shown in "git clone" hints. Leave unset to use the request's Host. # clone_base_url = "https://git.example.com" 
@@ -1,29 +1,29 @@ #!/usr/bin/env bash-# Build forge here and install it on the server (never builds on the server).+# Build RustForge here and install it on the server (never builds on the server). #   scripts/deploy.sh                # binary + unit; keeps the server's config-#   scripts/deploy.sh --config       # also overwrite /etc/forge/forge.toml+#   scripts/deploy.sh --config       # also overwrite /etc/rustforge/rustforge.toml set -euo pipefail cd "$(dirname "$0")/.." -SSH_HOST="${FORGE_SSH_HOST:-rust@88.99.212.146}"-SSH_PORT="${FORGE_SSH_PORT:-2222}"+SSH_HOST="${RUSTFORGE_SSH_HOST:-rust@88.99.212.146}"+SSH_PORT="${RUSTFORGE_SSH_PORT:-2222}" SSH=(ssh -o BatchMode=yes -p "$SSH_PORT" "$SSH_HOST") SCP=(scp -q -o BatchMode=yes -P "$SSH_PORT")  cargo build --release-"${SCP[@]}" target/release/forge deploy/forge.service deploy/forge.toml.example README.md "$SSH_HOST:/tmp/"+"${SCP[@]}" target/release/rustforge deploy/rustforge.service deploy/rustforge.toml.example README.md "$SSH_HOST:/tmp/" "${SSH[@]}" "set -e-  sudo install -d -m 755 /opt/forge/bin-  sudo install -m 755 /tmp/forge /opt/forge/bin/forge-  sudo install -m 644 /tmp/README.md /opt/forge/README.md-  sudo install -m 644 /tmp/forge.service /etc/systemd/system/forge.service-  sudo install -d -m 755 /etc/forge-  if [ '${1:-}' = '--config' ] || [ ! -f /etc/forge/forge.toml ]; then-    sudo install -m 640 -o root -g forge /tmp/forge.toml.example /etc/forge/forge.toml+  sudo install -d -m 755 /opt/rustforge/bin+  sudo install -m 755 /tmp/rustforge /opt/rustforge/bin/rustforge+  sudo install -m 644 /tmp/README.md /opt/rustforge/README.md+  sudo install -m 644 /tmp/rustforge.service /etc/systemd/system/rustforge.service+  sudo install -d -m 755 /etc/rustforge+  if [ '${1:-}' = '--config' ] || [ ! -f /etc/rustforge/rustforge.toml ]; then+    sudo install -m 640 -o root -g rustforge /tmp/rustforge.toml.example /etc/rustforge/rustforge.toml   fi-  rm -f /tmp/forge /tmp/forge.service /tmp/forge.toml.example /tmp/README.md+  rm -f /tmp/rustforge /tmp/rustforge.service /tmp/rustforge.toml.example /tmp/README.md   sudo systemctl daemon-reload-  sudo systemctl enable --quiet forge-  sudo systemctl restart forge+  sudo systemctl enable --quiet rustforge+  sudo systemctl restart rustforge   sleep 1-  systemctl is-active forge"+  systemctl is-active rustforge"
@@ -1,5 +1,5 @@ #!/usr/bin/env bash-# Mirror local working repos to the forge server, WITHOUT touching them.+# Mirror local working repos to the RustForge server, WITHOUT touching them. # # The working repos under /workspace have a strict "no remotes, no pushes" # rule (other agents work in them). So instead of pushing, this script:@@ -8,22 +8,22 @@ #      (git clone --mirror the first time, git remote update afterwards; #      the only remote that ever exists is inside the mirror itself), #   2. rsyncs that bare mirror over ssh to /srv/git/<name>.git on the server,-#   3. fixes ownership there to the `forge` user.+#   3. fixes ownership there to the `rustforge` user. # # Safe to run as often as you like (idempotent). Usage: #   scripts/sync-mirrors.sh            # sync every repo in the list below #   scripts/sync-mirrors.sh rust-kernel  # sync just one set -euo pipefail -SSH_HOST="${FORGE_SSH_HOST:-rust@88.99.212.146}"-SSH_PORT="${FORGE_SSH_PORT:-2222}"-MIRROR_DIR="${FORGE_MIRROR_DIR:-/workspace/.forge-mirrors}"-REMOTE_ROOT="${FORGE_REMOTE_ROOT:-/srv/git}"+SSH_HOST="${RUSTFORGE_SSH_HOST:-rust@88.99.212.146}"+SSH_PORT="${RUSTFORGE_SSH_PORT:-2222}"+MIRROR_DIR="${RUSTFORGE_MIRROR_DIR:-/workspace/.rustforge-mirrors}"+REMOTE_ROOT="${RUSTFORGE_REMOTE_ROOT:-/srv/git}"  # name=source-path. Missing sources are skipped with a note. REPOS=(   "rust-kernel=/workspace/rust-kernel"-  "rust-githost=/workspace/rust-githost"+  "rustforge=/workspace/rustforge"   "rustbelt-os=/workspace/rustbelt-os" ) @@ -60,19 +60,19 @@ sync_one() {   fi    # 2. Make sure the server-side bare repo exists (its own config and hooks).-  "${SSH[@]}" "$SSH_HOST" "sudo test -d '$remote' || sudo -u forge git init --quiet --bare '$remote'"+  "${SSH[@]}" "$SSH_HOST" "sudo test -d '$remote' || sudo -u rustforge git init --quiet --bare '$remote'"    # 3. Copy objects first, then refs/HEAD, so the server never sees a ref   #    pointing at an object that hasn't arrived yet. The server keeps its own   #    config/hooks/description (the mirror's config points at /workspace).-  local rsync=(rsync -a --chown=forge:forge --rsync-path="sudo rsync" -e "${SSH[*]}")+  local rsync=(rsync -a --chown=rustforge:rustforge --rsync-path="sudo rsync" -e "${SSH[*]}")   "${rsync[@]}" "$mirror/objects/" "$SSH_HOST:$remote/objects/"   "${rsync[@]}" --delete \     --exclude=/config --exclude=/hooks/ --exclude=/description --exclude=/FETCH_HEAD \     "$mirror/" "$SSH_HOST:$remote/"    # 4. Ownership (rsync --chown already does this; this is belt and braces).-  "${SSH[@]}" "$SSH_HOST" "sudo chown -R forge:forge '$remote'"+  "${SSH[@]}" "$SSH_HOST" "sudo chown -R rustforge:rustforge '$remote'"    log "$name: $(git -C "$mirror" rev-parse --abbrev-ref HEAD) @ $(git -C "$mirror" rev-parse --short HEAD) -> $SSH_HOST:$remote" }
@@ -45,7 +45,7 @@ fn default_listen() -> String { }  fn default_title() -> String {-    "git".to_string()+    "RustForge".to_string() }  #[derive(Debug, Clone, Deserialize)]@@ -161,6 +161,7 @@ mod tests {     fn defaults_apply() {         let cfg = Config::from_toml("").unwrap();         assert_eq!(cfg.server.listen, "127.0.0.1:8090");+        assert_eq!(cfg.server.site_title, "RustForge");         assert!(cfg.repos.is_empty());     } 
@@ -1,4 +1,4 @@-//! forge: a small, read-only, cgit-style git web host.+//! RustForge: a small, read-only, cgit-style git web host. //! //! Module map (read them in this order if you're new): //! - [`config`]     – the TOML file that lists which repos to serve
@@ -1,27 +1,27 @@ //! Entry point: read the config, start the web server. //!-//! Usage: forge [CONFIG.toml] [--listen ADDR]+//! Usage: rustforge [CONFIG.toml] [--listen ADDR] -use forge::config::Config;-use forge::web::{self, AppState};+use rustforge::config::Config;+use rustforge::web::{self, AppState}; use std::path::PathBuf; -const USAGE: &str = "usage: forge [CONFIG.toml] [--listen ADDR]+const USAGE: &str = "usage: rustforge [CONFIG.toml] [--listen ADDR] -Serves the git repositories listed in CONFIG.toml (default: forge.toml)+Serves the git repositories listed in CONFIG.toml (default: rustforge.toml) as a read-only website, and allows `git clone` over HTTP.";  #[tokio::main] async fn main() {     if let Err(e) = run().await {-        eprintln!("forge: {e}");+        eprintln!("rustforge: {e}");         std::process::exit(1);     } }  async fn run() -> Result<(), String> {     // Tiny hand-rolled argument parsing; enough for two options.-    let mut config_path = PathBuf::from("forge.toml");+    let mut config_path = PathBuf::from("rustforge.toml");     let mut listen_override = None;     let mut args = std::env::args().skip(1);     while let Some(arg) = args.next() {@@ -48,7 +48,7 @@ async fn run() -> Result<(), String> {         .await         .map_err(|e| format!("cannot listen on {listen}: {e}"))?;     eprintln!(-        "forge v{} serving {} repo(s) on http://{listen}/",+        "RustForge v{} serving {} repo(s) on http://{listen}/",         env!("CARGO_PKG_VERSION"),         state.repos.len()     );
@@ -49,7 +49,8 @@ fn layout(title: &str, site_title: &str, body: Markup) -> Markup {                 meta charset="utf-8";                 meta name="viewport" content="width=device-width, initial-scale=1";                 meta name="color-scheme" content="light dark";-                title { (title) }+                // e.g. "log - rust-kernel - RustForge"+                title { (title) @if title != site_title { " - " (site_title) } }                 link rel="stylesheet" href="/static/style.css";                 link rel="stylesheet" href="/static/highlight.css";             }@@ -58,7 +59,7 @@ fn layout(title: &str, site_title: &str, body: Markup) -> Markup {                     a.site-title href="/" { (site_title) }                 }                 main { (body) }-                footer { "generated by forge v" (env!("CARGO_PKG_VERSION")) }+                footer { "generated by RustForge v" (env!("CARGO_PKG_VERSION")) }             }         }     }
@@ -148,7 +148,7 @@ impl From<git::Error> for AppError {  impl IntoResponse for AppError {     fn into_response(self) -> Response {-        let page = pages::error("forge", self.status.as_u16(), &self.message);+        let page = pages::error("RustForge", self.status.as_u16(), &self.message);         (self.status, Html(page.into_string())).into_response()     } }
@@ -1,4 +1,4 @@-/* forge: plain, cgit-like styling. Light by default, dark when the OS asks. */+/* RustForge: plain, cgit-like styling. Light by default, dark when the OS asks. */ :root {   --bg: #ffffff; --fg: #1f2328; --muted: #6a737d; --link: #0645ad;   --border: #d0d7de; --head: #f6f8fa; --hover: #f3f6fa;
@@ -3,9 +3,9 @@  use axum::body::Body; use axum::http::{Request, StatusCode};-use forge::config::Config;-use forge::web::{router, AppState}; use http_body_util::BodyExt;+use rustforge::config::Config;+use rustforge::web::{router, AppState}; use std::path::Path; use std::process::Command; use tower::ServiceExt; // for `oneshot`@@ -47,7 +47,7 @@ fn fixture() -> Fixture {      std::fs::write(         work.join("README.md"),-        "# Hello Forge\n\nSome **bold** text.\n\n<script>alert('readme')</script>\n",+        "# Hello Readme\n\nSome **bold** text.\n\n<script>alert('readme')</script>\n",     )     .unwrap();     std::fs::create_dir(work.join("src")).unwrap();@@ -72,11 +72,11 @@ fn fixture() -> Fixture {     Fixture { dir, head, change_commit } } -fn state_for(f: &Fixture) -> forge::web::Shared {+fn state_for(f: &Fixture) -> rustforge::web::Shared {     let toml = format!(         r#"         [server]-        site_title = "test forge"+        site_title = "test site"         clone_base_url = "http://example.test"          [[repo]]@@ -116,6 +116,10 @@ async fn index_lists_repos_and_escapes_descriptions() {     assert!(body.contains("non-bare &lt;b&gt;repo&lt;/b&gt;"));     assert!(!body.contains("<b>repo</b>"));     assert!(body.contains("ago"), "idle column should show an age");+    assert!(body.contains("<title>test site</title>"));+    assert!(body.contains("generated by RustForge v"));+    let (_, body) = get(&f, "/work/log").await;+    assert!(body.contains("<title>log - work - test site</title>"), "{body}"); }  #[tokio::test]@@ -127,7 +131,7 @@ async fn summary_shows_refs_commits_and_safe_readme() {         assert!(body.contains("feature/x"), "branch listed");         assert!(body.contains("v1.0"), "tag listed");         assert!(body.contains("change &lt;greeting&gt;"), "commit subject escaped");-        assert!(body.contains("<h1>Hello Forge</h1>"), "README rendered");+        assert!(body.contains("<h1>Hello Readme</h1>"), "README rendered");         assert!(body.contains("<strong>bold</strong>"));         assert!(!body.contains("<script>"), "raw HTML in README must not pass through");         assert!(body.contains(&format!("git clone http://example.test/{repo}")));