use std::collections::HashMap;
use jiff::Timestamp;
use tedi::{Issue, IssueSelector};
use thiserror::Error;
pub trait Merge {
fn merge(&mut self, other: &Issue, force: bool) -> Result<(), MergeError>;
}
#[derive(Debug, Error)]
pub enum MergeError {
#[error("cannot merge virtual-only issue: virtual issues are local-only and should not participate in sync")]
VirtualIssue,
}
impl Merge for Issue {
fn merge(&mut self, other: &Issue, force: bool) -> Result<(), MergeError> {
if self.identity.is_virtual || other.identity.is_virtual {
return Err(MergeError::VirtualIssue);
}
let self_ts = self.identity.timestamps().cloned().unwrap_or_default();
let other_ts = other.identity.timestamps().cloned().unwrap_or_default();
let dominated_by = |self_field_ts: Option<Timestamp>, other_field_ts: Option<Timestamp>| -> bool {
if force {
return true;
}
match (self_field_ts, other_field_ts) {
(_, None) => false, (None, Some(_)) => true, (Some(s), Some(o)) => o > s, }
};
if dominated_by(self_ts.title, other_ts.title) {
self.contents.title = other.contents.title.clone();
}
if dominated_by(self_ts.labels, other_ts.labels) {
self.contents.labels = other.contents.labels.clone();
}
if dominated_by(self_ts.description, other_ts.description) {
let blocker_set_state = self.contents.blockers.set_state;
if let Some(other_body) = other.contents.comments.first() {
if let Some(self_body) = self.contents.comments.first_mut() {
self_body.body = other_body.body.clone();
} else {
self.contents.comments.insert(0, other_body.clone());
}
}
self.contents.blockers = other.contents.blockers.clone();
self.contents.blockers.set_state = blocker_set_state; }
if dominated_by(self_ts.state, other_ts.state) {
self.contents.state = other.contents.state.clone();
}
let self_comments_ts = self_ts.comments.iter().max().copied();
let other_comments_ts = other_ts.comments.iter().max().copied();
if dominated_by(self_comments_ts, other_comments_ts) {
let self_body = self.contents.comments.first().cloned();
self.contents.comments = other.contents.comments.clone();
if !dominated_by(self_ts.description, other_ts.description)
&& let Some(body) = self_body
{
if self.contents.comments.is_empty() {
self.contents.comments.push(body);
} else {
self.contents.comments[0] = body;
}
}
}
merge_children(&mut self.children, &other.children, force)?;
self.post_update(other);
Ok(())
}
}
fn merge_children(self_children: &mut HashMap<IssueSelector, Issue>, other_children: &HashMap<IssueSelector, Issue>, force: bool) -> Result<(), MergeError> {
for (selector, other_child) in other_children {
if let Some(self_child) = self_children.get_mut(selector) {
self_child.merge(other_child, force)?;
} else {
if !other_child.identity.is_virtual {
self_children.insert(*selector, other_child.clone());
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use tedi::{IssueContents, IssueIdentity, IssueIndex, IssueLink, IssueTimestamps, RepoInfo};
use super::*;
fn test_repo() -> RepoInfo {
RepoInfo::new("test", "repo")
}
fn make_linked_issue(title: &str, number: u64, timestamps: IssueTimestamps) -> Issue {
let url = format!("https://github.com/test/repo/issues/{number}");
let link = IssueLink::parse(&url).unwrap();
let parent_index = IssueIndex::repo_only(test_repo());
let identity = IssueIdentity::new_linked(Some(parent_index), None, link, timestamps);
Issue {
identity,
contents: IssueContents {
title: title.to_string(),
..Default::default()
},
children: HashMap::default(),
}
}
fn make_pending_issue(title: &str) -> Issue {
let parent_index = IssueIndex::repo_only(test_repo());
let identity = IssueIdentity::pending(parent_index);
Issue {
identity,
contents: IssueContents {
title: title.to_string(),
..Default::default()
},
children: HashMap::default(),
}
}
#[test]
fn test_merge_virtual_error() {
let parent_index = IssueIndex::repo_only(test_repo());
let mut virtual_issue = Issue {
identity: IssueIdentity::virtual_issue(parent_index),
contents: IssueContents::default(),
children: HashMap::default(),
};
let ts = Timestamp::now();
let timestamps = IssueTimestamps {
title: Some(ts),
description: Some(ts),
labels: Some(ts),
state: Some(ts),
comments: vec![],
};
let linked = make_linked_issue("test", 1, timestamps);
let result = virtual_issue.merge(&linked, false);
assert!(matches!(result, Err(MergeError::VirtualIssue)));
}
#[test]
fn test_merge_newer_wins() {
let old_ts = Timestamp::from_second(1000).unwrap();
let new_ts = Timestamp::from_second(2000).unwrap();
let old_timestamps = IssueTimestamps {
title: Some(old_ts),
description: None,
labels: None,
state: None,
comments: vec![],
};
let new_timestamps = IssueTimestamps {
title: Some(new_ts),
description: None,
labels: None,
state: None,
comments: vec![],
};
let mut self_issue = make_linked_issue("old title", 1, old_timestamps);
let other_issue = make_linked_issue("new title", 1, new_timestamps);
self_issue.merge(&other_issue, false).unwrap();
assert_eq!(self_issue.contents.title, "new title");
}
#[test]
fn test_merge_older_keeps_self() {
let old_ts = Timestamp::from_second(1000).unwrap();
let new_ts = Timestamp::from_second(2000).unwrap();
let new_timestamps = IssueTimestamps {
title: Some(new_ts),
description: None,
labels: None,
state: None,
comments: vec![],
};
let old_timestamps = IssueTimestamps {
title: Some(old_ts),
description: None,
labels: None,
state: None,
comments: vec![],
};
let mut self_issue = make_linked_issue("newer title", 1, new_timestamps);
let other_issue = make_linked_issue("older title", 1, old_timestamps);
self_issue.merge(&other_issue, false).unwrap();
assert_eq!(self_issue.contents.title, "newer title");
}
#[test]
fn test_merge_none_takes_some() {
let ts = Timestamp::from_second(1000).unwrap();
let none_timestamps = IssueTimestamps::default();
let some_timestamps = IssueTimestamps {
title: Some(ts),
description: None,
labels: None,
state: None,
comments: vec![],
};
let mut self_issue = make_linked_issue("self title", 1, none_timestamps);
let other_issue = make_linked_issue("other title", 1, some_timestamps);
self_issue.merge(&other_issue, false).unwrap();
assert_eq!(self_issue.contents.title, "other title");
}
#[test]
fn test_merge_force_always_takes_other() {
let old_ts = Timestamp::from_second(1000).unwrap();
let new_ts = Timestamp::from_second(2000).unwrap();
let new_timestamps = IssueTimestamps {
title: Some(new_ts),
description: None,
labels: None,
state: None,
comments: vec![],
};
let old_timestamps = IssueTimestamps {
title: Some(old_ts),
description: None,
labels: None,
state: None,
comments: vec![],
};
let mut self_issue = make_linked_issue("newer title", 1, new_timestamps);
let other_issue = make_linked_issue("older title", 1, old_timestamps);
self_issue.merge(&other_issue, true).unwrap();
assert_eq!(self_issue.contents.title, "older title");
}
#[test]
fn test_merge_pending_uses_default_timestamps() {
let ts = Timestamp::from_second(1000).unwrap();
let timestamps = IssueTimestamps {
title: Some(ts),
description: None,
labels: None,
state: None,
comments: vec![],
};
let mut pending = make_pending_issue("pending title");
let linked = make_linked_issue("linked title", 1, timestamps);
pending.merge(&linked, false).unwrap();
assert_eq!(pending.contents.title, "linked title");
}
#[test]
fn test_merge_same_timestamp_keeps_self() {
let ts = Timestamp::from_second(1000).unwrap();
let timestamps = IssueTimestamps {
title: Some(ts),
description: None,
labels: None,
state: None,
comments: vec![],
};
let mut self_issue = make_linked_issue("self title", 1, timestamps.clone());
let other_issue = make_linked_issue("other title", 1, timestamps);
self_issue.merge(&other_issue, false).unwrap();
assert_eq!(self_issue.contents.title, "self title");
}
}