wayver's git archive


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

src/handlers/index.rs@9b17ff94a4605ce4cb366db864eb0db65605b639

raw
Date Commit Message Author Files + -
2026-02-26 12:41 simple gitweb project listing-like implementation wayverd 5 56 40
...

1use std::fs;
2
3use axum::{
4    extract::State,
5    response::{IntoResponse as _, Response},
6};
7
8use crate::{
9    BileState,
10    config::Config,
11    error::{Context as _, Result},
12    git::Repository,
13    http::response::Html,
14    utils::filters,
15};
16
17#[derive(askama::Template)]
18#[template(path = "index.html")]
19struct IndexTemplate<'a> {
20    config: &'a Config,
21    sections: Vec<Section>,
22}
23
24struct Section {
25    name: Option<String>,
26    repos: Vec<Repository>,
27}
28
29#[tracing::instrument(skip_all)]
30pub(crate) async fn get(state: State<BileState>) -> Response {
31    state.spawn(move |state| inner(&state)).await
32}
33
34fn inner(state: &BileState) -> Result<Response> {
35    let Ok(read) = fs::read_dir(&state.config.project_root) else {
36        return Ok(Html(IndexTemplate {
37            config: &state.config,
38            sections: Vec::new(),
39        })
40        .into_response());
41    };
42
43    let mut sections = Vec::new();
44
45    for entry in read {
46        let entry = entry.context("failed to open directory entry")?;
47        let metadata = entry.metadata().context("failed to get file metadata")?;
48
49        if !metadata.is_dir() {
50            continue;
51        }
52
53        if entry
54            .file_name()
55            .to_str()
56            .is_some_and(|p| p != "." && p.starts_with('.'))
57        {
58            continue;
59        }
60
61        let Some(repo) = Repository::open_path(&state.config, &entry.path())
62            .context("failed to open repository")?
63        else {
64            continue;
65        };
66
67        // check for the export file in the git directory
68        // (the .git subfolder for non-bare repos)
69        if !repo.path().join(&state.config.export_ok).exists() {
70            continue;
71        }
72
73        let repo_section = repo.section();
74        let section = sections
75            .iter_mut()
76            .find(|s: &&mut Section| s.name == repo_section);
77        match section {
78            Some(section) => section.repos.push(repo),
79            None => sections.push(Section {
80                name: repo_section,
81                repos: vec![repo],
82            }),
83        }
84    }
85
86    sections.sort_by(|a, b| a.name.cmp(&b.name));
87    for section in &mut sections {
88        section.repos.sort_by(|a, b| a.name().cmp(&b.name()));
89    }
90
91    Ok(Html(IndexTemplate {
92        config: &state.config,
93        sections,
94    })
95    .into_response())
96}
97