use std::collections::{HashMap, HashSet};
use crate::{Comment, CommentIdentity, Issue};
#[allow(async_fn_in_trait)]
pub trait Sink<S> {
type Error: std::fmt::Debug + std::fmt::Display;
async fn sink(&mut self, old: Option<&Issue>) -> Result<bool, Self::Error>;
}
#[derive(Clone, Debug, Default)]
pub struct IssueDiff {
pub body_changed: bool,
pub state_changed: bool,
pub title_changed: bool,
pub labels_changed: bool,
pub comments_to_create: Vec<Comment>,
pub comments_to_update: Vec<(u64, Comment)>,
pub comments_to_delete: Vec<u64>,
pub children_to_create: Vec<Issue>,
pub children_to_delete: Vec<u64>,
}
impl IssueDiff {
#[cfg(test)]
pub fn has_changes(&self) -> bool {
self.body_changed
|| self.state_changed
|| self.title_changed
|| self.labels_changed
|| !self.comments_to_create.is_empty()
|| !self.comments_to_update.is_empty()
|| !self.comments_to_delete.is_empty()
|| !self.children_to_create.is_empty()
|| !self.children_to_delete.is_empty()
}
}
pub fn compute_node_diff(new: &Issue, old: Option<&Issue>) -> IssueDiff {
let mut diff = IssueDiff::default();
let Some(old) = old else {
for comment in new.contents.comments.iter().skip(1) {
if comment.is_pending() {
diff.comments_to_create.push(comment.clone());
}
}
for child in new.children.values() {
if child.is_local() {
diff.children_to_create.push(child.clone());
}
}
return diff;
};
let new_body = new.contents.comments.description();
let old_body = old.contents.comments.description();
diff.body_changed = new_body != old_body;
diff.state_changed = new.contents.state != old.contents.state;
diff.title_changed = new.contents.title != old.contents.title;
diff.labels_changed = new.contents.labels != old.contents.labels;
let old_comments: HashMap<u64, &Comment> = old.contents.comments.iter().skip(1).filter_map(|c| c.id().map(|id| (id, c))).collect();
let new_comment_ids: HashSet<u64> = new.contents.comments.iter().skip(1).filter_map(|c| c.id()).collect();
for comment in new.contents.comments.iter().skip(1) {
match &comment.identity {
CommentIdentity::Pending | CommentIdentity::Body => {
if !comment.body.is_empty() {
diff.comments_to_create.push(comment.clone());
}
}
CommentIdentity::Created { id, .. } => {
if let Some(old_comment) = old_comments.get(id) {
if comment.body.to_string() != old_comment.body.to_string() {
diff.comments_to_update.push((*id, comment.clone()));
}
}
}
}
}
for id in old_comments.keys() {
if !new_comment_ids.contains(id) {
diff.comments_to_delete.push(*id);
}
}
for child in new.children.values() {
if child.is_local() {
diff.children_to_create.push(child.clone());
}
}
for (selector, old_child) in &old.children {
if !new.children.contains_key(selector)
&& let Some(num) = old_child.git_id()
{
diff.children_to_delete.push(num);
}
}
diff
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BlockerSequence, CloseState, Comments, Events, IssueContents, IssueIdentity, IssueIndex, IssueLink, IssueTimestamps, RepoInfo};
fn make_issue(title: &str, number: Option<u64>) -> Issue {
let parent_index = IssueIndex::repo_only(RepoInfo::new("o", "r"));
let identity = match number {
Some(n) => {
let link = IssueLink::parse(&format!("https://github.com/o/r/issues/{n}")).unwrap();
IssueIdentity::new_linked(Some(parent_index), Some("testuser".to_string()), link, IssueTimestamps::default())
}
None => IssueIdentity::pending(parent_index),
};
Issue {
identity,
contents: IssueContents {
title: title.to_string(),
labels: vec![],
state: CloseState::Open,
comments: Comments::new(vec![Comment {
identity: CommentIdentity::Body,
body: Events::parse("body"),
}]),
blockers: BlockerSequence::default(),
},
children: HashMap::default(),
}
}
#[test]
fn test_compute_node_diff_no_changes() {
let issue = make_issue("Root", Some(1));
let diff = compute_node_diff(&issue, Some(&issue));
assert!(!diff.has_changes());
}
#[test]
fn test_compute_node_diff_body_changed() {
let old = make_issue("Root", Some(1));
let mut new = make_issue("Root", Some(1));
new.contents.comments[0].body = Events::parse("new body");
let diff = compute_node_diff(&new, Some(&old));
assert!(diff.body_changed);
insta::assert_debug_snapshot!(diff, @"
IssueDiff {
body_changed: true,
state_changed: false,
title_changed: false,
labels_changed: false,
comments_to_create: [],
comments_to_update: [],
comments_to_delete: [],
children_to_create: [],
children_to_delete: [],
}
");
}
#[test]
fn test_compute_node_diff_state_changed() {
let old = make_issue("Root", Some(1));
let mut new = make_issue("Root", Some(1));
new.contents.state = CloseState::Closed;
let diff = compute_node_diff(&new, Some(&old));
assert!(diff.state_changed);
insta::assert_debug_snapshot!(diff, @"
IssueDiff {
body_changed: false,
state_changed: true,
title_changed: false,
labels_changed: false,
comments_to_create: [],
comments_to_update: [],
comments_to_delete: [],
children_to_create: [],
children_to_delete: [],
}
");
}
#[test]
fn test_compute_node_diff_pending_comment() {
let old = make_issue("Root", Some(1));
let mut new = make_issue("Root", Some(1));
new.contents.comments.push(Comment {
identity: CommentIdentity::Pending,
body: Events::parse("new comment"),
});
let diff = compute_node_diff(&new, Some(&old));
insta::assert_debug_snapshot!(diff, @r#"
IssueDiff {
body_changed: false,
state_changed: false,
title_changed: false,
labels_changed: false,
comments_to_create: [
Comment {
identity: Pending,
body: Events(
[
Start(
Paragraph,
),
Text(
"new comment",
),
End(
Paragraph,
),
],
),
},
],
comments_to_update: [],
comments_to_delete: [],
children_to_create: [],
children_to_delete: [],
}
"#);
}
#[test]
fn test_compute_node_diff_comment_deleted() {
let mut old = make_issue("Root", Some(1));
old.contents.comments.push(Comment {
identity: CommentIdentity::Created { user: "user".to_string(), id: 123 },
body: Events::parse("old comment"),
});
let new = make_issue("Root", Some(1));
let diff = compute_node_diff(&new, Some(&old));
assert_eq!(diff.comments_to_delete, vec![123]);
insta::assert_debug_snapshot!(diff, @"
IssueDiff {
body_changed: false,
state_changed: false,
title_changed: false,
labels_changed: false,
comments_to_create: [],
comments_to_update: [],
comments_to_delete: [
123,
],
children_to_create: [],
children_to_delete: [],
}
");
}
#[test]
fn test_compute_node_diff_pending_child() {
let old = make_issue("Root", Some(1));
let mut new = make_issue("Root", Some(1));
let child = make_issue("New Child", None);
new.children.insert(child.selector(), child);
let diff = compute_node_diff(&new, Some(&old));
assert_eq!(diff.children_to_create.len(), 1);
}
}