c67e7434
- commit
- c67e74348fcd191fd051c82ee26e2bae710a9a7b
- parent
- author
- Christopher K. Schmitt <me@shmish.dev>
- date
- 2026-07-29
- committer
- Christopher K. Schmitt <me@shmish.dev>
- date
- 2026-07-29
- changes
- 23
- insertions
- 191
- deletions
- 149
Msrc/config.rs
-//! Global application configuration set once at startup.+//! Application configuration set once at startup.~//!-//! Values are stored in `OnceLock` statics and accessed via the corresponding-//! getter functions. All setters accept `Option` and silently skip `None`.+//! Compile-time constants ([`PKG_NAME`], [`PKG_VERSION`]) are read from+//! `Cargo.toml` via `env!()`. Runtime configuration is stored in+//! [`SiteConfig`] and threaded through [`AppState`](crate::routes::AppState)+//! rather than held in process-global statics.~-use std::sync::OnceLock;-~/// Package name read from `Cargo.toml` at compile time.~pub const PKG_NAME: &str = env!("CARGO_PKG_NAME");~~/// Package version read from `Cargo.toml` at compile time.~pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");--static HOSTNAME: OnceLock<String> = OnceLock::new();-static SSH_PREFIX: OnceLock<String> = OnceLock::new();-static BASE_URL: OnceLock<String> = OnceLock::new();-static FAVICON: OnceLock<Vec<u8>> = OnceLock::new();-static FAVICON_MIME: OnceLock<&'static str> = OnceLock::new();--/// Returns the configured hostname, or `"index"` if none was provided.-pub fn hostname() -> &'static str {- HOSTNAME.get().map_or("index", |s| s.as_str())-}--/// Returns the configured SSH prefix, or an empty string if none was provided.-pub fn ssh_prefix() -> &'static str {- SSH_PREFIX.get().map_or("", |s| s.as_str())-}--/// Returns the configured base URL path prefix, or an empty string if none was-/// provided.-pub fn base_url() -> &'static str {- BASE_URL.get().map_or("", |s| s.as_str())-}--/// Returns the favicon bytes loaded at startup.-pub fn favicon() -> &'static [u8] {- FAVICON.get().map_or(&[], |v| v.as_slice())-}--/// Returns the MIME type of the favicon.-pub fn favicon_mime_type() -> &'static str {- FAVICON_MIME.get().copied().unwrap_or("image/svg+xml")-}--/// Sets the hostname shown in breadcrumbs and clone URLs.-/// Silently skips `None`.-pub fn set_hostname(v: Option<String>) {- if let Some(v) = v {- let _ = HOSTNAME.set(v);- }-}--/// Sets the path prefix used in SSH clone URLs. Silently skips `None`.-pub fn set_ssh_prefix(v: Option<String>) {- if let Some(v) = v {- let _ = SSH_PREFIX.set(v);- }-}--/// Sets the URL path prefix for reverse-proxy deployments.-/// Silently skips `None`.-pub fn set_base_url(v: Option<String>) {- if let Some(v) = v {- let _ = BASE_URL.set(v);- }-}~-/// Sets the favicon bytes and MIME type served at `/favicon.ico`.-pub fn set_favicon(v: Vec<u8>, mime: &'static str) {- let _ = FAVICON.set(v);- let _ = FAVICON_MIME.set(mime);+/// Site-wide configuration set once at startup and shared across all handlers.+#[derive(Clone)]+pub struct SiteConfig {+ pub site_name: String,+ pub hostname: String,+ pub ssh_prefix: String,+ pub base_url: String,+ pub favicon: Vec<u8>,+ pub favicon_mime: &'static str,~}
Msrc/error.rs
~//! [`Result`] type alias. `Error` implements [`IntoResponse`] via askama's~//! `error.html` template.~+use std::sync::{Arc, OnceLock};+~use askama::Template;~use axum::http::{StatusCode, header};~use axum::response::{IntoResponse, Response};~use thiserror::Error;++use crate::config::SiteConfig;~~/// Unified error type for the application.~///
~~/// Convenience alias for `std::result::Result<T, Error>`.~pub type Result<T> = std::result::Result<T, Error>;++/// Module-level [`SiteConfig`] used only by the error template renderer.+///+/// Set once during startup in [`init_site`]. This exists because+/// [`Error::into_response`] does not have access to [`axum::extract::State`]+/// — it runs after the handler has returned. All other code paths read from+/// [`AppState`](crate::routes::AppState).+static SITE: OnceLock<Arc<SiteConfig>> = OnceLock::new();++/// Stores the [`SiteConfig`] for use by the error template.+///+/// Called once from `main` before the server starts.+pub fn init_site(site: Arc<SiteConfig>) {+ let _ = SITE.set(site);+}~+fn site() -> Arc<SiteConfig> {+ SITE.get()+ .cloned()+ .unwrap_or_else(|| {+ Arc::new(SiteConfig {+ site_name: crate::PKG_NAME.to_owned(),+ hostname: String::new(),+ ssh_prefix: String::new(),+ base_url: String::new(),+ favicon: Vec::new(),+ favicon_mime: "image/svg+xml",+ })+ })+}+~#[derive(Template)]~#[template(path = "error.html")]~struct ErrorTemplate {~ status: u16,~ kind: &'static str,~ message: String,- base_url: String,+ site: Arc<SiteConfig>,~}~~impl IntoResponse for Error {
~ status: status.as_u16(),~ kind,~ message,- base_url: crate::base_url().to_string(),+ site: site(),~ };~~ tpl.render().map_or_else(
~ self.to_string()~ };~ (status, fallback).into_response()+ },+ |html| {+ (+ status,+ [(header::CONTENT_TYPE, "text/html; charset=utf-8")],+ html,+ )+ .into_response()~ },- |html| (status, [(header::CONTENT_TYPE, "text/html; charset=utf-8")], html).into_response(),~ )~ }~}
Msrc/lib.rs
~pub mod theme;~~// Re-exports for askama Template derives (resolved at `crate::`).-pub use config::{PKG_NAME, PKG_VERSION, base_url, hostname, ssh_prefix};+pub use config::{PKG_NAME, PKG_VERSION};
Msrc/main.rs
-//! Binary entry point. Parses CLI arguments, initialises global configuration,-//! builds the CSS theme, constructs the axum router, and starts the HTTP server.+//! Binary entry point. Parses CLI arguments, constructs the application+//! configuration, builds the CSS theme, and starts the HTTP server.~~mod cli;~+use std::sync::Arc;+~use clap::Parser;-use teak::config;+use teak::config::{self, SiteConfig};~use teak::theme::{BASE_CSS, MARKDOWN_CSS};~use teak::{error, routes};~use tower_http::trace::TraceLayer;
~~ let args = cli::Args::parse();~- config::set_hostname(args.hostname);- config::set_ssh_prefix(args.ssh_prefix);- config::set_base_url(args.base_url);-~ let (favicon_data, favicon_mime) = match args.favicon {~ Some(path) => match std::fs::read(&path) {~ Ok(data) => {
~ },~ None => (include_bytes!("../static/favicon.svg").to_vec(), "image/svg+xml"),~ };- config::set_favicon(favicon_data, favicon_mime);++ let site = Arc::new(SiteConfig {+ site_name: args+ .hostname+ .clone()+ .unwrap_or_else(|| config::PKG_NAME.to_owned()),+ hostname: args.hostname.unwrap_or_default(),+ ssh_prefix: args.ssh_prefix.unwrap_or_default(),+ base_url: args.base_url.unwrap_or_default(),+ favicon: favicon_data,+ favicon_mime,+ });++ error::init_site(site.clone());~~ let theme_vars = match args.theme_file {~ Some(path) => std::fs::read_to_string(path)?,
~ let state = routes::AppState {~ root: args.root,~ css,+ site,~ };~ let router = routes::router(state).layer(TraceLayer::new_for_http());~ let addr = format!("{}:{}", args.bind, args.port);
Msrc/routes.rs
~~use std::collections::HashMap;~use std::path::{Component, PathBuf};+use std::sync::Arc;~+use crate::config::SiteConfig;~use crate::error::{Error, Result};~use axum::Router;~use axum::extract::{FromRequestParts, Path, State};
~ pub root: PathBuf,~ /// Concatenated CSS served at `/style.css`.~ pub css: String,+ /// Site-wide configuration set once at startup.+ pub site: Arc<SiteConfig>,~}~~/// Serves the concatenated stylesheet.
~}~~/// Serves the favicon bytes loaded at startup, with the correct MIME type.-async fn favicon() -> impl IntoResponse {+async fn favicon(State(state): State<AppState>) -> impl IntoResponse {~ (- [(header::CONTENT_TYPE, crate::config::favicon_mime_type())],- crate::config::favicon(),+ [(header::CONTENT_TYPE, state.site.favicon_mime)],+ state.site.favicon.clone(),~ )~}~
Mtemplates/base.html
~ <meta charset="UTF-8">~ <meta name="viewport" content="width=device-width, initial-scale=1.0">~ <title>{% block title %}{% endblock %}</title>- <link rel="stylesheet" href="{{ crate::base_url() }}/style.css">- <link rel="icon" type="image/svg+xml" href="{{ crate::base_url() }}/favicon.ico">+ <link rel="stylesheet" href="{{ site.base_url }}/style.css">+ <link rel="icon" type="image/svg+xml" href="{{ site.base_url }}/favicon.ico">~ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" crossorigin="anonymous">- <meta property="og:site_name" content="git browse">+ <meta property="og:site_name" content="{{ site.site_name }}">~ <meta property="og:type" content="website">~ <meta property="og:title" content="{% block og_title %}{% endblock %}">~ <meta property="og:description" content="{% block og_description %}{% endblock %}">
Mtemplates/blame.html
~~{% block breadcrumbs %}~<ul>- <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ sha }}">{{ sha[..8] }}</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}">{{ sha[..8] }}</a></li>~ {% for crumb in breadcrumbs %}~ <li>{% if loop.last %}{{ crumb.label }}{% else %}<a href="{{ crumb.url }}">{{ crumb.label }}</a>{% endif %}</li>~ {% endfor %}
~ <table class="blame-table">~ {% for line in lines %}~ <tr class="blame-row">- <td class="blame-sha"><a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ line.sha }}" style="color: {{ line.color }}">{{ line.short_sha }}</a></td>+ <td class="blame-sha"><a href="{{ site.base_url }}/{{ repo }}/commits/{{ line.sha }}" style="color: {{ line.color }}">{{ line.short_sha }}</a></td>~ <td class="blame-linenum">{{ line.num }}</td>~ <td class="blame-code">{{ line.content }}</td>~ </tr>
Mtemplates/branch_detail.html
~~{% block breadcrumbs %}~<ul>- <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/branches">branches</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/branches">branches</a></li>~ <li>{{ branch }}</li>~</ul>~{% endblock %}
~<details>~ <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>~ <ul>- <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.gz/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.tar.gz"><span class="archive-link">{{ repo }}-{{ branch }}.tar.gz</span><span>tar.gz</span></a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.xz/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.tar.xz"><span class="archive-link">{{ repo }}-{{ branch }}.tar.xz</span><span>tar.xz</span></a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/zip/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.zip"><span class="archive-link">{{ repo }}-{{ branch }}.zip</span><span>zip</span></a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.gz/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.tar.gz"><span class="archive-link">{{ repo }}-{{ branch }}.tar.gz</span><span>tar.gz</span></a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.xz/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.tar.xz"><span class="archive-link">{{ repo }}-{{ branch }}.tar.xz</span><span>tar.xz</span></a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/archive/zip/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.zip"><span class="archive-link">{{ repo }}-{{ branch }}.zip</span><span>zip</span></a></li>~ </ul>~</details>~~<div class="repo-nav">- <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ tip_sha }}/tree" class="repo-nav-item">+ <a href="{{ site.base_url }}/{{ repo }}/commits/{{ tip_sha }}/tree" class="repo-nav-item">~ <svg class="summary-icon"><use href="#icon-tree"/></svg>~ <span>Tree</span>~ <span class="repo-nav-item-meta">[{{ tip_sha[..8] }}]</span>
~{% else %}~<div class="commit-log">~ {% for commit in commits %}- <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ commit.sha }}" class="commit-row">+ <a href="{{ site.base_url }}/{{ repo }}/commits/{{ commit.sha }}" class="commit-row">~ <span class="commit-sha">{{ commit.sha[..8] }}</span>~ <span class="commit-message">{{ commit.message }}</span>~ <span class="commit-meta"><span>{{ commit.author }}</span><span>{{ commit.date }}</span></span>
Mtemplates/branch_list.html
~~{% block breadcrumbs %}~<ul>- <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>~ <li>branches</li>~</ul>~{% endblock %}
~{% else %}~<div class="branch-list">~ {% for branch in branches %}- <a href="{{ crate::base_url() }}/{{ repo }}/branches/{{ branch.name }}" class="branch-row">+ <a href="{{ site.base_url }}/{{ repo }}/branches/{{ branch.name }}" class="branch-row">~ <span class="branch-name">{{ branch.name }}</span>~ <span class="branch-date">{% if branch.is_default %}<span class="branch-default">default</span>{% endif %}{{ branch.date }}</span>~ </a>
Mtemplates/commit.html
~~{% block breadcrumbs %}~<ul>- <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/commits">commits</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/commits">commits</a></li>~ <li>{{ sha[..8] }}</li>~</ul>~{% endblock %}
~<details>~ <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>~ <ul>- <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.gz/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.tar.gz"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.tar.gz</span><span>tar.gz</span></a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.xz/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.tar.xz"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.tar.xz</span><span>tar.xz</span></a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/zip/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.zip"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.zip</span><span>zip</span></a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.gz/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.tar.gz"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.tar.gz</span><span>tar.gz</span></a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.xz/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.tar.xz"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.tar.xz</span><span>tar.xz</span></a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/archive/zip/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.zip"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.zip</span><span>zip</span></a></li>~ </ul>~</details>~~<div class="repo-nav">- <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ sha }}/tree" class="repo-nav-item">+ <a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}/tree" class="repo-nav-item">~ <svg class="summary-icon"><use href="#icon-tree"/></svg>~ <span>Tree</span>~ <span class="repo-nav-item-meta">[{{ sha[..8] }}]</span>
~{% if !refs.is_empty() %}~<dl class="tag-info">~ <dt>refs</dt>- <dd>{% for r in refs %}<div class="commit-ref-row"><a href="{{ crate::base_url() }}/{{ repo }}/{% if r.kind == "branch" %}branches{% else %}tags{% endif %}/{{ r.name }}" class="commit-ref commit-ref-{{ r.kind }}">{{ r.name }}</a></div>{% endfor %}</dd>+ <dd>{% for r in refs %}<div class="commit-ref-row"><a href="{{ site.base_url }}/{{ repo }}/{% if r.kind == "branch" %}branches{% else %}tags{% endif %}/{{ r.name }}" class="commit-ref commit-ref-{{ r.kind }}">{{ r.name }}</a></div>{% endfor %}</dd>~</dl>~{% endif %}~~<dl class="tag-info">- <dt>commit</dt><dd><a class="commit-sha" href="{{ crate::base_url() }}/{{ repo }}/commits/{{ sha }}">{{ sha }}</a></dd>+ <dt>commit</dt><dd><a class="commit-sha" href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}">{{ sha }}</a></dd>~ <dt>parent{% if parents.len() != 1 %}s{% endif %}</dt>- <dd>{% for (parent_sha, _) in parents %}<div class="commit-parent"><a class="commit-sha" href="{{ crate::base_url() }}/{{ repo }}/commits/{{ parent_sha }}">{{ parent_sha }}</a></div>{% endfor %}</dd>+ <dd>{% for (parent_sha, _) in parents %}<div class="commit-parent"><a class="commit-sha" href="{{ site.base_url }}/{{ repo }}/commits/{{ parent_sha }}">{{ parent_sha }}</a></div>{% endfor %}</dd>~ <dt>author</dt><dd>{{ author }}</dd>~ <dt>date</dt><dd>{{ date }}</dd>~</dl>
Mtemplates/commit_list.html
~~{% block breadcrumbs %}~<ul>- <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>~ <li>commits</li>~</ul>~{% endblock %}
~ <div class="commit-graph-svg">{{ graph_svg|safe }}</div>~ <div class="commit-graph-rows">~ {% for commit in commits %}- <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ commit.sha }}" class="commit-graph-row" style="border-left: 3px solid {{ commit.color }}">+ <a href="{{ site.base_url }}/{{ repo }}/commits/{{ commit.sha }}" class="commit-graph-row" style="border-left: 3px solid {{ commit.color }}">~ <span class="commit-message">{% for r in commit.refs %}<span class="commit-ref commit-ref-{{ r.kind }}">{{ r.name }}</span> {% endfor %}{{ commit.message }}</span>~ <span class="commit-sha">{{ commit.sha[..8] }}</span>~ <span class="commit-date">{{ commit.date }}</span>
Mtemplates/error.html
~ <meta charset="UTF-8">~ <meta name="viewport" content="width=device-width, initial-scale=1.0">~ <title>{{ status }} - {{ kind }}</title>- <link rel="stylesheet" href="{{ base_url }}/style.css">+ <link rel="stylesheet" href="{{ site.base_url }}/style.css">~</head>~<body>~ <header>~ <ul>- <li><a href="{{ base_url }}/">{{ crate::hostname() }}</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>~ <li>error</li>~ </ul>~ </header>
Mtemplates/repo.html
~~{% block breadcrumbs %}~<ul>- <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ name }}">{{ name }}</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+ <li><a href="{{ site.base_url }}/{{ name }}">{{ name }}</a></li>~</ul>~{% endblock %}~
~<details>~ <summary><svg class="summary-icon"><use href="#icon-copy"/></svg>Clone</summary>~ <ul>- <li><button data-url="git@{{ crate::hostname() }}:{{ crate::ssh_prefix() }}{{ name }}.git"><span>git@{{ crate::hostname() }}:{{ crate::ssh_prefix() }}{{ name }}.git</span><span>SSH</span></button></li>- <li><button data-url="https://{{ crate::hostname() }}{{ crate::base_url() }}/{{ name }}.git"><span>https://{{ crate::hostname() }}/{{ name }}.git</span><span>HTTPS</span></button></li>+ <li><button data-url="git@{{ site.hostname }}:{{ site.ssh_prefix }}{{ name }}.git"><span>git@{{ site.hostname }}:{{ site.ssh_prefix }}{{ name }}.git</span><span>SSH</span></button></li>+ <li><button data-url="https://{{ site.hostname }}{{ site.base_url }}/{{ name }}.git"><span>https://{{ site.hostname }}/{{ name }}.git</span><span>HTTPS</span></button></li>~ </ul>~</details>~
~<details>~ <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>~ <ul>- <li><a href="{{ crate::base_url() }}/{{ name }}/archive/tar.gz/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.tar.gz"><span class="archive-link">{{ name }}-{{ branch }}.tar.gz</span><span>tar.gz</span></a></li>- <li><a href="{{ crate::base_url() }}/{{ name }}/archive/tar.xz/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.tar.xz"><span class="archive-link">{{ name }}-{{ branch }}.tar.xz</span><span>tar.xz</span></a></li>- <li><a href="{{ crate::base_url() }}/{{ name }}/archive/zip/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.zip"><span class="archive-link">{{ name }}-{{ branch }}.zip</span><span>zip</span></a></li>+ <li><a href="{{ site.base_url }}/{{ name }}/archive/tar.gz/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.tar.gz"><span class="archive-link">{{ name }}-{{ branch }}.tar.gz</span><span>tar.gz</span></a></li>+ <li><a href="{{ site.base_url }}/{{ name }}/archive/tar.xz/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.tar.xz"><span class="archive-link">{{ name }}-{{ branch }}.tar.xz</span><span>tar.xz</span></a></li>+ <li><a href="{{ site.base_url }}/{{ name }}/archive/zip/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.zip"><span class="archive-link">{{ name }}-{{ branch }}.zip</span><span>zip</span></a></li>~ </ul>~</details>~{% endif %}
~ <summary><svg class="summary-icon"><use href="#icon-branch"/></svg>Branches <span>[{{ branches.len() }}]</span></summary>~ <ul>~ {% for branch in branches.iter().take(5) %}- <li><a href="{{ crate::base_url() }}/{{ name }}/branches/{{ branch.name }}"><span class="branch-name">{{ branch.name }}</span>{% if branch.is_default %}<span>default</span>{% endif %}<span>{{ branch.date }}</span></a></li>+ <li><a href="{{ site.base_url }}/{{ name }}/branches/{{ branch.name }}"><span class="branch-name">{{ branch.name }}</span>{% if branch.is_default %}<span>default</span>{% endif %}<span>{{ branch.date }}</span></a></li>~ {% endfor %}~ {% if branches.len() > 5 %}- <li><a href="{{ crate::base_url() }}/{{ name }}/branches" class="see-all"><span>see all {{ branches.len() }} branches</span><span>[>]</span></a></li>+ <li><a href="{{ site.base_url }}/{{ name }}/branches" class="see-all"><span>see all {{ branches.len() }} branches</span><span>[>]</span></a></li>~ {% endif %}~ {% if branches.is_empty() %}~ <li>no branches</li>
~ <summary><svg class="summary-icon"><use href="#icon-tag"/></svg>Tags <span>[{{ tags.len() }}]</span></summary>~ <ul>~ {% for tag in tags.iter().take(5) %}- <li><a href="{{ crate::base_url() }}/{{ name }}/tags/{{ tag.name }}"><span class="tag-name">{{ tag.name }}</span><span>{{ tag.date }}</span></a></li>+ <li><a href="{{ site.base_url }}/{{ name }}/tags/{{ tag.name }}"><span class="tag-name">{{ tag.name }}</span><span>{{ tag.date }}</span></a></li>~ {% endfor %}~ {% if tags.len() > 5 %}- <li><a href="{{ crate::base_url() }}/{{ name }}/tags" class="see-all"><span>see all {{ tags.len() }} tags</span><span>[>]</span></a></li>+ <li><a href="{{ site.base_url }}/{{ name }}/tags" class="see-all"><span>see all {{ tags.len() }} tags</span><span>[>]</span></a></li>~ {% endif %}~ {% if tags.is_empty() %}~ <li>no tags</li>
~~<div class="repo-nav">~ {% if let Some(sha) = head_sha %}- <a href="{{ crate::base_url() }}/{{ name }}/commits/{{ sha }}/tree" class="repo-nav-item">+ <a href="{{ site.base_url }}/{{ name }}/commits/{{ sha }}/tree" class="repo-nav-item">~ <svg class="summary-icon"><use href="#icon-tree"/></svg>~ <span>Tree</span>~ <span class="repo-nav-item-meta">[{{ sha[..8] }}]</span>~ <span class="repo-nav-item-arrow">[>]</span>~ </a>~ {% endif %}- <a href="{{ crate::base_url() }}/{{ name }}/commits" class="repo-nav-item">+ <a href="{{ site.base_url }}/{{ name }}/commits" class="repo-nav-item">~ <svg class="summary-icon"><use href="#icon-commits"/></svg>~ <span>Commits</span>~ <span class="repo-nav-item-meta">[{{ commit_count }}]</span>
Mtemplates/repo_list.html
~~{% block breadcrumbs %}~<ul>- <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>~</ul>~{% endblock %}~
~{% else %}~<div class="repo-list">~ {% for repo in repos %}- <a href="{{ crate::base_url() }}/{{ repo.name }}" class="repo-card">+ <a href="{{ site.base_url }}/{{ repo.name }}" class="repo-card">~ <span class="repo-name">{{ repo.name }}</span>~ {% if let Some(desc) = &repo.description %}~ <span class="repo-desc">{{ desc }}</span>
Mtemplates/tag_detail.html
~~{% block breadcrumbs %}~<ul>- <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/tags">tags</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/tags">tags</a></li>~ <li>{{ tag }}</li>~</ul>~{% endblock %}
~<details>~ <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>~ <ul>- <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.gz/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.tar.gz"><span class="archive-link">{{ repo }}-{{ tag }}.tar.gz</span><span>tar.gz</span></a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.xz/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.tar.xz"><span class="archive-link">{{ repo }}-{{ tag }}.tar.xz</span><span>tar.xz</span></a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/zip/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.zip"><span class="archive-link">{{ repo }}-{{ tag }}.zip</span><span>zip</span></a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.gz/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.tar.gz"><span class="archive-link">{{ repo }}-{{ tag }}.tar.gz</span><span>tar.gz</span></a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.xz/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.tar.xz"><span class="archive-link">{{ repo }}-{{ tag }}.tar.xz</span><span>tar.xz</span></a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/archive/zip/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.zip"><span class="archive-link">{{ repo }}-{{ tag }}.zip</span><span>zip</span></a></li>~ </ul>~</details>~~<div class="repo-nav">- <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ commit_sha }}/tree" class="repo-nav-item">+ <a href="{{ site.base_url }}/{{ repo }}/commits/{{ commit_sha }}/tree" class="repo-nav-item">~ <svg class="summary-icon"><use href="#icon-tree"/></svg>~ <span>Tree</span>~ <span class="repo-nav-item-meta">[{{ commit_sha[..8] }}]</span>
~{% endif %}~~<dl class="tag-info">- <dt>commit</dt><dd><a class="commit-sha" href="{{ crate::base_url() }}/{{ repo }}/commits/{{ commit_sha }}">{{ commit_sha }}</a></dd>+ <dt>commit</dt><dd><a class="commit-sha" href="{{ site.base_url }}/{{ repo }}/commits/{{ commit_sha }}">{{ commit_sha }}</a></dd>~ <dt>author</dt><dd>{{ commit_author }}</dd>~ <dt>date</dt><dd>{{ commit_date }}</dd>~</dl>
Mtemplates/tag_list.html
~~{% block breadcrumbs %}~<ul>- <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>~ <li>tags</li>~</ul>~{% endblock %}
~{% else %}~<div class="tag-list">~ {% for tag in tags %}- <a href="{{ crate::base_url() }}/{{ repo }}/tags/{{ tag.name }}" class="tag-row">+ <a href="{{ site.base_url }}/{{ repo }}/tags/{{ tag.name }}" class="tag-row">~ <span class="tag-name">{{ tag.name }}</span>~ <span class="tag-date">{{ tag.date }}</span>~ </a>
Mtemplates/tree.html
~~{% block breadcrumbs %}~<ul>- <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>- <li><a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ sha }}">{{ sha[..8] }}</a></li>+ <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>+ <li><a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}">{{ sha[..8] }}</a></li>~ {% for crumb in breadcrumbs %}~ <li>{% if loop.last %}{{ crumb.label }}{% else %}<a href="{{ crumb.url }}">{{ crumb.label }}</a>{% endif %}</li>~ {% endfor %}
~ <span>Raw</span>~ <span class="repo-nav-item-arrow">[>]</span>~ </a>- <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ sha }}/blame/{{ path }}" class="repo-nav-item">+ <a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}/blame/{{ path }}" class="repo-nav-item">~ <svg class="summary-icon"><use href="#icon-blame"/></svg>~ <span>Blame</span>~ <span class="repo-nav-item-arrow">[>]</span>
Msrc/routes/blame.rs
~use axum::response::IntoResponse;~use serde::Deserialize;~+use crate::config::SiteConfig;~use crate::error::Result;~use crate::git;~use crate::routes::{AppState, Breadcrumb, RepoName};
~ tree_url: String,~ breadcrumbs: Vec<Breadcrumb>,~ lines: Vec<git::BlameEntry>,+ site: std::sync::Arc<SiteConfig>,~}~~/// Renders the blame view for a file.
~ let git_repo = git::open_repo(&state.root, &name)?;~ let lines = git::load_blame(&git_repo, ¶ms.sha, ¶ms.path)?;~- let b = crate::config::base_url();+ let b = &state.site.base_url;~ let tree_url = format!("{b}/{name}/commits/{}/tree/{}", params.sha, params.path);~ let breadcrumbs = crate::routes::build_breadcrumbs(b, &name, ¶ms.sha, ¶ms.path);~
~ tree_url,~ breadcrumbs,~ lines,+ site: state.site,~ })~}
Msrc/routes/branch.rs
~use axum::response::IntoResponse;~use serde::Deserialize;~+use crate::config::SiteConfig;~use crate::error::Result;~use crate::git;~use crate::routes::{AppState, RepoName};
~struct BranchList {~ repo: String,~ branches: Vec<git::BranchInfo>,+ site: std::sync::Arc<SiteConfig>,~}~~/// Template context for the branch detail (log) page.
~ commits: Vec<git::CommitSummary>,~ after: Option<String>,~ next_cursor: Option<String>,+ site: std::sync::Arc<SiteConfig>,~}~~/// Renders the full list of branches for a repository.
~) -> Result<impl IntoResponse> {~ let git_repo = git::open_repo(&state.root, &repo)?;~ let branches = git::load_branches(&git_repo);- Ok(BranchList { repo, branches })+ Ok(BranchList {+ repo,+ branches,+ site: state.site,+ })~}~~/// Renders a paginated log of commits reachable from the tip of the given branch.
~ commits: log.commits,~ after,~ next_cursor: log.next_cursor,+ site: state.site,~ })~}
Msrc/routes/commit.rs
~use gix::bstr::ByteSlice;~use serde::Deserialize;~+use crate::config::SiteConfig;~use crate::error::{Error, Result};~use crate::git::{self, GitResultExt as _, commit_date, committer_date};~use crate::routes::{AppState, RepoName};
~ after: Option<String>,~ /// Cursor for the next page of older commits (`None` means no more pages).~ next_cursor: Option<String>,+ site: std::sync::Arc<SiteConfig>,~}~~/// Renders a paginated commit log with an SVG ancestry graph.
~ commits,~ after,~ next_cursor,+ site: state.site,~ })~}~
~ lines_added: u64,~ lines_removed: u64,~ file_changes: Vec<FileChange>,+ site: std::sync::Arc<SiteConfig>,~}~~/// Displays a single commit: metadata, diff stat, and per-file inline diffs.
~ lines_added,~ lines_removed,~ file_changes,+ site: state.site,~ })~}
Msrc/routes/repo.rs
~~use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd, html};~+use crate::config::SiteConfig;~use crate::error::Result;~use crate::git::{self, commit_date};~use crate::routes::{AppState, RepoName};
~ head_sha: Option<String>,~ default_branch: Option<String>,~ readme: Option<String>,+ site: std::sync::Arc<SiteConfig>,~}~~/// Renders the detail page for a single repository, including its branches,
~~ let head_sha = head_id.map(|id| id.to_string());~+ let base_url = state.site.base_url.clone();+ let site = state.site;+~ let readme = ["HEAD:README.md", "HEAD:README", "HEAD:readme.md"]~ .iter()~ .find_map(|&spec| {
~ let dest_url = match &head_sha {~ Some(sha) if is_relative_url(&dest_url) => format!(~ "{}/{}/commits/{}/raw/{}",- crate::config::base_url(),+ base_url,~ name,~ sha,~ dest_url
~ let dest_url = match &head_sha {~ Some(sha) if is_relative_url(&dest_url) => format!(~ "{}/{}/commits/{}/tree/{}",- crate::config::base_url(),+ base_url,~ name,~ sha,~ dest_url
~ head_sha,~ default_branch,~ readme,+ site,~ })~}~
~#[template(path = "repo_list.html")]~struct RepoList {~ repos: Vec<RepoSummary>,+ site: std::sync::Arc<SiteConfig>,~}~~/// Renders the repository list page by scanning the root directory for git
~~ repos.sort_by(|a, b| b.last_commit.cmp(&a.last_commit));~- Ok(RepoList { repos })+ Ok(RepoList { repos, site: state.site })~}
Msrc/routes/tag.rs
~use axum::response::IntoResponse;~use serde::Deserialize;~+use crate::config::SiteConfig;~use crate::error::Result;~use crate::git;~use crate::routes::{AppState, RepoName};
~struct TagList {~ repo: String,~ tags: Vec<git::TagInfo>,+ site: std::sync::Arc<SiteConfig>,~}~~/// Template context for the tag detail page.
~ commit_sha: String,~ commit_author: String,~ commit_date: String,+ site: std::sync::Arc<SiteConfig>,~}~~/// Renders the full list of tags for a repository, sorted newest first.
~) -> Result<impl IntoResponse> {~ let git_repo = git::open_repo(&state.root, &repo)?;~ let tags = git::load_tags(&git_repo);- Ok(TagList { repo, tags })+ Ok(TagList {+ repo,+ tags,+ site: state.site,+ })~}~~/// Renders the detail page for a single tag.
~ commit_sha: commit.sha,~ commit_author: commit.author,~ commit_date: commit.date,+ site: state.site,~ })~}
Msrc/routes/tree.rs
~use syntect::html::{ClassStyle, ClassedHTMLGenerator};~use syntect::parsing::SyntaxSet;~+use crate::config::SiteConfig;~use crate::error::{Error, Result};~use crate::git::{self, GitResultExt as _};~use crate::routes::{AppState, Breadcrumb, RepoName};
~ content_html: String,~ raw_url: String,~ parent_url: String,+ site: std::sync::Arc<SiteConfig>,~}~~/// Renders a tree or file at `{sha}:{path}` in the given repository.
~ (obj, path)~ };~- let b = crate::config::base_url();+ let b = &state.site.base_url;~ let breadcrumbs = crate::routes::build_breadcrumbs(b, &name, ¶ms.sha, &resolved_path);~ let raw_url = if resolved_path.is_empty() {~ String::new()
~ content_html: String::new(),~ raw_url,~ parent_url,+ site: state.site,~ }~ .into_response())~ }
~ content_html,~ raw_url,~ parent_url,+ site: state.site,~ }~ .into_response())~ }