diff options
Diffstat (limited to 'pkgs/by-name/ba/back/src/web/generate')
-rw-r--r-- | pkgs/by-name/ba/back/src/web/generate/mod.rs | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/pkgs/by-name/ba/back/src/web/generate/mod.rs b/pkgs/by-name/ba/back/src/web/generate/mod.rs new file mode 100644 index 0000000..10146bb --- /dev/null +++ b/pkgs/by-name/ba/back/src/web/generate/mod.rs @@ -0,0 +1,227 @@ +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<CollapsedIssue>, + + /// 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<String> { + let repository = config + .repositories + .get(repo_path)? + .open(config.repositories.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::<Vec<CollapsedIssue>>(); + + // 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<String> { + let repository = config + .repositories + .get(repo_path)? + .open(config.repositories.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<RepoValue>, + + /// 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<String> { + let repos: Vec<RepoValue> = config + .repositories + .iter() + .filter_map( + |raw_repo| match raw_repo.open(config.repositories.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("<No owner>".to_owned()); + + let description = fs::read_to_string(repo.git_dir().join("description")) + .unwrap_or("<No description>".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<String> { + use rss::{ChannelBuilder, Item, ItemBuilder}; + + let repository = config + .repositories + .get(repo_path)? + .open(config.repositories.scan_path())? + .to_thread_local(); + + let issues: Vec<CollapsedIssue> = issues_from_repository(&repository)? + .into_iter() + .map(|issue| issue.collapse()) + .collect(); + + // Collect all Items as rss items + let mut items: Vec<Item> = 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, + 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, + issue.id + )) + .build() + }) + .collect::<Vec<Item>>() + }) + .collect::<Vec<Item>>(), + ); + + 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(channel.to_string()) +} |