aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ba/back/src/web/generate
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/ba/back/src/web/generate')
-rw-r--r--pkgs/by-name/ba/back/src/web/generate/mod.rs227
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())
+}