diff options
Diffstat (limited to 'pkgs/by-name/ba/back/src')
-rw-r--r-- | pkgs/by-name/ba/back/src/cli.rs | 24 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/config/mod.rs | 72 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/error/mod.rs | 94 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/error/responder.rs | 23 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/git_bug/dag/mod.rs | 143 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/git_bug/format/mod.rs | 144 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/git_bug/issue/entity/mod.rs | 78 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/git_bug/issue/identity/mod.rs | 79 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/git_bug/issue/label/mod.rs | 85 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/git_bug/issue/mod.rs | 185 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/git_bug/issue/operation/mod.rs | 124 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/git_bug/issue/operation/operation_type.rs | 51 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/git_bug/mod.rs | 28 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/main.rs | 52 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/web/issue_html.rs | 166 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/web/mod.rs | 186 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/web/prefix.rs | 35 |
17 files changed, 0 insertions, 1569 deletions
diff --git a/pkgs/by-name/ba/back/src/cli.rs b/pkgs/by-name/ba/back/src/cli.rs deleted file mode 100644 index 79f0d63..0000000 --- a/pkgs/by-name/ba/back/src/cli.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -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 deleted file mode 100644 index 7351ad8..0000000 --- a/pkgs/by-name/ba/back/src/config/mod.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -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 - // `<tags>` of all kinds _should_ be invalid. <2024-12-26> - pub source_code_repository_url: Url, - pub repository: ThreadSafeRepository, - pub root: Url, -} - -#[derive(Deserialize)] -struct RawBackConfig { - source_code_repository_url: Url, - repository_path: PathBuf, - root_url: Url, -} - -impl BackConfig { - pub fn from_config_file(path: &Path) -> error::Result<Self> { - 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<RawBackConfig> for BackConfig { - type Error = error::Error; - - fn try_from(value: RawBackConfig) -> Result<Self, Self::Error> { - 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, - root: value.root_url, - }) - } -} diff --git a/pkgs/by-name/ba/back/src/error/mod.rs b/pkgs/by-name/ba/back/src/error/mod.rs deleted file mode 100644 index 8b71700..0000000 --- a/pkgs/by-name/ba/back/src/error/mod.rs +++ /dev/null @@ -1,94 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use std::{fmt::Display, io, path::PathBuf}; - -use thiserror::Error; - -use crate::web::prefix::BackPrefix; - -pub type Result<T> = std::result::Result<T, Error>; - -pub mod responder; - -#[derive(Error, Debug)] -pub enum Error { - ConfigParse { - file: PathBuf, - error: serde_json::Error, - }, - ConfigRead { - file: PathBuf, - error: io::Error, - }, - RocketLaunch(#[from] rocket::Error), - - RepoOpen { - repository_path: PathBuf, - error: Box<gix::open::Error>, - }, - RepoRefsIter(#[from] gix::refs::packed::buffer::open::Error), - RepoRefsPrefixed(#[from] std::io::Error), - - IssuesPrefixMissing { - prefix: BackPrefix, - }, - IssuesPrefixParse(#[from] gix::hash::prefix::from_hex::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::RocketLaunch(error) => { - write!(f, "while trying to start back: {error}") - } - Error::RepoOpen { - repository_path, - error, - } => { - write!( - f, - "while trying to open the repository ({}): {error}", - repository_path.display() - ) - } - Error::RepoRefsIter(error) => { - write!(f, "while iteration over the refs in a repository: {error}",) - } - Error::RepoRefsPrefixed(error) => { - write!(f, "while prefixing the refs with a path: {error}") - } - Error::IssuesPrefixMissing { prefix } => { - write!( - f, - "There is no 'issue' associated with the prefix: {prefix}" - ) - } - Error::IssuesPrefixParse(error) => { - write!(f, "The given prefix can not be parsed as prefix: {error}") - } - } - } -} diff --git a/pkgs/by-name/ba/back/src/error/responder.rs b/pkgs/by-name/ba/back/src/error/responder.rs deleted file mode 100644 index 7bea961..0000000 --- a/pkgs/by-name/ba/back/src/error/responder.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use rocket::{ - response::{self, Responder, Response}, - Request, -}; - -use super::Error; - -impl<'r> Responder<'r, 'static> for Error { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { - Response::build_from(self.to_string().respond_to(req)?).ok() - } -} diff --git a/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs b/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs deleted file mode 100644 index 9c158a7..0000000 --- a/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs +++ /dev/null @@ -1,143 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use std::path::Path; - -use gix::{bstr::ByteSlice, refs::Target, Commit, Id, ObjectId, Repository}; - -use crate::error; - -use super::issue::{ - entity::{Entity, RawEntity}, - CollapsedIssue, RawCollapsedIssue, -}; - -#[derive(Debug)] -pub struct Dag { - entities: Vec<Entity>, -} - -impl Dag { - pub fn collapse(self) -> CollapsedIssue { - let raw_collapsed_issue = self.entities.into_iter().rev().fold( - RawCollapsedIssue::default(), - |mut collapsed_issue, entity| { - collapsed_issue.append_entity(entity); - collapsed_issue - }, - ); - - CollapsedIssue::from(raw_collapsed_issue) - } - - /// Construct a DAG from the root child upwards. - pub fn construct(repo: &Repository, id: ObjectId) -> Self { - let mut entities = vec![]; - - let base_commit = repo - .find_object(id) - .expect("The object with this id should exist.") - .try_into_commit() - .expect("The git-bug's data model enforces this."); - - entities.push(Self::commit_to_operations(repo, &base_commit)); - - let mut current_commit = base_commit; - while let Some(parent_id) = Self::try_get_parent(repo, ¤t_commit) { - entities.push(Self::commit_to_operations(repo, &parent_id)); - current_commit = parent_id; - } - - Self { - entities: { - entities - .into_iter() - .map(|(raw_entity, id)| Entity::from_raw(repo, raw_entity, id)) - .collect() - }, - } - } - - fn commit_to_operations<'b>(repo: &Repository, id: &Commit<'b>) -> (RawEntity, 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("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 operations = 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"); - - (operations, id.id()) - } - - fn try_get_parent<'a>(repo: &'a Repository, base_commit: &Commit<'a>) -> Option<Commit<'a>> { - let count = base_commit.parent_ids().count(); - - match count { - 0 => None, - 1 => { - let parent = base_commit.parent_ids().last().expect("One does exist"); - - let parent_id = parent.object().expect("The object exists").id; - Some( - repo.find_object(parent_id) - .expect("This is a valid id") - .try_into_commit() - .expect("This should be a commit"), - ) - } - other => { - unreachable!( - "Each commit, used by git-bug should only have one parent, but found: {other}" - ); - } - } - } -} - -pub fn issues_from_repository(repo: &Repository) -> error::Result<Vec<Dag>> { - let dags = repo - .refs - .iter()? - .prefixed(Path::new("refs/bugs/"))? - .map(|val| { - let reference = val.expect("All `git-bug` references in 'refs/bugs' should be objects"); - - if let Target::Object(id) = reference.target { - Dag::construct(repo, id) - } else { - unreachable!("All 'refs/bugs/' should contain a clear target."); - } - }) - .collect::<Vec<Dag>>(); - - Ok(dags) -} diff --git a/pkgs/by-name/ba/back/src/git_bug/format/mod.rs b/pkgs/by-name/ba/back/src/git_bug/format/mod.rs deleted file mode 100644 index b3b6bcc..0000000 --- a/pkgs/by-name/ba/back/src/git_bug/format/mod.rs +++ /dev/null @@ -1,144 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use std::fmt::Display; - -use chrono::DateTime; -use markdown::to_html; -use serde::Deserialize; -use serde_json::Value; - -#[derive(Debug, Default, Clone)] -/// Markdown content. -pub struct MarkDown { - value: String, -} - -impl Display for MarkDown { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(to_html(&self.value).as_str()) - } -} -impl From<&Value> for MarkDown { - fn from(value: &Value) -> Self { - Self { - value: value.as_str().expect("This will exist").to_owned(), - } - } -} - -/// An UNIX time stamp. -/// -/// These should only ever be used for human-display, because timestamps are unreliably in a -/// distributed system. -/// Because of this reason, there is no `value()` function. -#[derive(Debug, Default, Clone, Copy)] -pub struct TimeStamp { - value: u64, -} -impl From<&Value> for TimeStamp { - fn from(value: &Value) -> Self { - Self { - value: value.as_u64().expect("This must exist"), - } - } -} -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()) - } -} - -/// An UNIX time stamp. -/// -/// These should only ever be used for human-display, because timestamps are unreliably in a -/// distributed system. -/// -/// This one allows underlying access to it's value and is only obtainable via `unsafe` code. -/// The reason behind this is, that you might need to access this to improve the display for humans -/// (i.e., sorting by date). -#[derive(Debug, Default, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] -pub struct UnsafeTimeStamp { - value: u64, -} -impl TimeStamp { - /// # Safety - /// This is not really unsafe, but there is just no way your can trust a time stamp in a - /// distributed system. As such access to the raw value could lead to bugs. - pub unsafe fn to_unsafe(self) -> UnsafeTimeStamp { - UnsafeTimeStamp { value: self.value } - } -} - -#[derive(Debug, Default, Deserialize, Clone, PartialEq, Eq)] -/// A string that should be escaped when injected into html content. -pub struct HtmlString { - value: String, -} - -impl From<MarkDown> for HtmlString { - fn from(value: MarkDown) -> Self { - Self { value: value.value } - } -} - -impl From<&Value> for HtmlString { - fn from(value: &Value) -> Self { - Self { - value: value.as_str().expect("This will exist").to_owned(), - } - } -} -impl Display for HtmlString { - 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/git_bug/issue/entity/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/entity/mod.rs deleted file mode 100644 index f2e9af0..0000000 --- a/pkgs/by-name/ba/back/src/git_bug/issue/entity/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use std::fmt::Display; - -use gix::Repository; -use serde::Deserialize; -use serde_json::Value; - -use super::{ - identity::{Author, RawAuthor}, - operation::Operation, -}; - -#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] -#[serde(from = "Value")] -pub struct Id { - value: String, -} -impl From<Value> for Id { - fn from(value: Value) -> Self { - Self::from(&value) - } -} -impl From<&Value> for Id { - fn from(value: &Value) -> Self { - Self { - value: value.as_str().expect("This should be a string").to_owned(), - } - } -} -impl From<gix::Id<'_>> for Id { - fn from(value: gix::Id<'_>) -> Self { - Self { - value: value.shorten().expect("This should work?").to_string(), - } - } -} -impl Display for Id { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.value.fmt(f) - // let shortend = self.value.shorten().expect("This should work."); - // f.write_str(shortend.to_string().as_str()) - } -} - -#[derive(Debug)] -pub struct Entity { - pub id: Id, - pub author: Author, - pub operations: Vec<Operation>, -} - -impl Entity { - pub fn from_raw<'a>(repo: &'a Repository, raw: RawEntity, id: gix::Id<'a>) -> Self { - Self { - id: Id::from(id), - author: Author::construct(repo, raw.author), - operations: raw.operations, - } - } -} - -#[derive(Deserialize)] -pub struct RawEntity { - pub author: RawAuthor, - - #[serde(alias = "ops")] - pub operations: Vec<Operation>, -} diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/identity/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/identity/mod.rs deleted file mode 100644 index bbf483c..0000000 --- a/pkgs/by-name/ba/back/src/git_bug/issue/identity/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use std::fmt::Display; - -use gix::{bstr::ByteSlice, Repository}; -use serde::Deserialize; -use serde_json::Value; - -use crate::{get, git_bug::format::HtmlString}; - -use super::entity::Id; - -#[derive(Debug, Clone)] -pub struct Author { - pub name: HtmlString, - pub email: HtmlString, - pub id: Id, -} - -impl Display for Author { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "({}, {})", self.name, self.email) - } -} - -impl Author { - pub fn construct(repo: &Repository, raw: RawAuthor) -> Self { - let commit_obj = repo - .find_reference(&format!("refs/identities/{}", raw.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: get! {json, "name"}, - email: get! {json, "email"}, - id: raw.id, - } - } -} - -#[derive(Deserialize)] -pub struct RawAuthor { - id: Id, -} diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/label/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/label/mod.rs deleted file mode 100644 index a971234..0000000 --- a/pkgs/by-name/ba/back/src/git_bug/issue/label/mod.rs +++ /dev/null @@ -1,85 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use std::fmt::Display; - -use serde::Deserialize; -use sha2::{Digest, Sha256}; - -use crate::git_bug::format::HtmlString; - -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] -pub struct Label { - value: HtmlString, -} - -impl Display for Label { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.value.fmt(f) - } -} - -impl Label { - /// RGBA from a Label computed in a deterministic way - /// This is taken completely from `git_bug` - pub fn associate_color(&self) -> Color { - // colors from: https://material-ui.com/style/color/ - let colors = vec![ - Color::from_rgba(244, 67, 54, 255), // red - Color::from_rgba(233, 30, 99, 255), // pink - Color::from_rgba(156, 39, 176, 255), // purple - Color::from_rgba(103, 58, 183, 255), // deepPurple - Color::from_rgba(63, 81, 181, 255), // indigo - Color::from_rgba(33, 150, 243, 255), // blue - Color::from_rgba(3, 169, 244, 255), // lightBlue - Color::from_rgba(0, 188, 212, 255), // cyan - Color::from_rgba(0, 150, 136, 255), // teal - Color::from_rgba(76, 175, 80, 255), // green - Color::from_rgba(139, 195, 74, 255), // lightGreen - Color::from_rgba(205, 220, 57, 255), // lime - Color::from_rgba(255, 235, 59, 255), // yellow - Color::from_rgba(255, 193, 7, 255), // amber - Color::from_rgba(255, 152, 0, 255), // orange - Color::from_rgba(255, 87, 34, 255), // deepOrange - Color::from_rgba(121, 85, 72, 255), // brown - Color::from_rgba(158, 158, 158, 255), // grey - Color::from_rgba(96, 125, 139, 255), // blueGrey - ]; - - let hash = Sha256::digest(self.to_string().as_bytes()); - - let id: usize = hash - .into_iter() - .map(|val| val as usize) - .fold(0, |acc, val| (acc + val) % colors.len()); - - colors[id] - } -} - -#[derive(Default, Clone, Copy, Debug)] -pub struct Color { - pub red: u32, - pub green: u32, - pub blue: u32, - pub alpha: u32, -} - -impl Color { - pub fn from_rgba(red: u32, green: u32, blue: u32, alpha: u32) -> Self { - Self { - red, - green, - blue, - alpha, - } - } -} diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs deleted file mode 100644 index f27bfec..0000000 --- a/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs +++ /dev/null @@ -1,185 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use std::fmt::Display; - -use entity::{Entity, Id}; -use identity::Author; -use label::Label; -use operation::Operation; -use serde_json::Value; - -use super::format::{MarkDown, TimeStamp}; - -pub mod entity; -pub mod identity; -pub mod label; -pub mod operation; - -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub enum Status { - Open, - Closed, -} -impl From<&Value> for Status { - fn from(value: &Value) -> Self { - match value.as_u64().expect("This should be a integer") { - 1 => Self::Open, - 2 => Self::Closed, - other => unimplemented!("Invalid status string: '{other}'"), - } - } -} -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 CollapsedIssue { - pub id: Id, - pub author: Author, - pub timestamp: TimeStamp, - pub title: MarkDown, - pub message: MarkDown, - pub comments: Vec<Comment>, - pub status: Status, - pub last_status_change: TimeStamp, - pub labels: Vec<Label>, -} -impl From<RawCollapsedIssue> for CollapsedIssue { - fn from(r: RawCollapsedIssue) -> Self { - macro_rules! get { - ($name:ident) => { - r.$name.expect(concat!( - "'", - stringify!($name), - "' is unset, when trying to collapes an issue! (This is likely a bug)" - )) - }; - } - - Self { - id: get! {id}, - author: get! {author}, - timestamp: get! {timestamp}, - title: get! {title}, - message: get! {message}, - comments: r.comments, - status: get! {status}, - last_status_change: get! {last_status_change}, - labels: r.labels, - } - } -} - -#[derive(Debug)] -pub struct Comment { - pub id: Id, - pub author: Author, - pub timestamp: TimeStamp, - pub message: MarkDown, -} - -#[derive(Debug, Default)] -pub struct RawCollapsedIssue { - pub id: Option<Id>, - pub author: Option<Author>, - pub timestamp: Option<TimeStamp>, - pub title: Option<MarkDown>, - pub message: Option<MarkDown>, - pub status: Option<Status>, - pub last_status_change: Option<TimeStamp>, - - // NOTE(@bpeetz): These values set here already, because an issue without these - // would be perfectly valid. <2024-12-26> - pub labels: Vec<Label>, - pub comments: Vec<Comment>, -} - -impl RawCollapsedIssue { - pub fn append_entity(&mut self, entity: Entity) { - for op in entity.operations { - match op { - Operation::AddComment { timestamp, message } => { - self.comments.push(Comment { - id: entity.id.clone(), - author: entity.author.clone(), - timestamp, - message, - }); - } - Operation::Create { - timestamp, - title, - message, - } => { - self.id = Some(entity.id.clone()); - self.author = Some(entity.author.clone()); - self.timestamp = Some(timestamp.clone()); - self.title = Some(title); - self.message = Some(message); - self.status = Some(Status::Open); // This is the default in git_bug - self.last_status_change = Some(timestamp); - } - Operation::EditComment { - timestamp, - target, - message, - } => { - let comments = &mut self.comments; - - let target_comment = comments - .iter_mut() - .find(|comment| comment.id == target) - .expect("The target must be a valid comment"); - - // TODO(@bpeetz): We should probably set a `edited = true` flag here. <2024-12-26> - // TODO(@bpeetz): Should we also change the author? <2024-12-26> - - target_comment.timestamp = timestamp; - target_comment.message = message; - } - Operation::LabelChange { - timestamp: _, - added, - removed, - } => { - let labels = self.labels.clone(); - - self.labels = labels - .into_iter() - .filter(|val| !removed.contains(val)) - .chain(added.into_iter()) - .collect(); - } - Operation::SetStatus { timestamp, status } => { - self.status = Some(status); - self.last_status_change = Some(timestamp); - } - Operation::SetTitle { - timestamp: _, - title, - was: _, - } => { - self.title = Some(title); - } - - Operation::NoOp {} => unimplemented!(), - Operation::SetMetadata {} => unimplemented!(), - } - } - } -} diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/operation/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/operation/mod.rs deleted file mode 100644 index 7f861a7..0000000 --- a/pkgs/by-name/ba/back/src/git_bug/issue/operation/mod.rs +++ /dev/null @@ -1,124 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use std::convert::Infallible; - -use operation_type::OperationType; -use serde::Deserialize; -use serde_json::Value; - -use crate::{ - get, - git_bug::format::{MarkDown, TimeStamp}, -}; - -use super::{entity, label::Label, Status}; - -pub mod operation_type; - -#[derive(Deserialize, Debug)] -#[serde(try_from = "Value")] -pub enum Operation { - AddComment { - timestamp: TimeStamp, - message: MarkDown, - }, - Create { - timestamp: TimeStamp, - title: MarkDown, - message: MarkDown, - }, - EditComment { - timestamp: TimeStamp, - target: entity::Id, - message: MarkDown, - }, - LabelChange { - timestamp: TimeStamp, - added: Vec<Label>, - removed: Vec<Label>, - }, - SetStatus { - timestamp: TimeStamp, - status: Status, - }, - SetTitle { - timestamp: TimeStamp, - title: MarkDown, - was: MarkDown, - }, - - // These seem to be just weird non-operation, operations. - // defined in: git-bug/entities/bug/operation.go - NoOp {}, - SetMetadata {}, -} - -impl TryFrom<Value> for Operation { - type Error = Infallible; - - fn try_from(value: Value) -> Result<Self, Self::Error> { - let operation_type = OperationType::from_json_int( - value - .get("type") - .expect("Should exist") - .as_u64() - .expect("This should work"), - ); - - let op = match operation_type { - OperationType::AddComment => Self::AddComment { - timestamp: get! {value, "timestamp" }, - message: get! {value, "message"}, - }, - OperationType::Create => Self::Create { - timestamp: get! {value, "timestamp"}, - title: get! {value, "title"}, - message: get! {value, "message"}, - }, - OperationType::EditComment => Self::EditComment { - timestamp: get! {value, "timestamp"}, - target: get! {value, "target"}, - message: get! {value, "message"}, - }, - OperationType::LabelChange => Self::LabelChange { - timestamp: get! {value, "timestamp"}, - added: serde_json::from_value( - value - .get("added") - .expect("This should be available") - .to_owned(), - ) - .expect("This should be parsable"), - removed: serde_json::from_value( - value - .get("removed") - .expect("This should be available") - .to_owned(), - ) - .expect("This should be parsable"), - }, - OperationType::SetStatus => Self::SetStatus { - timestamp: get! {value, "timestamp"}, - status: get! {value, "status"}, - }, - OperationType::SetTitle => Self::SetTitle { - timestamp: get! {value, "timestamp"}, - title: get! {value, "title"}, - was: get! {value, "was"}, - }, - OperationType::NoOp => Self::NoOp {}, - OperationType::SetMetadata => Self::SetMetadata {}, - }; - - Ok(op) - } -} diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/operation/operation_type.rs b/pkgs/by-name/ba/back/src/git_bug/issue/operation/operation_type.rs deleted file mode 100644 index 69d272f..0000000 --- a/pkgs/by-name/ba/back/src/git_bug/issue/operation/operation_type.rs +++ /dev/null @@ -1,51 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -pub enum OperationType { - AddComment, - Create, - EditComment, - LabelChange, - NoOp, - SetMetadata, - SetStatus, - SetTitle, -} - -impl OperationType { - // NOTE(@bpeetz): This mapping should always be the same as `git_bug`'s. - // The mapping is defined in `git-bug/entities/bug/operation.go`. <2024-12-26> - pub fn to_json_int(self) -> u64 { - match self { - OperationType::Create => 1, - OperationType::SetTitle => 2, - OperationType::AddComment => 3, - OperationType::SetStatus => 4, - OperationType::LabelChange => 5, - OperationType::EditComment => 6, - OperationType::NoOp => 7, - OperationType::SetMetadata => 8, - } - } - pub fn from_json_int(value: u64) -> Self { - match value { - 1 => OperationType::Create, - 2 => OperationType::SetTitle, - 3 => OperationType::AddComment, - 4 => OperationType::SetStatus, - 5 => OperationType::LabelChange, - 6 => OperationType::EditComment, - 7 => OperationType::NoOp, - 8 => OperationType::SetMetadata, - other => unimplemented!("The operation type {other} is not recognized."), - } - } -} diff --git a/pkgs/by-name/ba/back/src/git_bug/mod.rs b/pkgs/by-name/ba/back/src/git_bug/mod.rs deleted file mode 100644 index c0a5372..0000000 --- a/pkgs/by-name/ba/back/src/git_bug/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -pub mod dag; -pub mod format; -pub mod issue; - -#[macro_export] -macro_rules! get { - ($value:expr, $name:expr) => { - $value - .get($name) - .expect(concat!( - "Expected field ", - stringify!($name), - "to exists, but was missing." - )) - .into() - }; -} diff --git a/pkgs/by-name/ba/back/src/main.rs b/pkgs/by-name/ba/back/src/main.rs deleted file mode 100644 index 961c39b..0000000 --- a/pkgs/by-name/ba/back/src/main.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use std::process; - -use clap::Parser; -use config::BackConfig; -use rocket::routes; -use web::feed; - -use crate::web::{closed, open, show_issue, styles}; - -mod cli; -pub mod config; -mod error; -pub mod git_bug; -mod web; - -fn main() -> Result<(), String> { - if let Err(err) = rocket_main() { - eprintln!("Error {err}"); - process::exit(1); - } else { - Ok(()) - } -} - -#[rocket::main] -async fn rocket_main() -> Result<(), error::Error> { - let args = cli::Cli::parse(); - - let config = BackConfig::from_config_file(&args.config_file)?; - - rocket::build() - .mount("/", routes![open, closed, show_issue, styles, feed]) - .manage(config) - .ignite() - .await - .expect("This error should only happen on a miss-configuration.") - .launch() - .await?; - - Ok(()) -} diff --git a/pkgs/by-name/ba/back/src/web/issue_html.rs b/pkgs/by-name/ba/back/src/web/issue_html.rs deleted file mode 100644 index 45c0281..0000000 --- a/pkgs/by-name/ba/back/src/web/issue_html.rs +++ /dev/null @@ -1,166 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use rocket::response::content::RawHtml; - -use crate::{ - config::BackConfig, - git_bug::{ - format::HtmlString, - issue::{identity::Author, CollapsedIssue, Comment}, - }, -}; - -impl CollapsedIssue { - pub fn to_list_entry(&self) -> RawHtml<String> { - let comment_list = if self.comments.is_empty() { - String::new() - } else { - let comments_string = if self.comments.len() > 1 { - "comments" - } else { - "comment" - }; - - format!( - r#" - <span class="comment-count"> - {} {}</span> - "#, - self.comments.len(), - comments_string - ) - }; - - let CollapsedIssue { - id, - title, - message: _, - author, - timestamp, - comments: _, - status: _, - last_status_change: _, - labels: _, - } = self; - - let Author { name, email, id: _ } = author; - - RawHtml(format!( - r#" - <li> - <a href="/issue/{id}"> - <p> - <span class="issue-subject">{title}</span> - </p> - <span class="issue-number">{id}</span> - <span class="created-by-at">Opened by <span class="user-name">{name}</span> <span class="user-email"><{email}></span> at <span class="timestamp">{timestamp}</span></span>{comment_list} </a> - </li> -"#, - )) - } - - pub fn to_html(&self, config: &BackConfig) -> RawHtml<String> { - let comments = if self.comments.is_empty() { - String::new() - } else { - let fmt_comments: String = self - .comments - .iter() - .map(|val| { - let Comment { - id, - author, - message, - timestamp, - } = val; - let Author { - name, - email: _, - id: _, - } = author; - - format!( - r#" - <li class="comment" id="{id}"> - {message} - <p class="comment-info"><span class="user-name">{name} at {timestamp}</span></p> - </li> - "#, - ) - }) - .collect::<Vec<String>>() - .join("\n"); - - format!( - r#" - <ol class="issue-history"> - {fmt_comments} - </ol> - "# - ) - }; - - { - let CollapsedIssue { - id, - title, - message, - author, - timestamp, - comments: _, - status: _, - last_status_change: _, - labels: _, - } = self; - let Author { name, email, id: _ } = author; - let html_title = HtmlString::from(title.clone()); - - RawHtml(format!( - r#" -<!DOCTYPE html> -<html lang="en"> - <head> - <title>{html_title} | Back</title> - <link href="/style.css" rel="stylesheet" type="text/css"> - <meta content="width=device-width,initial-scale=1" name="viewport"> - </head> - <body> - <div class="content"> - <nav> - <a href="/issues/open">Open Issues</a> - <a href="/issues/closed">Closed Issues</a> - </nav> - <header> - <h1>{title}</h1> - <div class="issue-number">{id}</div> - </header> - <main> - <div class="issue-info"> - <span class="created-by-at">Opened by <span class="user-name">{name}</span> <span class="user-email"><{email}></span> at <span class="timestamp">{timestamp}</span></span> - </div> - {message} - {comments} - </main> - <footer> - <nav> - <a href="/issues/open">Open Issues</a> - <a href="{}">Source code</a> - <a href="/issues/closed">Closed Issues</a> - </nav> - </footer> - </div> - </body> -</html> -"#, - config.source_code_repository_url - )) - } - } -} diff --git a/pkgs/by-name/ba/back/src/web/mod.rs b/pkgs/by-name/ba/back/src/web/mod.rs deleted file mode 100644 index f7a4077..0000000 --- a/pkgs/by-name/ba/back/src/web/mod.rs +++ /dev/null @@ -1,186 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use crate::{ - config::BackConfig, - error::{self, Error}, - git_bug::{ - dag::issues_from_repository, - issue::{CollapsedIssue, Status}, - }, -}; -use prefix::BackPrefix; -use rocket::{ - get, - response::content::{RawCss, RawHtml}, - State, -}; - -mod issue_html; -pub mod prefix; - -#[get("/style.css")] -pub fn styles() -> RawCss<String> { - RawCss(include_str!("../../assets/style.css").to_owned()) -} - -pub fn issue_list_boilerplate( - config: &State<BackConfig>, - wanted_status: Status, - counter_status: Status, -) -> error::Result<RawHtml<String>> { - let repository = &config.repository; - - let mut issue_list = issues_from_repository(&repository.to_thread_local())? - .into_iter() - .map(|issue| issue.collapse()) - .collect::<Vec<CollapsedIssue>>(); - - // Sort by date descending. - issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() }); - issue_list.reverse(); - - let issue_list_str = issue_list.into_iter().fold(String::new(), |acc, issue| { - format!("{}{}", acc, { - if issue.status == wanted_status { - let issue_entry = issue.to_list_entry(); - issue_entry.0 - } else { - String::new() - } - }) - }); - - let counter_status_lower = counter_status.to_string().to_lowercase(); - Ok(RawHtml(format!( - r#" - <!DOCTYPE html> - <html lang="en"> - <head> - <title>Back</title> - <link href="/style.css" rel="stylesheet" type="text/css"> - <meta content="width=device-width,initial-scale=1" name="viewport"> - </head> - <body> - <div class="content"> - <header> - <h1>{wanted_status} Issues</h1> - </header> - <main> - <div class="issue-links"> - <a href="/issues/{counter_status_lower}/">View {counter_status} issues</a> - <a href="{}">Source code</a> - <!-- - <form class="issue-search" method="get"> - <input name="search" title="Issue search query" type="search"> - <input class="sr-only" type="submit" value="Search Issues"> - </form> - --> - </div> - <ol class="issue-list"> - {issue_list_str} - </ol> - </main> - </div> - </body> - </html> - "#, - config.source_code_repository_url - ))) -} - -#[get("/issues/open")] -pub fn open(config: &State<BackConfig>) -> error::Result<RawHtml<String>> { - issue_list_boilerplate(config, Status::Open, Status::Closed) -} -#[get("/issues/closed")] -pub fn closed(config: &State<BackConfig>) -> error::Result<RawHtml<String>> { - issue_list_boilerplate(config, Status::Closed, Status::Open) -} - -#[get("/issues/feed")] -pub fn feed(config: &State<BackConfig>) -> error::Result<RawHtml<String>> { - use rss::{ChannelBuilder, Item, ItemBuilder}; - - //Collect all Items as rss items - let mut items: Vec<Item> = issues_from_repository(&config.repository.to_thread_local())? - .into_iter() - .map(|issue| issue.collapse()) - .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/{}", &config.root.to_string(), issue.id)) - .build() - }) - .collect(); - //Append all comments after converting them to rss items - items.extend( - issues_from_repository(&config.repository.to_thread_local())? - .into_iter() - .map(|issue| issue.collapse()) - .filter(|issue| issue.comments.len() > 0) - .map(|issue| { - issue - .comments - .into_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/{}", &config.root.to_string(), issue.id)) - .build() - }) - .collect::<Vec<Item>>() - }) - .flatten() - .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(RawHtml(channel.to_string())) -} - -#[get("/issue/<prefix>")] -pub fn show_issue( - config: &State<BackConfig>, - prefix: Result<BackPrefix, gix::hash::prefix::from_hex::Error>, -) -> error::Result<RawHtml<String>> { - // NOTE(@bpeetz): Explicitly unwrap the `prefix` here (instead of taking the unwrapped value as - // argument), to avoid triggering rockets "errors forward to the next route" feature. - // This ensures, that our error message actually reaches the user. <2024-12-26> - let prefix = prefix?; - - let repository = config.repository.to_thread_local(); - - let all_issues: Vec<CollapsedIssue> = issues_from_repository(&repository)? - .into_iter() - .map(|val| val.collapse()) - .collect(); - - let maybe_issue = all_issues - .iter() - .find(|issue| issue.id.to_string().starts_with(&prefix.to_string())); - - match maybe_issue { - Some(issue) => Ok(issue.to_html(config)), - None => Err(Error::IssuesPrefixMissing { prefix }), - } -} diff --git a/pkgs/by-name/ba/back/src/web/prefix.rs b/pkgs/by-name/ba/back/src/web/prefix.rs deleted file mode 100644 index 5143799..0000000 --- a/pkgs/by-name/ba/back/src/web/prefix.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Back - An extremely simple git issue tracking system. Inspired by tvix's -// panettone -// -// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// 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 <https://www.gnu.org/licenses/agpl.txt>. - -use std::fmt::Display; - -use gix::hash::Prefix; -use rocket::request::FromParam; - -#[derive(Debug)] -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<Self, Self::Error> { - let prefix = Prefix::from_hex(param)?; - - Ok(Self { prefix }) - } -} |