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