From a3111a5d214db66b7d676940b8f8bfda5074bd45 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Thu, 26 Dec 2024 10:00:45 +0100 Subject: fix(pkgs/back): Use rocket to manage the configuration values This reduces the amount of needed `unwraps`/`expects` and allows us to streamline the parsing processes. --- pkgs/by-name/ba/back/src/issues/format/mod.rs | 88 ------- pkgs/by-name/ba/back/src/issues/issue/mod.rs | 332 -------------------------- pkgs/by-name/ba/back/src/issues/issue/raw.rs | 145 ----------- pkgs/by-name/ba/back/src/issues/issue_show.rs | 34 --- pkgs/by-name/ba/back/src/issues/mod.rs | 134 ----------- 5 files changed, 733 deletions(-) delete mode 100644 pkgs/by-name/ba/back/src/issues/format/mod.rs delete mode 100644 pkgs/by-name/ba/back/src/issues/issue/mod.rs delete mode 100644 pkgs/by-name/ba/back/src/issues/issue/raw.rs delete mode 100644 pkgs/by-name/ba/back/src/issues/issue_show.rs delete mode 100644 pkgs/by-name/ba/back/src/issues/mod.rs (limited to 'pkgs/by-name/ba/back/src/issues') diff --git a/pkgs/by-name/ba/back/src/issues/format/mod.rs b/pkgs/by-name/ba/back/src/issues/format/mod.rs deleted file mode 100644 index f78d3b3..0000000 --- a/pkgs/by-name/ba/back/src/issues/format/mod.rs +++ /dev/null @@ -1,88 +0,0 @@ -// 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 std::fmt::Display; - -use markdown::to_html; - -#[derive(Debug, Default, Clone)] -pub struct Markdown { - value: String, -} - -impl From for Markdown { - fn from(value: String) -> Self { - Self { value } - } -} -impl Display for Markdown { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(to_html(&self.value).as_str()) - } -} - -#[derive(Debug, Default)] -pub struct BackString { - value: String, -} - -impl From for BackString { - fn from(value: Markdown) -> Self { - Self { value: value.value } - } -} - -impl From for BackString { - fn from(value: String) -> Self { - Self { value } - } -} -impl Display for BackString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(escape_html(&self.value).as_str()) - } -} - -// From `tera::escape_html` -/// Escape HTML following [OWASP](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) -/// -/// Escape the following characters with HTML entity encoding to prevent switching -/// into any execution context, such as script, style, or event handlers. Using -/// hex entities is recommended in the spec. In addition to the 5 characters -/// significant in XML (&, <, >, ", '), the forward slash is included as it helps -/// to end an HTML entity. -/// -/// ```text -/// & --> & -/// < --> < -/// > --> > -/// " --> " -/// ' --> ' ' is not recommended -/// / --> / forward slash is included as it helps end an HTML entity -/// ``` -#[inline] -pub fn escape_html(input: &str) -> String { - let mut output = String::with_capacity(input.len() * 2); - for c in input.chars() { - match c { - '&' => output.push_str("&"), - '<' => output.push_str("<"), - '>' => output.push_str(">"), - '"' => output.push_str("""), - '\'' => output.push_str("'"), - '/' => output.push_str("/"), - _ => output.push(c), - } - } - - // Not using shrink_to_fit() on purpose - output -} diff --git a/pkgs/by-name/ba/back/src/issues/issue/mod.rs b/pkgs/by-name/ba/back/src/issues/issue/mod.rs deleted file mode 100644 index b78f473..0000000 --- a/pkgs/by-name/ba/back/src/issues/issue/mod.rs +++ /dev/null @@ -1,332 +0,0 @@ -// 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 std::fmt::Display; - -use chrono::DateTime; -use gix::{bstr::ByteSlice, Commit, Id, ObjectId, Repository}; -use raw::{Operation, RawIssue}; -use rocket::response::content::RawHtml; - -use crate::SOURCE_CODE_REPOSITORY; - -use super::format::{BackString, Markdown}; - -mod raw; - -#[derive(Debug, Default)] -pub struct TimeStamp { - value: u64, -} -impl TimeStamp { - pub fn new(val: u64) -> Self { - Self { value: val } - } -} -impl Display for TimeStamp { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let date = - DateTime::from_timestamp(self.value as i64, 0).expect("This timestamp should be vaild"); - - let newdate = date.format("%Y-%m-%d %H:%M:%S"); - f.write_str(newdate.to_string().as_str()) - } -} - -#[derive(Debug)] -pub struct Comment<'a> { - pub id: Id<'a>, - pub author: Author, - pub message: Markdown, - pub timestamp: TimeStamp, -} - -#[derive(Debug, Default)] -pub struct Author { - name: BackString, - email: BackString, -} - -#[derive(Debug)] -pub struct IssueId<'a> { - value: Id<'a>, -} -impl<'a> IssueId<'a> { - pub fn new(id: Id<'a>) -> Self { - Self { value: id } - } -} -impl Display for IssueId<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let shortend = self.value.shorten().expect("This should work."); - f.write_str(shortend.to_string().as_str()) - } -} - -#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] -pub enum Status { - #[default] - Open, - Closed, -} -impl Display for Status { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Status::Open => f.write_str("Open"), - Status::Closed => f.write_str("Closed"), - } - } -} - -#[derive(Debug)] -pub struct Issue<'a> { - pub id: IssueId<'a>, - pub author: Author, - pub timestamp: TimeStamp, - pub title: Markdown, - pub message: Markdown, - pub comments: Vec>, - pub status: Status, - pub last_status_change: Option, -} -impl<'a> Issue<'a> { - pub fn default_with_id(id: Id<'a>) -> Self { - Self { - id: IssueId::new(id), - author: Author::default(), - timestamp: TimeStamp::default(), - title: Markdown::default(), - message: Markdown::default(), - comments: >::default(), - status: Status::default(), - last_status_change: >::default(), - } - } - - pub fn from_commit_id(repo: &'a Repository, commit_id: ObjectId) -> Self { - fn unwrap_id<'b>(repo: &Repository, id: &Commit<'b>) -> (RawIssue, Id<'b>) { - let tree_obj = repo - .find_object(id.tree_id().unwrap()) - .expect("The object with this id should exist.") - .try_into_tree() - .expect("The git-bug's data model enforces this."); - - let ops_ref = tree_obj.find_entry("ops").unwrap(); - - let issue_data = repo - .find_object(ops_ref.object_id()) - .expect("The object with this id should exist.") - .try_into_blob() - .expect("The git-bug's data model enforces this.") - .data - .clone(); - - let raw_issue = serde_json::from_str( - issue_data - .to_str() - .expect("git-bug's ensures, that this is valid json."), - ) - .expect("The returned json should be valid"); - - (raw_issue, id.id()) - } - - let commit_obj = repo - .find_object(commit_id) - .expect("The object with this id should exist.") - .try_into_commit() - .expect("The git-bug's data model enforces this."); - - let mut issues = vec![unwrap_id(repo, &commit_obj)]; - - let mut current_commit_obj = commit_obj; - while current_commit_obj.parent_ids().count() != 0 { - assert_eq!( - current_commit_obj.parent_ids().count(), - 1, - "There should be only one parent" - ); - let parent = current_commit_obj - .parent_ids() - .last() - .expect("One does exist"); - - let parent_id = parent.object().expect("The object exists").id; - let parent_commit = repo - .find_object(parent_id) - .expect("This is a valid id") - .try_into_commit() - .expect("This should be a commit"); - - issues.push(unwrap_id(repo, &parent_commit)); - current_commit_obj = parent_commit; - } - - let mut final_issue = Self::default_with_id(current_commit_obj.id()); - for (issue, id) in issues { - for op in issue.operations { - match op { - Operation::AddComment { timestamp, message } => { - final_issue.comments.push(Comment { - id, - author: issue.author.load_identity(repo), - message: Markdown::from(message), - timestamp: TimeStamp::new(timestamp), - }) - } - Operation::Create { - timestamp, - title, - message, - } => { - final_issue.author = issue.author.load_identity(repo); - final_issue.title = Markdown::from(title); - final_issue.message = Markdown::from(message); - final_issue.timestamp = TimeStamp::new(timestamp); - } - Operation::SetStatus { timestamp, status } => { - final_issue.status = status; - final_issue.last_status_change = Some(TimeStamp::new(timestamp)); - } - } - } - } - final_issue - } - - pub fn to_list_entry(&self) -> RawHtml { - let comment_list = if self.comments.is_empty() { - String::new() - } else { - format!( - r#" - - {} comments - "#, - self.comments.len() - ) - }; - let Issue { - id, - title, - message: _, - author, - timestamp, - comments: _, - status: _, - last_status_change: _, - } = self; - let Author { name, email } = author; - RawHtml(format!( - r#" -
  • - -

    - {title} -

    - {id} - Opened by {name} <{email}> at {timestamp}{comment_list}
    -
  • -"#, - )) - } - - pub fn to_html(&self) -> RawHtml { - let fmt_comments: String = self - .comments - .iter() - .map(|val| { - let Comment { - id, - author, - message, - timestamp, - } = val; - let Author { name, email: _ } = author; - - format!( - r#" -
  • - {message} -

    {name} at {timestamp}

    -
  • - "#, - ) - }) - .collect::>() - .join("\n"); - - let maybe_comments = if fmt_comments.is_empty() { - String::new() - } else { - format!( - r#" -
      - {fmt_comments} -
    - "# - ) - }; - - { - let Issue { - id, - title, - message, - author, - timestamp, - comments: _, - status: _, - last_status_change: _, - } = self; - let Author { name, email } = author; - let html_title = BackString::from(title.clone()); - - RawHtml(format!( - r#" - - - - {html_title} | Back - - - - -
    - -
    -

    {title}

    -
    {id}
    -
    -
    -
    - Opened by {name} <{email}> at {timestamp} -
    - {message} - {maybe_comments} -
    - -
    - - -"#, - SOURCE_CODE_REPOSITORY.get().expect("This should be set") - )) - } - } -} diff --git a/pkgs/by-name/ba/back/src/issues/issue/raw.rs b/pkgs/by-name/ba/back/src/issues/issue/raw.rs deleted file mode 100644 index 48d2a9f..0000000 --- a/pkgs/by-name/ba/back/src/issues/issue/raw.rs +++ /dev/null @@ -1,145 +0,0 @@ -// 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 gix::{bstr::ByteSlice, Repository}; -use serde::Deserialize; -use serde_json::Value; - -use crate::issues::format::BackString; - -use super::{Author, Status}; - -macro_rules! get { - ($value:expr, $name:expr, $type_fun:ident) => { - $value - .get($name) - .expect(concat!( - "Expected field ", - stringify!($name), - "to exists, but was missing." - )) - .$type_fun() - .expect(concat!( - "Failed to interpret field ", - stringify!($name), - " as ", - stringify!($type), - "!" - )) - }; -} - -#[derive(Deserialize)] -pub(super) struct RawIssue { - pub(super) author: RawAuthor, - - #[serde(alias = "ops")] - pub(super) operations: Vec, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub(super) struct RawAuthor { - id: String, -} - -impl RawAuthor { - pub fn load_identity(&self, repo: &Repository) -> Author { - let commit_obj = repo - .find_reference(&format!("refs/identities/{}", self.id)) - .expect("All authors should also have identities") - .peel_to_commit() - .expect("All identities should be commits"); - let tree_obj = repo - .find_tree( - commit_obj - .tree() - .expect("The commit should have an tree associated with it") - .id, - ) - .expect("This should be a tree"); - let data = repo - .find_blob( - tree_obj - .find_entry("version") - .expect("This entry should exist") - .object() - .expect("This should point to a blob entry") - .id, - ) - .expect("This blob should exist") - .data - .clone(); - - let json: Value = serde_json::from_str(data.to_str().expect("This is encoded json")) - .expect("This is valid json"); - - Author { - name: BackString::from(get! {json, "name", as_str}.to_owned()), - email: BackString::from(get! {json, "email", as_str}.to_owned()), - } - } -} - -#[derive(Deserialize)] -#[serde(from = "Value")] -pub(super) enum Operation { - AddComment { - timestamp: u64, - message: String, - // files: Option, TODO - }, - SetStatus { - timestamp: u64, - status: Status, - }, - Create { - timestamp: u64, - title: String, - message: String, - // files: Option, TODO - }, -} - -impl From for Status { - fn from(value: u64) -> Self { - match value { - 1 => Status::Open, - 2 => Status::Closed, - other => todo!("The status ({other}) is not yet implemented."), - } - } -} - -impl From for Operation { - fn from(value: Value) -> Self { - match value - .get("type") - .expect("Should exist") - .as_u64() - .expect("This should work") - { - 1 => Self::Create { - title: get! {value, "title", as_str}.to_owned(), - message: get! {value, "message", as_str}.to_owned(), - timestamp: get! {value, "timestamp", as_u64}, - }, - 3 => Self::AddComment { - message: get! {value, "message", as_str}.to_owned(), - timestamp: get! {value, "timestamp", as_u64}, - }, - 4 => Self::SetStatus { - status: Status::from(get! {value, "status", as_u64}), - timestamp: get! {value, "timestamp", as_u64}, - }, - other => todo!("The type ({other}) is not yet added as a a valid operation. It's value is: '{value}''"), - } - } -} diff --git a/pkgs/by-name/ba/back/src/issues/issue_show.rs b/pkgs/by-name/ba/back/src/issues/issue_show.rs deleted file mode 100644 index 638840e..0000000 --- a/pkgs/by-name/ba/back/src/issues/issue_show.rs +++ /dev/null @@ -1,34 +0,0 @@ -// 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 std::fmt::Display; - -use gix::hash::Prefix; -use rocket::request::FromParam; - -pub struct BackPrefix { - prefix: Prefix, -} -impl Display for BackPrefix { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.prefix.fmt(f) - } -} - -impl<'a> FromParam<'a> for BackPrefix { - type Error = gix::hash::prefix::from_hex::Error; - - fn from_param(param: &'a str) -> Result { - let prefix = Prefix::from_hex(param)?; - - Ok(Self { prefix }) - } -} diff --git a/pkgs/by-name/ba/back/src/issues/mod.rs b/pkgs/by-name/ba/back/src/issues/mod.rs deleted file mode 100644 index 744d5ba..0000000 --- a/pkgs/by-name/ba/back/src/issues/mod.rs +++ /dev/null @@ -1,134 +0,0 @@ -// 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 std::path::Path; - -use crate::{ - issues::issue::{Issue, Status}, - SOURCE_CODE_REPOSITORY, -}; -use format::BackString; -use gix::{refs::Target, Repository}; -use issue_show::BackPrefix; -use rocket::{ - get, - response::content::{RawCss, RawHtml}, -}; - -use crate::REPOSITORY; - -mod format; -mod issue; -mod issue_show; - -#[get("/style.css")] -pub fn styles() -> RawCss { - RawCss(include_str!("../../assets/style.css").to_owned()) -} - -fn list_all_issues(repo: &'_ Repository) -> Vec> { - repo.refs - .iter() - .expect("We should be able to iterate over references") - .prefixed(Path::new("refs/bugs/")) - .expect("The 'refs/bugs/' namespace should exist") - .map(|val| { - let reference = val.expect("'val' should be an object?"); - if let Target::Object(commit_id) = reference.target { - Issue::from_commit_id(repo, commit_id) - } else { - unreachable!("All 'refs/bugs/' should contain a clear target."); - } - }) - .collect() -} - -pub fn issue_list_boilerplate(wanted_status: Status, counter_status: Status) -> RawHtml { - let repository = REPOSITORY.get().expect("This should have been set"); - - let issue_list = list_all_issues(&repository.to_thread_local()) - .iter() - .fold(String::new(), |acc, val| { - format!("{}{}", acc, &issue_to_string(val, wanted_status).0) - }); - - let counter_status_lower = counter_status.to_string().to_lowercase(); - RawHtml(format!( - r#" - - - - Back - - - - -
    -
    -

    {wanted_status} Issues

    -
    -
    - -
      - {issue_list} -
    -
    -
    - - -"#, - SOURCE_CODE_REPOSITORY.get().expect("This should be set") - )) -} - -#[get("/issues/open")] -pub fn open() -> RawHtml { - issue_list_boilerplate(Status::Open, Status::Closed) -} -#[get("/issues/closed")] -pub fn closed() -> RawHtml { - issue_list_boilerplate(Status::Closed, Status::Open) -} - -#[get("/issue/")] -pub fn show_issue(prefix: BackPrefix) -> RawHtml { - let repository = REPOSITORY - .get() - .expect("This should have been set") - .to_thread_local(); - - let all_issues = list_all_issues(&repository); - let maybe_issue = all_issues - .iter() - .find(|issue| issue.id.to_string().starts_with(&prefix.to_string())); - - match maybe_issue { - Some(issue) => issue.to_html(), - None => RawHtml(format!("Issue with id '{prefix}' not found!")), - } -} - -fn issue_to_string(issue: &Issue<'_>, status: Status) -> RawHtml { - if issue.status == status { - issue.to_list_entry() - } else { - RawHtml(String::default()) - } -} -- cgit 1.4.1