use lonca_core::oplog::OPLOG_REF;
use lonca_core::{ChangeId, GitId};
use crate::state::State;
pub const TRUNK_REF: &str = "refs/heads/main";
pub const POLICY_REF: &str = "refs/lonca/policy";
pub const CHANGES_PREFIX: &str = "refs/lonca/changes/";
pub const STAGING_REFS: [&str; 2] = ["refs/lonca/import/initial", "refs/lonca/incoming"];
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RefCheck {
Fine,
Differs(Option<GitId>),
NotLogged,
Unknown,
}
pub fn change_ref(name: &str) -> Option<(ChangeId, u64)> {
let rest = name.strip_prefix(CHANGES_PREFIX)?;
let (change, seq) = rest.split_once('/')?;
let parsed = (ChangeId::from_reverse_hex(change)?, seq.parse().ok()?);
(change_ref_name(&parsed.0, parsed.1) == name).then_some(parsed)
}
pub fn change_ref_name(change: &ChangeId, seq: u64) -> String {
format!("{CHANGES_PREFIX}{change}/{seq}")
}
pub fn check(state: &State, name: &str, found: &GitId) -> RefCheck {
let against = |want: &Option<GitId>| match want {
Some(w) if w == found => RefCheck::Fine,
w => RefCheck::Differs(w.clone()),
};
match name {
TRUNK_REF => against(&state.trunk),
POLICY_REF => against(&state.policy),
OPLOG_REF => RefCheck::Fine,
n if STAGING_REFS.contains(&n) => RefCheck::Fine,
n => match change_ref(n) {
Some((_, seq)) if seq >= state.next_seq => RefCheck::NotLogged,
Some((change, seq)) => match state.proposed.get(&seq) {
Some((c, commit)) if *c == change && commit == found => RefCheck::Fine,
Some((c, commit)) if *c == change => RefCheck::Differs(Some(commit.clone())),
_ => RefCheck::Unknown,
},
None => RefCheck::Unknown,
},
}
}
pub fn missing_changes<'a>(
state: &'a State,
present: &'a std::collections::BTreeSet<String>,
) -> impl Iterator<Item = (String, &'a GitId)> + 'a {
state
.proposed
.iter()
.map(|(seq, (change, commit))| (change_ref_name(change, *seq), commit))
.filter(move |(name, _)| !present.contains(name))
}
#[cfg(test)]
mod tests {
use super::*;
fn gid(c: char) -> GitId {
GitId(std::iter::repeat_n(c, 40).collect())
}
#[test]
fn every_ref_is_known_named_right_or_reported() {
let c = ChangeId::random();
let mut s = State {
trunk: Some(gid('a')),
policy: Some(gid('b')),
next_seq: 5,
..State::default()
};
s.proposed.insert(3, (c, gid('c')));
assert_eq!(check(&s, TRUNK_REF, &gid('a')), RefCheck::Fine);
assert_eq!(
check(&s, TRUNK_REF, &gid('x')),
RefCheck::Differs(Some(gid('a')))
);
assert_eq!(
check(&s, POLICY_REF, &gid('x')),
RefCheck::Differs(Some(gid('b')))
);
assert_eq!(check(&s, OPLOG_REF, &gid('x')), RefCheck::Fine);
for staging in STAGING_REFS {
assert_eq!(check(&s, staging, &gid('x')), RefCheck::Fine);
}
let own = change_ref_name(&c, 3);
assert_eq!(check(&s, &own, &gid('c')), RefCheck::Fine);
assert_eq!(
check(&s, &own, &gid('x')),
RefCheck::Differs(Some(gid('c')))
);
assert_eq!(
check(&s, &change_ref_name(&c, 4), &gid('c')),
RefCheck::Unknown
);
let other = ChangeId::random();
assert_eq!(
check(&s, &change_ref_name(&other, 3), &gid('c')),
RefCheck::Unknown
);
assert_eq!(
check(&s, &change_ref_name(&c, 5), &gid('c')),
RefCheck::NotLogged
);
for unknown in [
"refs/heads/other",
"refs/tags/v1",
"refs/lonca/other",
"refs/lonca/changes/x/1",
"refs/lonca/changes/1",
"refs/notes/commits",
] {
assert_eq!(
check(&s, unknown, &gid('c')),
RefCheck::Unknown,
"{unknown}"
);
}
let before = State::default();
assert_eq!(
check(&before, TRUNK_REF, &gid('a')),
RefCheck::Differs(None)
);
}
#[test]
fn a_change_ref_parses_back() {
let c = ChangeId::random();
assert_eq!(change_ref(&change_ref_name(&c, 7)), Some((c, 7)));
let name = format!("{CHANGES_PREFIX}{c}/+7");
assert_eq!(change_ref(&name), None);
assert_eq!(change_ref(&format!("{CHANGES_PREFIX}{c}/x")), None);
assert_eq!(change_ref(&format!("{CHANGES_PREFIX}{c}/07")), None);
assert_eq!(change_ref(&format!("{CHANGES_PREFIX}{c}")), None);
assert_eq!(change_ref("refs/heads/main"), None);
}
#[test]
fn missing_proposal_refs_are_found() {
let (a, b) = (ChangeId::random(), ChangeId::random());
let mut s = State::default();
s.proposed.insert(1, (a, gid('a')));
s.proposed.insert(2, (b, gid('b')));
let present = [change_ref_name(&a, 1)].into_iter().collect();
let missing: Vec<_> = missing_changes(&s, &present).collect();
assert_eq!(missing, [(change_ref_name(&b, 2), &gid('b'))]);
}
}