Parent [>]
1mod archive;
2mod blame;
3mod branch;
4mod commit;
5mod raw;
6mod repo;
7mod tag;
8mod tree;
9
10use std::collections::HashMap;
11use std::path::{Component, PathBuf};
12
13use axum::Router;
14use axum::extract::{FromRequestParts, Path, State};
15use axum::http::header;
16use axum::http::request::Parts;
17use axum::response::IntoResponse;
18use axum::routing::get;
19use jiff::Timestamp;
20use jiff::tz::{Offset, TimeZone};
21
22use crate::error::{Error, Result};
23
24/// Returns the commit's author timestamp as Unix seconds.
25pub(super) fn commit_timestamp(commit: &gix::Commit) -> Option<i64> {
26    commit.time().ok().map(|t| t.seconds)
27}
28
29/// Formats a commit's author date as a `YYYY-MM-DD` string in the commit's
30/// local timezone, returning `None` if the time cannot be parsed.
31pub(super) fn commit_date(commit: &gix::Commit) -> Option<String> {
32    let time = commit.time().ok()?;
33    let zone = TimeZone::fixed(Offset::from_seconds(time.offset).ok()?);
34
35    Timestamp::from_second(time.seconds)
36        .map(|ts| ts.to_zoned(zone).date().to_string())
37        .ok()
38}
39
40/// Formats a commit's committer date as a `YYYY-MM-DD` string in the
41/// commit's local timezone, returning `None` if the time cannot be parsed.
42pub(super) fn committer_date(commit: &gix::Commit) -> Option<String> {
43    let sig = commit.committer().ok()?;
44    let time = sig.time().ok()?;
45    let zone = TimeZone::fixed(Offset::from_seconds(time.offset).ok()?);
46
47    Timestamp::from_second(time.seconds)
48        .map(|ts| ts.to_zoned(zone).date().to_string())
49        .ok()
50}
51
52/// Axum extractor that pulls the `{repo}` path parameter and validates it
53/// contains no path traversal components before passing it to handlers.
54pub struct RepoName(pub String);
55
56impl<S: Send + Sync> FromRequestParts<S> for RepoName {
57    type Rejection = Error;
58
59    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self> {
60        let Path(params) = Path::<HashMap<String, String>>::from_request_parts(parts, state)
61            .await
62            .map_err(|e| Error::BadRequest(e.to_string()))?;
63
64        let name = params
65            .get("repo")
66            .ok_or_else(|| Error::BadRequest("missing repo parameter".to_string()))?
67            .clone();
68
69        // Reject names containing non-normal components (e.g. `..`) to prevent
70        // directory traversal.
71        if std::path::Path::new(&name)
72            .components()
73            .any(|c| !matches!(c, Component::Normal(_)))
74        {
75            return Err(Error::BadRequest("invalid repository name".to_string()));
76        }
77
78        Ok(RepoName(name))
79    }
80}
81
82/// Shared application state passed to all route handlers.
83#[derive(Clone)]
84pub struct AppState {
85    /// Path to the directory containing bare git repositories.
86    pub root: PathBuf,
87    /// Concatenated CSS served at `/style.css`.
88    pub css: String,
89}
90
91/// Serves the concatenated stylesheet.
92async fn style(State(state): State<AppState>) -> impl IntoResponse {
93    (
94        [(header::CONTENT_TYPE, "text/css; charset=utf-8")],
95        state.css,
96    )
97}
98
99/// Serves the favicon (SVG or ICO bytes loaded at startup).
100async fn favicon() -> impl IntoResponse {
101    ([(header::CONTENT_TYPE, "image/svg+xml")], crate::favicon())
102}
103
104/// Builds the application router with all routes and shared state attached.
105pub fn router(state: AppState) -> Router {
106    Router::new()
107        .route("/favicon.ico", get(favicon))
108        .route("/style.css", get(style))
109        .route("/", get(repo::list))
110        .route("/{repo}", get(repo::detail))
111        .route("/{repo}/tags", get(tag::list))
112        .route("/{repo}/tags/{*tag}", get(tag::detail))
113        .route("/{repo}/branches", get(branch::list))
114        .route("/{repo}/branches/{*branch}", get(branch::detail))
115        .route("/{repo}/commits", get(commit::list))
116        .route("/{repo}/commits/{sha}", get(commit::detail))
117        .route("/{repo}/commits/{sha}/tree", get(tree::handler))
118        .route("/{repo}/commits/{sha}/tree/{*path}", get(tree::handler))
119        .route("/{repo}/commits/{sha}/blame/{*path}", get(blame::handler))
120        .route("/{repo}/commits/{sha}/raw/{*path}", get(raw::handler))
121        .route("/{repo}/archive/{format}/{*ref}", get(archive::handler))
122        .with_state(state)
123}