aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ba/back/src/issues/issue
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-12-23 18:29:35 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-12-23 18:29:35 +0100
commit4bcadcd2244989a8bb7d8dffdaf11f131b96c8ff (patch)
treea081c1c4216f4d1517940c2bc92dc7d8b3e15bb9 /pkgs/by-name/ba/back/src/issues/issue
parentfix(modules/disko): Remove deprecated legacy type and migrate to `by-name` (diff)
downloadnixos-server-4bcadcd2244989a8bb7d8dffdaf11f131b96c8ff.zip
feat(pkgs/back): Init
Other options, for example `git-bug webui --read-only` is just to bugged to be useful.
Diffstat (limited to '')
-rw-r--r--pkgs/by-name/ba/back/src/issues/issue/mod.rs328
-rw-r--r--pkgs/by-name/ba/back/src/issues/issue/raw.rs145
-rw-r--r--pkgs/by-name/ba/back/src/issues/issue_show.rs34
3 files changed, 507 insertions, 0 deletions
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 <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 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<Comment<'a>>,
+ pub status: Status,
+ pub last_status_change: Option<TimeStamp>,
+}
+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: <Vec<Comment>>::default(),
+ status: Status::default(),
+ last_status_change: <Option<TimeStamp>>::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<String> {
+ let comment_list = if self.comments.is_empty() {
+ String::new()
+ } else {
+ format!(
+ r#"
+ <span class="comment-count"> - {} comments</span>
+ "#,
+ self.comments.len()
+ )
+ };
+ let Issue {
+ id,
+ title,
+ message: _,
+ author,
+ timestamp,
+ comments: _,
+ status: _,
+ last_status_change: _,
+ } = self;
+ let Author { name, email } = 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">&lt;{email}&gt;</span> at <span class="timestamp">{timestamp}</span></span>{comment_list} </a>
+ </li>
+"#,
+ ))
+ }
+
+ pub fn to_html(&self) -> RawHtml<String> {
+ let fmt_comments: String = self
+ .comments
+ .iter()
+ .map(|val| {
+ let Comment {
+ id,
+ author,
+ message,
+ timestamp,
+ } = val;
+ let Author { name, email: _ } = 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");
+
+ let maybe_comments = if fmt_comments.is_empty() {
+ String::new()
+ } else {
+ format!(
+ r#"
+ <ol class="issue-history">
+ {fmt_comments}
+ </ol>
+ "#
+ )
+ };
+
+ {
+ 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#"
+<!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">&lt;{email}&gt;</span> at <span class="timestamp">{timestamp}</span></span>
+ </div>
+ {message}
+ {maybe_comments}
+ </main>
+ <footer>
+ <nav>
+ <a href="/issues/open">Open Issues</a>
+ <a href="/issues/closed">Closed Issues</a>
+ </nav>
+ </footer>
+ </div>
+ </body>
+</html>
+"#,
+ ))
+ }
+ }
+}
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 <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::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<Operation>,
+}
+
+#[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<String>, TODO
+ },
+ SetStatus {
+ timestamp: u64,
+ status: Status,
+ },
+ Create {
+ timestamp: u64,
+ title: String,
+ message: String,
+ // files: Option<String>, TODO
+ },
+}
+
+impl From<u64> 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<Value> 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}''"),
+ }
+ }
+}
diff --git a/pkgs/by-name/ba/back/src/issues/issue_show.rs b/pkgs/by-name/ba/back/src/issues/issue_show.rs
new file mode 100644
index 0000000..638840e
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/issues/issue_show.rs
@@ -0,0 +1,34 @@
+// 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;
+
+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 })
+ }
+}