From 4bcadcd2244989a8bb7d8dffdaf11f131b96c8ff Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Mon, 23 Dec 2024 18:29:35 +0100 Subject: feat(pkgs/back): Init Other options, for example `git-bug webui --read-only` is just to bugged to be useful. --- pkgs/by-name/ba/back/src/issues/issue/mod.rs | 328 +++++++++++++++++++++++++++ pkgs/by-name/ba/back/src/issues/issue/raw.rs | 145 ++++++++++++ 2 files changed, 473 insertions(+) create mode 100644 pkgs/by-name/ba/back/src/issues/issue/mod.rs create mode 100644 pkgs/by-name/ba/back/src/issues/issue/raw.rs (limited to 'pkgs/by-name/ba/back/src/issues/issue') diff --git a/pkgs/by-name/ba/back/src/issues/issue/mod.rs b/pkgs/by-name/ba/back/src/issues/issue/mod.rs new file mode 100644 index 0000000..ada7593 --- /dev/null +++ b/pkgs/by-name/ba/back/src/issues/issue/mod.rs @@ -0,0 +1,328 @@ +// Back - An extremely simple git issue tracking system. Inspired by tvix's +// panettone +// +// Copyright (C) 2024 Benedikt Peetz +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This file is part of Back. +// +// You should have received a copy of the License along with this program. +// If not, see . + +use std::fmt::Display; + +use chrono::DateTime; +use gix::{bstr::ByteSlice, Commit, Id, ObjectId, Repository}; +use raw::{Operation, RawIssue}; +use rocket::response::content::RawHtml; + +use super::format::{BackString, Markdown}; + +mod raw; + +#[derive(Debug, Default)] +pub struct TimeStamp { + value: u64, +} +impl TimeStamp { + pub fn new(val: u64) -> Self { + Self { value: val } + } +} +impl Display for TimeStamp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let date = + DateTime::from_timestamp(self.value as i64, 0).expect("This timestamp should be vaild"); + + let newdate = date.format("%Y-%m-%d %H:%M:%S"); + f.write_str(newdate.to_string().as_str()) + } +} + +#[derive(Debug)] +pub struct Comment<'a> { + pub id: Id<'a>, + pub author: Author, + pub message: Markdown, + pub timestamp: TimeStamp, +} + +#[derive(Debug, Default)] +pub struct Author { + name: BackString, + email: BackString, +} + +#[derive(Debug)] +pub struct IssueId<'a> { + value: Id<'a>, +} +impl<'a> IssueId<'a> { + pub fn new(id: Id<'a>) -> Self { + Self { value: id } + } +} +impl Display for IssueId<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let shortend = self.value.shorten().expect("This should work."); + f.write_str(shortend.to_string().as_str()) + } +} + +#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] +pub enum Status { + #[default] + Open, + Closed, +} +impl Display for Status { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Status::Open => f.write_str("Open"), + Status::Closed => f.write_str("Closed"), + } + } +} + +#[derive(Debug)] +pub struct Issue<'a> { + pub id: IssueId<'a>, + pub author: Author, + pub timestamp: TimeStamp, + pub title: Markdown, + pub message: Markdown, + pub comments: Vec>, + pub status: Status, + pub last_status_change: Option, +} +impl<'a> Issue<'a> { + pub fn default_with_id(id: Id<'a>) -> Self { + Self { + id: IssueId::new(id), + author: Author::default(), + timestamp: TimeStamp::default(), + title: Markdown::default(), + message: Markdown::default(), + comments: >::default(), + status: Status::default(), + last_status_change: >::default(), + } + } + + pub fn from_commit_id(repo: &'a Repository, commit_id: ObjectId) -> Self { + fn unwrap_id<'b>(repo: &Repository, id: &Commit<'b>) -> (RawIssue, Id<'b>) { + let tree_obj = repo + .find_object(id.tree_id().unwrap()) + .expect("The object with this id should exist.") + .try_into_tree() + .expect("The git-bug's data model enforces this."); + + let ops_ref = tree_obj.find_entry("ops").unwrap(); + + let issue_data = repo + .find_object(ops_ref.object_id()) + .expect("The object with this id should exist.") + .try_into_blob() + .expect("The git-bug's data model enforces this.") + .data + .clone(); + + let raw_issue = serde_json::from_str( + issue_data + .to_str() + .expect("git-bug's ensures, that this is valid json."), + ) + .expect("The returned json should be valid"); + + (raw_issue, id.id()) + } + + let commit_obj = repo + .find_object(commit_id) + .expect("The object with this id should exist.") + .try_into_commit() + .expect("The git-bug's data model enforces this."); + + let mut issues = vec![unwrap_id(repo, &commit_obj)]; + + let mut current_commit_obj = commit_obj; + while current_commit_obj.parent_ids().count() != 0 { + assert_eq!( + current_commit_obj.parent_ids().count(), + 1, + "There should be only one parent" + ); + let parent = current_commit_obj + .parent_ids() + .last() + .expect("One does exist"); + + let parent_id = parent.object().expect("The object exists").id; + let parent_commit = repo + .find_object(parent_id) + .expect("This is a valid id") + .try_into_commit() + .expect("This should be a commit"); + + issues.push(unwrap_id(repo, &parent_commit)); + current_commit_obj = parent_commit; + } + + let mut final_issue = Self::default_with_id(current_commit_obj.id()); + for (issue, id) in issues { + for op in issue.operations { + match op { + Operation::AddComment { timestamp, message } => { + final_issue.comments.push(Comment { + id, + author: issue.author.load_identity(repo), + message: Markdown::from(message), + timestamp: TimeStamp::new(timestamp), + }) + } + Operation::Create { + timestamp, + title, + message, + } => { + final_issue.author = issue.author.load_identity(repo); + final_issue.title = Markdown::from(title); + final_issue.message = Markdown::from(message); + final_issue.timestamp = TimeStamp::new(timestamp); + } + Operation::SetStatus { timestamp, status } => { + final_issue.status = status; + final_issue.last_status_change = Some(TimeStamp::new(timestamp)); + } + } + } + } + final_issue + } + + pub fn to_list_entry(&self) -> RawHtml { + let comment_list = if self.comments.is_empty() { + String::new() + } else { + format!( + r#" + - {} comments + "#, + self.comments.len() + ) + }; + let Issue { + id, + title, + message: _, + author, + timestamp, + comments: _, + status: _, + last_status_change: _, + } = self; + let Author { name, email } = author; + RawHtml(format!( + r#" +
  • + +

    + {title} +

    + {id} - Opened by {name} <{email}> at {timestamp}{comment_list}
    +
  • +"#, + )) + } + + pub fn to_html(&self) -> RawHtml { + let fmt_comments: String = self + .comments + .iter() + .map(|val| { + let Comment { + id, + author, + message, + timestamp, + } = val; + let Author { name, email: _ } = author; + + format!( + r#" +
  • + {message} +

    {name} at {timestamp}

    +
  • + "#, + ) + }) + .collect::>() + .join("\n"); + + let maybe_comments = if fmt_comments.is_empty() { + String::new() + } else { + format!( + r#" +
      + {fmt_comments} +
    + "# + ) + }; + + { + let Issue { + id, + title, + message, + author, + timestamp, + comments: _, + status: _, + last_status_change: _, + } = self; + let Author { name, email } = author; + let html_title = BackString::from(title.clone()); + + RawHtml(format!( + r#" + + + + {html_title} | Back + + + + +
    + +
    +

    {title}

    +
    {id}
    +
    +
    +
    + Opened by {name} <{email}> at {timestamp} +
    + {message} + {maybe_comments} +
    + +
    + + +"#, + )) + } + } +} diff --git a/pkgs/by-name/ba/back/src/issues/issue/raw.rs b/pkgs/by-name/ba/back/src/issues/issue/raw.rs new file mode 100644 index 0000000..48d2a9f --- /dev/null +++ b/pkgs/by-name/ba/back/src/issues/issue/raw.rs @@ -0,0 +1,145 @@ +// Back - An extremely simple git issue tracking system. Inspired by tvix's +// panettone +// +// Copyright (C) 2024 Benedikt Peetz +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This file is part of Back. +// +// You should have received a copy of the License along with this program. +// If not, see . + +use gix::{bstr::ByteSlice, Repository}; +use serde::Deserialize; +use serde_json::Value; + +use crate::issues::format::BackString; + +use super::{Author, Status}; + +macro_rules! get { + ($value:expr, $name:expr, $type_fun:ident) => { + $value + .get($name) + .expect(concat!( + "Expected field ", + stringify!($name), + "to exists, but was missing." + )) + .$type_fun() + .expect(concat!( + "Failed to interpret field ", + stringify!($name), + " as ", + stringify!($type), + "!" + )) + }; +} + +#[derive(Deserialize)] +pub(super) struct RawIssue { + pub(super) author: RawAuthor, + + #[serde(alias = "ops")] + pub(super) operations: Vec, +} + +#[derive(Deserialize, Clone, Default, Debug)] +pub(super) struct RawAuthor { + id: String, +} + +impl RawAuthor { + pub fn load_identity(&self, repo: &Repository) -> Author { + let commit_obj = repo + .find_reference(&format!("refs/identities/{}", self.id)) + .expect("All authors should also have identities") + .peel_to_commit() + .expect("All identities should be commits"); + let tree_obj = repo + .find_tree( + commit_obj + .tree() + .expect("The commit should have an tree associated with it") + .id, + ) + .expect("This should be a tree"); + let data = repo + .find_blob( + tree_obj + .find_entry("version") + .expect("This entry should exist") + .object() + .expect("This should point to a blob entry") + .id, + ) + .expect("This blob should exist") + .data + .clone(); + + let json: Value = serde_json::from_str(data.to_str().expect("This is encoded json")) + .expect("This is valid json"); + + Author { + name: BackString::from(get! {json, "name", as_str}.to_owned()), + email: BackString::from(get! {json, "email", as_str}.to_owned()), + } + } +} + +#[derive(Deserialize)] +#[serde(from = "Value")] +pub(super) enum Operation { + AddComment { + timestamp: u64, + message: String, + // files: Option, TODO + }, + SetStatus { + timestamp: u64, + status: Status, + }, + Create { + timestamp: u64, + title: String, + message: String, + // files: Option, TODO + }, +} + +impl From for Status { + fn from(value: u64) -> Self { + match value { + 1 => Status::Open, + 2 => Status::Closed, + other => todo!("The status ({other}) is not yet implemented."), + } + } +} + +impl From for Operation { + fn from(value: Value) -> Self { + match value + .get("type") + .expect("Should exist") + .as_u64() + .expect("This should work") + { + 1 => Self::Create { + title: get! {value, "title", as_str}.to_owned(), + message: get! {value, "message", as_str}.to_owned(), + timestamp: get! {value, "timestamp", as_u64}, + }, + 3 => Self::AddComment { + message: get! {value, "message", as_str}.to_owned(), + timestamp: get! {value, "timestamp", as_u64}, + }, + 4 => Self::SetStatus { + status: Status::from(get! {value, "status", as_u64}), + timestamp: get! {value, "timestamp", as_u64}, + }, + other => todo!("The type ({other}) is not yet added as a a valid operation. It's value is: '{value}''"), + } + } +} -- cgit 1.4.1