wayver's git archive


a simple self-hosted git server
git clone https://git.wayver.dev/bile

src/handlers/repo_log.rs@6e060abc1f25c2e1b79fe06bfa8b72cd26952ee1

raw
Date Commit Message Author Files + -
2026-02-19 17:51 large refactoring wayverd 53 2153 1683
...

1use axum::{
2    extract::{Path, State},
3    http::StatusCode,
4    response::{IntoResponse as _, Response},
5};
6
7use crate::{
8    BileState,
9    config::Config,
10    error::Context as _,
11    git::Repository,
12    http::{
13        extractor::{ObjectName, Ref, RepoName},
14        response::{ErrorPage, Html, Redirect, Result},
15    },
16    utils::filters,
17};
18
19#[derive(askama::Template)]
20#[template(path = "log.html")]
21struct RepoLogTemplate<'a> {
22    config: &'a Config,
23    repo: &'a Repository,
24    commits: Vec<git2::Commit<'a>>,
25    branch: String,
26    // the spec the user should be linked to to see the next page of commits
27    next_page: Option<String>,
28}
29
30#[tracing::instrument(skip_all)]
31pub(crate) async fn get_1(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
32    state
33        .spawn(move |state| inner(&state, &repo_name, None, None))
34        .await
35}
36
37#[tracing::instrument(skip_all)]
38pub(crate) async fn get_2(
39    state: State<BileState>,
40    Path((repo_name, r#ref)): Path<(RepoName, Ref)>,
41) -> Response {
42    state
43        .spawn(move |state| inner(&state, &repo_name, Some(&r#ref), None))
44        .await
45}
46
47#[tracing::instrument(skip_all)]
48pub(crate) async fn get_3(
49    state: State<BileState>,
50    Path((repo_name, r#ref, object_name)): Path<(RepoName, Ref, ObjectName)>,
51) -> Response {
52    state
53        .spawn(move |state| inner(&state, &repo_name, Some(&r#ref), Some(&object_name)))
54        .await
55}
56
57fn inner(
58    state: &BileState,
59    repo_name: &RepoName,
60    r#ref: Option<&Ref>,
61    object_name: Option<&ObjectName>,
62) -> Result<Response> {
63    let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
64    else {
65        return Ok(ErrorPage::new(&state.config)
66            .with_status(StatusCode::NOT_FOUND)
67            .into_response());
68    };
69
70    if repo.is_empty()? {
71        return Ok(Redirect::permanent(&format!("/{repo_name}"))
72            .unwrap_or(Redirect::PERMANENT_ROOT)
73            .into_response());
74    }
75
76    let r = r#ref.map_or("HEAD", |r| r.0.as_str());
77
78    let next_page_spec = if repo.is_shallow() {
79        String::new()
80    } else if let Some(i) = r.rfind('~') {
81        // there is a tilde, try to find a number too
82        let n = r[i + 1..].parse::<usize>().ok().unwrap_or(1);
83        format!("{}~{}", &r[..i], n + state.config.log_per_page)
84    } else {
85        // there was no tilde
86        format!("{}~{}", r, state.config.log_per_page)
87    };
88
89    let Some(mut commits) = repo
90        .commits_for_obj(r, state.config.log_per_page + 1, object_name)
91        .context("failed to get commits for object")?
92    else {
93        return Ok(ErrorPage::new(&state.config)
94            .with_status(StatusCode::NOT_FOUND)
95            .into_response());
96    };
97
98    // check if there even is a next page
99    let next_page = if commits.len() < state.config.log_per_page + 1 {
100        None
101    } else {
102        // remove additional commit from next page check
103        commits.pop();
104        Some(next_page_spec)
105    };
106
107    let branch = repo.ref_or_head_shorthand(r#ref)?;
108
109    Ok(Html(RepoLogTemplate {
110        config: &state.config,
111        repo: &repo,
112        commits,
113        branch,
114        next_page,
115    })
116    .into_response())
117}
118