summary refs log tree commit diff stats
path: root/pkgs/by-name/ba/back/src/git_bug/issue
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/ba/back/src/git_bug/issue')
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/entity/mod.rs78
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/identity/mod.rs71
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/label/mod.rs85
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/mod.rs185
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/operation/mod.rs124
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/operation/operation_type.rs51
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."),
+        }
+    }
+}