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/Cargo.lock | 122 ++++++++++ pkgs/by-name/ba/back/Cargo.toml | 4 +- pkgs/by-name/ba/back/contrib/config.json | 4 + pkgs/by-name/ba/back/src/cli.rs | 13 + pkgs/by-name/ba/back/src/config/mod.rs | 58 +++++ pkgs/by-name/ba/back/src/error/mod.rs | 56 +++++ 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 ---------- pkgs/by-name/ba/back/src/main.rs | 73 ++---- pkgs/by-name/ba/back/src/web/format/mod.rs | 88 +++++++ pkgs/by-name/ba/back/src/web/issue/mod.rs | 337 ++++++++++++++++++++++++++ pkgs/by-name/ba/back/src/web/issue/raw.rs | 145 +++++++++++ pkgs/by-name/ba/back/src/web/issue_show.rs | 34 +++ pkgs/by-name/ba/back/src/web/mod.rs | 133 ++++++++++ 17 files changed, 1014 insertions(+), 786 deletions(-) create mode 100644 pkgs/by-name/ba/back/contrib/config.json create mode 100644 pkgs/by-name/ba/back/src/cli.rs create mode 100644 pkgs/by-name/ba/back/src/config/mod.rs create mode 100644 pkgs/by-name/ba/back/src/error/mod.rs 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 create mode 100644 pkgs/by-name/ba/back/src/web/format/mod.rs create mode 100644 pkgs/by-name/ba/back/src/web/issue/mod.rs create mode 100644 pkgs/by-name/ba/back/src/web/issue/raw.rs create mode 100644 pkgs/by-name/ba/back/src/web/issue_show.rs create mode 100644 pkgs/by-name/ba/back/src/web/mod.rs (limited to 'pkgs/by-name') diff --git a/pkgs/by-name/ba/back/Cargo.lock b/pkgs/by-name/ba/back/Cargo.lock index ed7f120..b72f23c 100644 --- a/pkgs/by-name/ba/back/Cargo.lock +++ b/pkgs/by-name/ba/back/Cargo.lock @@ -70,6 +70,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -141,11 +190,13 @@ name = "back" version = "0.1.0" dependencies = [ "chrono", + "clap", "gix", "markdown", "rocket", "serde", "serde_json", + "thiserror", "url", ] @@ -246,12 +297,58 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "clru" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "cookie" version = "0.18.1" @@ -1469,6 +1566,12 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1775,6 +1878,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.14" @@ -2548,6 +2657,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.91" @@ -2898,6 +3013,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -2912,6 +3028,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.0" diff --git a/pkgs/by-name/ba/back/Cargo.toml b/pkgs/by-name/ba/back/Cargo.toml index f697511..2fc0600 100644 --- a/pkgs/by-name/ba/back/Cargo.toml +++ b/pkgs/by-name/ba/back/Cargo.toml @@ -23,12 +23,14 @@ license = "AGPL-3.0-or-later" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] chrono = "0.4.39" +clap = { version = "4.5.23", features = ["derive"] } gix = "0.69.1" markdown = "1.0.0-alpha.21" rocket = "0.5.1" serde = "1.0.216" serde_json = "1.0.134" -url = "2.5.4" +thiserror = "2.0.9" +url = { version = "2.5.4", features = ["serde"] } [profile.release] lto = true diff --git a/pkgs/by-name/ba/back/contrib/config.json b/pkgs/by-name/ba/back/contrib/config.json new file mode 100644 index 0000000..10173fb --- /dev/null +++ b/pkgs/by-name/ba/back/contrib/config.json @@ -0,0 +1,4 @@ +{ + "source_code_repository_url": "https://git.foss-syndicate.org/vhack.eu/nixos-server/tree/pkgs/by-name/ba/back", + "repository_path": "/path/to/your/repository" +} diff --git a/pkgs/by-name/ba/back/src/cli.rs b/pkgs/by-name/ba/back/src/cli.rs new file mode 100644 index 0000000..1820bf3 --- /dev/null +++ b/pkgs/by-name/ba/back/src/cli.rs @@ -0,0 +1,13 @@ +use std::path::PathBuf; + +use clap::Parser; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +#[allow(clippy::module_name_repetitions)] +/// An extremely simple git issue tracking system. +/// Inspired by tvix's panettone +pub struct Cli { + /// The path to the configuration file. The file should be written in JSON. + pub config_file: PathBuf, +} diff --git a/pkgs/by-name/ba/back/src/config/mod.rs b/pkgs/by-name/ba/back/src/config/mod.rs new file mode 100644 index 0000000..4986a41 --- /dev/null +++ b/pkgs/by-name/ba/back/src/config/mod.rs @@ -0,0 +1,58 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use gix::ThreadSafeRepository; +use serde::Deserialize; +use url::Url; + +use crate::error::{self, Error}; + +pub struct BackConfig { + // NOTE(@bpeetz): We do not need to html escape this, as the value must be a valid url. As such + // `` of all kinds _should_ be invalid. <2024-12-26> + pub source_code_repository_url: Url, + pub repository: ThreadSafeRepository, +} + +#[derive(Deserialize)] +struct RawBackConfig { + source_code_repository_url: Url, + repository_path: PathBuf, +} + +impl BackConfig { + pub fn from_config_file(path: &Path) -> error::Result { + let value = fs::read_to_string(path).map_err(|err| Error::ConfigRead { + file: path.to_owned(), + error: err, + })?; + + let raw: RawBackConfig = + serde_json::from_str(&value).map_err(|err| Error::ConfigParse { + file: path.to_owned(), + error: err, + })?; + + Self::try_from(raw) + } +} + +impl TryFrom for BackConfig { + type Error = error::Error; + + fn try_from(value: RawBackConfig) -> Result { + let repository = { + ThreadSafeRepository::open(&value.repository_path).map_err(|err| Error::RepoOpen { + repository_path: value.repository_path, + error: Box::new(err), + }) + }?; + + Ok(Self { + repository, + source_code_repository_url: value.source_code_repository_url, + }) + } +} diff --git a/pkgs/by-name/ba/back/src/error/mod.rs b/pkgs/by-name/ba/back/src/error/mod.rs new file mode 100644 index 0000000..7e1c9cf --- /dev/null +++ b/pkgs/by-name/ba/back/src/error/mod.rs @@ -0,0 +1,56 @@ +use std::{fmt::Display, io, path::PathBuf}; + +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum Error { + ConfigParse { + file: PathBuf, + error: serde_json::Error, + }, + ConfigRead { + file: PathBuf, + error: io::Error, + }, + RepoOpen { + repository_path: PathBuf, + error: Box, + }, + RocketLaunch(#[from] rocket::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ConfigParse { file, error } => { + write!( + f, + "while trying to parse the config file ({}): {error}", + file.display() + ) + } + Error::ConfigRead { file, error } => { + write!( + f, + "while trying to read the config file ({}): {error}", + file.display() + ) + } + Error::RepoOpen { + repository_path, + error, + } => { + write!( + f, + "while trying to open the repository ({}): {error}", + repository_path.display() + ) + } + Error::RocketLaunch(error) => { + write!(f, "while trying to start back: {error}") + } + } + } +} 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()) - } -} diff --git a/pkgs/by-name/ba/back/src/main.rs b/pkgs/by-name/ba/back/src/main.rs index 86fe196..e8f36d2 100644 --- a/pkgs/by-name/ba/back/src/main.rs +++ b/pkgs/by-name/ba/back/src/main.rs @@ -9,62 +9,31 @@ // You should have received a copy of the License along with this program. // If not, see . -use std::{env::args, path::PathBuf, process, sync::OnceLock}; +use clap::Parser; +use config::BackConfig; +use rocket::routes; -use gix::ThreadSafeRepository; -use rocket::{launch, routes}; -use url::Url; +use crate::web::{closed, open, show_issue, styles}; -use crate::issues::{closed, open, show_issue, styles}; +mod cli; +pub mod config; +mod error; +mod web; -mod issues; +#[rocket::main] +async fn main() -> Result<(), error::Error> { + let args = cli::Cli::parse(); -static REPOSITORY: OnceLock = OnceLock::new(); -static SOURCE_CODE_REPOSITORY: OnceLock = OnceLock::new(); + let config = BackConfig::from_config_file(&args.config_file)?; -#[launch] -fn rocket() -> _ { - let repository_path = { - let maybe_path = args().skip(1).rev().last(); - if let Some(path) = maybe_path { - PathBuf::from(path) - } else { - eprintln!("Usage: back "); - process::exit(1); - } - }; - let source_code_url = { - match std::env::var("BACK_SOURCE_CODE_REPOSITORY_URL") { - Ok(value) => match Url::parse(&value) { - Ok(url) => url, - Err(err) => { - eprintln!("Can't parse `BACK_SOURCE_CODE_REPOSITORY_URL` value as url: {err}"); - process::exit(1); - } - }, - Err(err) => { - eprintln!("back needs you to specify a source code repositiory as `BACK_SOURCE_CODE_REPOSITORY_URL`: {err}"); - process::exit(1); - } - } - }; + rocket::build() + .mount("/", routes![open, closed, show_issue, styles]) + .manage(config) + .ignite() + .await + .expect("This error should only happen on a miss-configuration.") + .launch() + .await?; - SOURCE_CODE_REPOSITORY - .set(source_code_url) - .expect("This should be unset by this stage"); - - REPOSITORY - .set( - ThreadSafeRepository::open(&repository_path).unwrap_or_else(|err| { - eprintln!( - "Error while opening repository ('{}'): {} ", - repository_path.display(), - err - ); - process::exit(1); - }), - ) - .expect("There should be only one thread accessing this right now"); - - rocket::build().mount("/", routes![open, closed, show_issue, styles]) + Ok(()) } diff --git a/pkgs/by-name/ba/back/src/web/format/mod.rs b/pkgs/by-name/ba/back/src/web/format/mod.rs new file mode 100644 index 0000000..f78d3b3 --- /dev/null +++ b/pkgs/by-name/ba/back/src/web/format/mod.rs @@ -0,0 +1,88 @@ +// 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/web/issue/mod.rs b/pkgs/by-name/ba/back/src/web/issue/mod.rs new file mode 100644 index 0000000..79ef70f --- /dev/null +++ b/pkgs/by-name/ba/back/src/web/issue/mod.rs @@ -0,0 +1,337 @@ +// 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::config::BackConfig; + +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() + .expect("All of git-bug's commits should have trees attached to them'"), + ) + .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") + .expect("All of git-bug's trees should contain a 'ops' json file"); + + 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, config: &BackConfig) -> 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} +
    + +
    + + +"#, + config.source_code_repository_url + )) + } + } +} diff --git a/pkgs/by-name/ba/back/src/web/issue/raw.rs b/pkgs/by-name/ba/back/src/web/issue/raw.rs new file mode 100644 index 0000000..bb447ec --- /dev/null +++ b/pkgs/by-name/ba/back/src/web/issue/raw.rs @@ -0,0 +1,145 @@ +// 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::web::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/web/issue_show.rs b/pkgs/by-name/ba/back/src/web/issue_show.rs new file mode 100644 index 0000000..638840e --- /dev/null +++ b/pkgs/by-name/ba/back/src/web/issue_show.rs @@ -0,0 +1,34 @@ +// 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/web/mod.rs b/pkgs/by-name/ba/back/src/web/mod.rs new file mode 100644 index 0000000..ed91e7e --- /dev/null +++ b/pkgs/by-name/ba/back/src/web/mod.rs @@ -0,0 +1,133 @@ +// 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::{ + config::BackConfig, + web::issue::{Issue, Status}, +}; +use gix::{refs::Target, Repository}; +use issue_show::BackPrefix; +use rocket::{ + get, + response::content::{RawCss, RawHtml}, + State, +}; + +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( + config: &State, + wanted_status: Status, + counter_status: Status, +) -> RawHtml { + let repository = &config.repository; + + 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} +
    +
    +
    + + +"#, + config.source_code_repository_url + )) +} + +#[get("/issues/open")] +pub fn open(config: &State) -> RawHtml { + issue_list_boilerplate(config, Status::Open, Status::Closed) +} +#[get("/issues/closed")] +pub fn closed(config: &State) -> RawHtml { + issue_list_boilerplate(config, Status::Closed, Status::Open) +} + +#[get("/issue/")] +pub fn show_issue(config: &State, prefix: BackPrefix) -> RawHtml { + let repository = config.repository.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(config), + 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