use std::{fs, path::Path}; use gix::hash::Prefix; use log::info; use rinja::Template; use url::Url; use crate::{ config::BackConfig, error, git_bug::{ dag::issues_from_repository, issue::{CollapsedIssue, Status}, }, }; #[derive(Template)] #[template(path = "./issues.html")] struct IssuesTemplate { wanted_status: Status, counter_status: Status, issues: Vec, /// The path to the repository repo_path: String, /// The URL to `back`'s source code source_code_repository_url: Url, } pub fn issues( config: &BackConfig, wanted_status: Status, counter_status: Status, repo_path: &Path, ) -> error::Result { let repository = config .repositories()? .get(repo_path)? .open(&config.scan_path)?; let mut issue_list = issues_from_repository(&repository.to_thread_local())? .into_iter() .map(|issue| issue.collapse()) .filter(|issue| issue.status == wanted_status) .collect::>(); // Sort by date descending. // SAFETY: // The time stamp is only used for sorting, so a malicious attacker could only affect the issue // sorting. issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() }); issue_list.reverse(); Ok(IssuesTemplate { wanted_status, counter_status, source_code_repository_url: config.source_code_repository_url.clone(), issues: issue_list, repo_path: repo_path.display().to_string(), } .render() .expect("This should always work")) } use crate::git_bug::format::HtmlString; #[derive(Template)] #[template(path = "./issue.html")] struct IssueTemplate { issue: CollapsedIssue, /// The path to the repository repo_path: String, /// The URL to `back`'s source code source_code_repository_url: Url, } pub fn issue(config: &BackConfig, repo_path: &Path, prefix: Prefix) -> error::Result { let repository = config .repositories()? .get(repo_path)? .open(&config.scan_path)? .to_thread_local(); let maybe_issue = issues_from_repository(&repository)? .into_iter() .map(|val| val.collapse()) .find(|issue| issue.id.to_string().starts_with(&prefix.to_string())); match maybe_issue { Some(issue) => Ok(IssueTemplate { issue, repo_path: repo_path.display().to_string(), source_code_repository_url: config.source_code_repository_url.clone(), } .render() .expect("This should always work")), None => Err(error::Error::IssuesPrefixMissing { prefix }), } } #[derive(Template)] #[template(path = "./repos.html")] struct ReposTemplate { repos: Vec, /// The URL to `back`'s source code source_code_repository_url: Url, } struct RepoValue { description: String, owner: String, path: String, } pub fn repos(config: &BackConfig) -> error::Result { let repos: Vec = config .repositories()? .iter() .filter_map(|raw_repo| match raw_repo.open(&config.scan_path) { Ok(repo) => { let repo = repo.to_thread_local(); let git_config = repo.config_snapshot(); let path = raw_repo.path().to_string_lossy().to_string(); let owner = git_config .string("cgit.owner") .map(|v| v.to_string()) .unwrap_or("".to_owned()); let description = fs::read_to_string(repo.git_dir().join("description")) .unwrap_or("".to_owned()); Some(RepoValue { description, owner, path, }) } Err(err) => { info!( "Repo '{}' could not be opened: '{err}'", raw_repo.path().display() ); None } }) .collect(); Ok(ReposTemplate { repos, source_code_repository_url: config.source_code_repository_url.clone(), } .render() .expect("this should work")) } pub fn feed(config: &BackConfig, repo_path: &Path) -> error::Result { use rss::{ChannelBuilder, Item, ItemBuilder}; let repository = config .repositories()? .get(repo_path)? .open(&config.scan_path)? .to_thread_local(); let issues: Vec = issues_from_repository(&repository)? .into_iter() .map(|issue| issue.collapse()) .collect(); // Collect all Items as rss items let mut items: Vec = issues .iter() .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/{}", repo_path.display(), &config.root_url, issue.id )) .build() }) .collect(); // Append all comments after converting them to rss items items.extend( issues .iter() .filter(|issue| !issue.comments.is_empty()) .flat_map(|issue| { issue .comments .iter() .map(|comment| { ItemBuilder::default() .title(issue.title.to_string()) .author(comment.author.to_string()) .description(comment.message.to_string()) .pub_date(comment.timestamp.to_string()) .link(format!( "/{}/{}/issue/{}", repo_path.display(), &config.root_url, issue.id )) .build() }) .collect::>() }) .collect::>(), ); let channel = ChannelBuilder::default() .title("Issues") .link(config.root_url.to_string()) .description(format!("The rss feed for issues on {}.", &config.root_url)) .items(items) .build(); Ok(channel.to_string()) }