// Back - An extremely simple git issue tracking system. Inspired by tvix's // panettone // // Copyright (C) 2024 Benedikt Peetz // SPDX-License-Identifier: AGPL-3.0-or-later // // This file is part of Back. // // You should have received a copy of the License along with this program. // If not, see . use crate::{ config::BackConfig, error::{self, Error}, git_bug::{ dag::issues_from_repository, issue::{CollapsedIssue, Status}, }, }; use prefix::BackPrefix; use rocket::{ get, response::content::{RawCss, RawHtml}, State, }; mod issue_html; pub mod prefix; #[get("/style.css")] pub fn styles() -> RawCss { RawCss(include_str!("../../assets/style.css").to_owned()) } pub fn issue_list_boilerplate( config: &State, wanted_status: Status, counter_status: Status, ) -> error::Result> { let repository = &config.repository; let mut issue_list = issues_from_repository(&repository.to_thread_local())? .into_iter() .map(|issue| issue.collapse()) .collect::>(); // Sort by date descending. issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() }); issue_list.reverse(); let issue_list_str = issue_list.into_iter().fold(String::new(), |acc, issue| { format!("{}{}", acc, { if issue.status == wanted_status { let issue_entry = issue.to_list_entry(); issue_entry.0 } else { String::new() } }) }); let counter_status_lower = counter_status.to_string().to_lowercase(); Ok(RawHtml(format!( r#" Back

{wanted_status} Issues

    {issue_list_str}
"#, config.source_code_repository_url ))) } #[get("/issues/open")] pub fn open(config: &State) -> error::Result> { issue_list_boilerplate(config, Status::Open, Status::Closed) } #[get("/issues/closed")] pub fn closed(config: &State) -> error::Result> { issue_list_boilerplate(config, Status::Closed, Status::Open) } #[get("/issues/feed")] pub fn feed(config: &State) -> error::Result> { use rss::{ChannelBuilder, Item, ItemBuilder}; let items: Vec = issues_from_repository(&config.repository.to_thread_local())? .into_iter() .map(|issue| issue.collapse()) .map(|issue| { ItemBuilder::default() .title(issue.title.to_string()) .author(issue.author.to_string()) .description(issue.message.to_string()) .pub_date(issue.timestamp.to_string()) .link(format!("{}/issue/{}", &config.root.to_string(), issue.id)) .build() }) .collect(); let channel = ChannelBuilder::default() .title("Issues") .link(config.root.to_string()) .description(format!("The rss feed for issues on {}.", config.root)) .items(items) .build(); Ok(RawHtml(channel.to_string())) } #[get("/issue/")] pub fn show_issue( config: &State, prefix: Result, ) -> error::Result> { // NOTE(@bpeetz): Explicitly unwrap the `prefix` here (instead of taking the unwrapped value as // argument), to avoid triggering rockets "errors forward to the next route" feature. // This ensures, that our error message actually reaches the user. <2024-12-26> let prefix = prefix?; let repository = config.repository.to_thread_local(); let all_issues: Vec = issues_from_repository(&repository)? .into_iter() .map(|val| val.collapse()) .collect(); let maybe_issue = all_issues .iter() .find(|issue| issue.id.to_string().starts_with(&prefix.to_string())); match maybe_issue { Some(issue) => Ok(issue.to_html(config)), None => Err(Error::IssuesPrefixMissing { prefix }), } }