aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ba/back/src/git_bug
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/ba/back/src/git_bug')
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/dag/mod.rs143
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/format/mod.rs123
-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
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/mod.rs28
9 files changed, 888 insertions, 0 deletions
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
new file mode 100644
index 0000000..9c158a7
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs
@@ -0,0 +1,143 @@
+// 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, &current_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
new file mode 100644
index 0000000..4ebf6d4
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/format/mod.rs
@@ -0,0 +1,123 @@
+// 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())
+ }
+}
+
+#[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
+/// & --> &amp;
+/// < --> &lt;
+/// > --> &gt;
+/// " --> &quot;
+/// ' --> &#x27; &apos; is not recommended
+/// / --> &#x2F; 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("&amp;"),
+ '<' => output.push_str("&lt;"),
+ '>' => output.push_str("&gt;"),
+ '"' => output.push_str("&quot;"),
+ '\'' => output.push_str("&#x27;"),
+ '/' => output.push_str("&#x2F;"),
+ _ => 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
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."),
+ }
+ }
+}
diff --git a/pkgs/by-name/ba/back/src/git_bug/mod.rs b/pkgs/by-name/ba/back/src/git_bug/mod.rs
new file mode 100644
index 0000000..c0a5372
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/mod.rs
@@ -0,0 +1,28 @@
+// 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()
+ };
+}