use std::cmp::Ordering;
use chrono::{DateTime, SecondsFormat, TimeDelta, Utc};
use onetaskgraph_plugin_api::Repository;
pub const SETTLED: TimeDelta = TimeDelta::seconds(60);
#[must_use]
pub fn settled(issue: DateTime<Utc>, item: DateTime<Utc>, served: DateTime<Utc>) -> bool {
served - issue.max(item) > SETTLED
}
pub const PREFIX: &str = "gh2|";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Parts {
pub issue: DateTime<Utc>,
pub item: DateTime<Utc>,
pub comments: u64,
pub relationships: u64,
pub repository: Repository,
}
#[must_use]
pub fn spell(parts: &Parts) -> String {
format!(
"{PREFIX}{}|{}|{}|{}|{}",
parts.issue.to_rfc3339_opts(SecondsFormat::AutoSi, true),
parts.item.to_rfc3339_opts(SecondsFormat::AutoSi, true),
parts.comments,
parts.relationships,
parts.repository.as_str()
)
}
fn parts(token: &str) -> Option<Parts> {
let mut parts = token.strip_prefix(PREFIX)?.splitn(5, '|');
let issue = DateTime::parse_from_rfc3339(parts.next()?).ok()?;
let item = DateTime::parse_from_rfc3339(parts.next()?).ok()?;
let comments = parts.next()?.parse().ok()?;
let relationships = parts.next()?.parse().ok()?;
let repository = Repository::try_from(parts.next()?.to_owned()).ok()?;
Some(Parts {
issue: issue.with_timezone(&Utc),
item: item.with_timezone(&Utc),
comments,
relationships,
repository,
})
}
#[must_use]
pub fn order(a: &str, b: &str) -> Option<Ordering> {
let (a, b) = (parts(a)?, parts(b)?);
if a.repository != b.repository {
return None;
}
let forward = [
a.issue.cmp(&b.issue),
a.item.cmp(&b.item),
a.relationships.cmp(&b.relationships),
];
if forward.iter().all(|part| part.is_eq()) {
Some(b.comments.cmp(&a.comments))
} else if forward.iter().all(|part| part.is_le()) {
Some(Ordering::Less)
} else if forward.iter().all(|part| part.is_ge()) {
Some(Ordering::Greater)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone as _;
fn at(second: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 10, 1, 0, 0, second).unwrap()
}
fn token(issue: u32, item: u32, comments: u64, relationships: u64) -> String {
spell(&Parts {
issue: at(issue),
item: at(item),
comments,
relationships,
repository: repository("github.com/acme/widgets"),
})
}
fn repository(origin: &str) -> Repository {
Repository::try_from(origin.to_owned()).expect("a repository origin")
}
#[test]
fn every_change_kind_orders_the_later_version_after_the_earlier() {
let before = token(1, 1, 2, 3);
for after in [
token(2, 1, 3, 3), token(2, 1, 2, 3), token(1, 1, 1, 3), token(1, 2, 2, 3), token(1, 1, 2, 4), token(1, 1, 2, 5), ] {
assert_eq!(order(&after, &before), Some(Ordering::Greater), "{after}");
assert_eq!(order(&before, &after), Some(Ordering::Less), "{after}");
}
assert_eq!(order(&before, &before), Some(Ordering::Equal));
}
#[test]
fn a_token_is_given_only_once_the_later_time_is_more_than_the_margin_behind_the_read() {
let served = at(0) + SETTLED + TimeDelta::seconds(1);
assert!(settled(at(0), at(0), served));
assert!(
!settled(at(0), at(1), served),
"the board item's later time counts"
);
assert!(!settled(at(1), at(0), served), "and so does the issue's");
assert!(
!settled(at(0), at(0), at(0) + SETTLED),
"exactly the margin is not past it"
);
assert!(
!settled(at(5), at(5), at(5)),
"read in the second of the change"
);
}
#[test]
fn crossed_parts_another_repository_and_foreign_values_are_not_ordered() {
assert_eq!(order(&token(2, 1, 0, 0), &token(1, 2, 0, 0)), None);
assert_eq!(
order(&token(2, 1, 0, 0), &token(1, 1, 0, 1)),
None,
"a later time beside fewer relationship events is no version read after the other"
);
let transferred = spell(&Parts {
issue: at(1),
item: at(1),
comments: 0,
relationships: 0,
repository: repository("github.com/acme/gadgets"),
});
assert_eq!(
order(&transferred, &token(1, 1, 0, 0)),
None,
"a token naming another repository is not equal, and not ordered"
);
assert_eq!(order("md1|abc", &token(1, 1, 0, 0)), None);
assert_eq!(
order(
"gh2|not-a-time|x|1|0|github.com/a/b",
"gh2|not-a-time|x|1|0|github.com/a/b"
),
None
);
assert_eq!(
order(
"gh2|2026-10-01T00:00:01Z|2026-10-01T00:00:01Z|0|0|",
"gh2|2026-10-01T00:00:01Z|2026-10-01T00:00:01Z|0|0|"
),
None,
"a token naming no repository"
);
assert_eq!(
order(
"gh2|2026-10-01T00:00:01Z|2026-10-01T00:00:01Z|0|0|a/b",
"gh2|2026-10-01T00:00:01Z|2026-10-01T00:00:01Z|0|0|a/b"
),
None,
"a token naming something that is no repository origin"
);
assert_eq!(
order(
"gh1|2026-10-01T00:00:01Z|2026-10-01T00:00:01Z|0",
"gh1|2026-10-01T00:00:01Z|2026-10-01T00:00:01Z|0"
),
None,
"a token from before the relationship count is not ordered, even against itself"
);
assert_eq!(
order(&format!("{}|extra", token(1, 1, 0, 0)), &token(1, 1, 0, 0)),
None,
"anything after the repository is part of it, and names another"
);
}
}