use super::GitTreeError;
use super::cmd::for_each_ref_under;
use std::collections::HashMap;
use std::path::Path;
const CONFLICTED_PREFIX: &str = "refs/lernie/conflicted/";
const BUDGET_PREFIX: &str = "refs/lernie/budget-exhausted/";
const ABANDONED_PREFIX: &str = "refs/lernie/abandoned/";
const NOTIFY_PREFIX: &str = "refs/lernie/notify/";
#[derive(Debug, Default)]
pub(super) struct Marks {
conflicted: HashMap<String, String>,
budget: HashMap<String, String>,
abandoned: HashMap<String, String>,
notify: HashMap<String, String>,
}
impl Marks {
pub(super) fn from_repo(git_dir: &Path) -> Result<Self, GitTreeError> {
Ok(Self {
conflicted: oids_under(git_dir, CONFLICTED_PREFIX)?,
budget: oids_under(git_dir, BUDGET_PREFIX)?,
abandoned: oids_under(git_dir, ABANDONED_PREFIX)?,
notify: oids_under(git_dir, NOTIFY_PREFIX)?,
})
}
pub(super) fn conflicted_oid(&self, agent_id: &str) -> Option<String> {
self.conflicted.get(agent_id).cloned()
}
pub(super) fn budget_oid(&self, agent_id: &str) -> Option<String> {
self.budget.get(agent_id).cloned()
}
pub(super) fn abandoned_oid(&self, agent_id: &str) -> Option<String> {
self.abandoned.get(agent_id).cloned()
}
pub(super) fn notify_oid(&self, agent_id: &str) -> Option<String> {
self.notify.get(agent_id).cloned()
}
}
fn oids_under(git_dir: &Path, prefix: &str) -> Result<HashMap<String, String>, GitTreeError> {
let out = for_each_ref_under(git_dir, prefix)?;
Ok(parse_oids(&out, prefix))
}
fn parse_oids(stdout: &[u8], prefix: &str) -> HashMap<String, String> {
String::from_utf8_lossy(stdout)
.lines()
.filter_map(|line| {
let (refname, oid) = line.trim().split_once(' ')?;
let id = refname.strip_prefix(prefix)?;
(!id.is_empty()).then(|| (id.to_string(), oid.to_string()))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_oids_strips_prefix_and_keeps_oid() {
let out = b"refs/lernie/conflicted/a-b 1111111111111111111111111111111111111111\n\
refs/lernie/conflicted/c-d 2222222222222222222222222222222222222222\n";
let oids = parse_oids(out, CONFLICTED_PREFIX);
assert_eq!(oids.len(), 2);
assert_eq!(
oids.get("a-b").map(String::as_str),
Some("1".repeat(40).as_str())
);
assert_eq!(
oids.get("c-d").map(String::as_str),
Some("2".repeat(40).as_str())
);
}
#[test]
fn parse_oids_ignores_nonmatching_and_malformed_lines() {
let out = b"refs/heads/agents/a-b 3333333333333333333333333333333333333333\n\
refs/lernie/budget-exhausted/x-y 4444444444444444444444444444444444444444\n\
nospaceline\n";
let oids = parse_oids(out, BUDGET_PREFIX);
assert_eq!(oids.len(), 1);
assert_eq!(
oids.get("x-y").map(String::as_str),
Some("4".repeat(40).as_str())
);
}
#[test]
fn parse_oids_drops_empty_id() {
let out = b"refs/lernie/notify/ 5555555555555555555555555555555555555555\n";
assert!(parse_oids(out, NOTIFY_PREFIX).is_empty());
}
#[test]
fn parse_oids_empty_input_is_empty_map() {
assert!(parse_oids(b"", CONFLICTED_PREFIX).is_empty());
}
#[test]
fn getters_return_oid_or_none() {
let marks = Marks {
conflicted: HashMap::from([("a-b".to_string(), "aa".to_string())]),
budget: HashMap::from([("c-d".to_string(), "bb".to_string())]),
abandoned: HashMap::from([("e-f".to_string(), "cc".to_string())]),
notify: HashMap::from([("g-h".to_string(), "dd".to_string())]),
};
assert_eq!(marks.conflicted_oid("a-b").as_deref(), Some("aa"));
assert_eq!(marks.conflicted_oid("c-d"), None);
assert_eq!(marks.budget_oid("c-d").as_deref(), Some("bb"));
assert_eq!(marks.abandoned_oid("e-f").as_deref(), Some("cc"));
assert_eq!(marks.notify_oid("g-h").as_deref(), Some("dd"));
assert_eq!(marks.notify_oid("a-b"), None);
}
}