diff options
Diffstat (limited to 'pkgs/by-name/ba/back/src/issues/issue/mod.rs')
-rw-r--r-- | pkgs/by-name/ba/back/src/issues/issue/mod.rs | 328 |
1 files changed, 328 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"><{email}></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"><{email}></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> +"#, + )) + } + } +} |