// 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!(), } } } }