raw
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 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