repos
/ repos-rust master

repos-rust

mirror archived upstream

A minimal self-hosted git browser on Rust axum: bare repos rendered as a website with commits, diffs, syntax-highlighted blobs, atom feeds, and clone over HTTPS.

axumdockergitgit-browsergitoxidegixrustself-hosted

3.1 KB · 93 lines · Rust Raw History
 1use axum::{http::header, middleware as axum_middleware, Router};
 2use minijinja::Environment;
 3use std::path::PathBuf;
 4use std::sync::Arc;
 5use tower_http::services::ServeDir;
 6use tower_http::set_header::SetResponseHeaderLayer;
 7
 8use crate::middleware::log_requests;
 9use crate::routes;
10use crate::templates;
11
12#[derive(Clone)]
13pub struct AppState {
14    pub env: Arc<Environment<'static>>,
15    pub config: Arc<Config>,
16}
17
18#[derive(Debug, Clone)]
19pub struct Config {
20    pub root: PathBuf,
21    pub repo_root: PathBuf,
22    pub base_url: String,
23    pub clone_base: String,
24    pub site_title: String,
25    pub site_tagline: String,
26}
27
28impl AppState {
29    pub fn from_env() -> Self {
30        let root: PathBuf = std::env::var("REPOS_ROOT")
31            .map(PathBuf::from)
32            .unwrap_or_else(|_| PathBuf::from("."));
33        let repo_root: PathBuf = std::env::var("REPOS_REPO_ROOT")
34            .map(PathBuf::from)
35            .unwrap_or_else(|_| PathBuf::from("/srv/git"));
36        let base_url = std::env::var("BASE_URL").unwrap_or_default();
37        let clone_base = std::env::var("REPOS_CLONE_BASE")
38            .unwrap_or_else(|_| "https://repos.bythewood.me".to_string());
39        let site_title = std::env::var("REPOS_TITLE").unwrap_or_else(|_| "repos".to_string());
40        let site_tagline = std::env::var("REPOS_TAGLINE").unwrap_or_default();
41
42        let templates_dir = root.join("templates");
43        let manifest_path = root.join("dist/.vite/manifest.json");
44        let env = Arc::new(templates::build_env(&templates_dir, &manifest_path));
45
46        let config = Arc::new(Config {
47            root,
48            repo_root,
49            base_url,
50            clone_base,
51            site_title,
52            site_tagline,
53        });
54
55        Self { env, config }
56    }
57}
58
59pub fn router(state: AppState) -> Router {
60    let dist_dir = state.config.root.join("dist");
61
62    let static_cache = SetResponseHeaderLayer::if_not_present(
63        header::CACHE_CONTROL,
64        header::HeaderValue::from_static("public, max-age=31536000, immutable"),
65    );
66
67    Router::new()
68        // Smart-HTTP clone endpoints live at /:name.git/... and need to be
69        // matched before the catch-all repo routes; merge clone first.
70        .merge(routes::clone::router())
71        // seo routes (favicon, robots) before the repo catch-all so
72        // /favicon.ico doesn't hit /:name.
73        .merge(routes::seo::router())
74        .merge(routes::index::router())
75        .merge(routes::atom::router())
76        .merge(routes::log::router())
77        .merge(routes::commit::router())
78        .merge(routes::tree::router())
79        .merge(routes::blob::router())
80        // repo landing page is the most general single-segment route, so
81        // merge it last to avoid swallowing /static, /favicon.ico, etc.
82        .merge(routes::repo::router())
83        .nest_service(
84            "/static",
85            tower::ServiceBuilder::new()
86                .layer(static_cache)
87                .service(ServeDir::new(&dist_dir)),
88        )
89        .fallback(crate::middleware::not_found)
90        .layer(axum_middleware::from_fn(log_requests))
91        .with_state(state)
92}