diff options
Diffstat (limited to 'pkgs/by-name/ba/back/src/git_bug/issue')
6 files changed, 594 insertions, 0 deletions
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 new file mode 100644 index 0000000..f2e9af0 --- /dev/null +++ b/pkgs/by-name/ba/back/src/git_bug/issue/entity/mod.rs @@ -0,0 +1,78 @@ +// 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 new file mode 100644 index 0000000..0c2f426 --- /dev/null +++ b/pkgs/by-name/ba/back/src/git_bug/issue/identity/mod.rs @@ -0,0 +1,71 @@ +// 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 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 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 new file mode 100644 index 0000000..a971234 --- /dev/null +++ b/pkgs/by-name/ba/back/src/git_bug/issue/label/mod.rs @@ -0,0 +1,85 @@ +// 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 new file mode 100644 index 0000000..f27bfec --- /dev/null +++ b/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs @@ -0,0 +1,185 @@ +// 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 new file mode 100644 index 0000000..7f861a7 --- /dev/null +++ b/pkgs/by-name/ba/back/src/git_bug/issue/operation/mod.rs @@ -0,0 +1,124 @@ +// 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 new file mode 100644 index 0000000..69d272f --- /dev/null +++ b/pkgs/by-name/ba/back/src/git_bug/issue/operation/operation_type.rs @@ -0,0 +1,51 @@ +// 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."), + } + } +} |