use std::collections::BTreeMap;
use anyhow::{Context, Result};
use serde::Serialize;
use crate::warehouse::dep_graph::WorkspaceGraph;
use crate::warehouse::iceberg::IcebergWarehouse;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ChangeSet {
pub changed: Vec<String>,
pub affected: Vec<String>,
pub build_order: Vec<String>,
}
pub async fn detect(
wh: &IcebergWarehouse,
graph: &WorkspaceGraph,
workspace_name: &str,
) -> Result<ChangeSet> {
let history = crate::release::pipeline::query_release_history(wh, workspace_name, Some(1))
.await
.context("read release history (Urðr)")?;
let recorded: BTreeMap<String, String> = history
.last()
.map(|r| {
r.repos
.iter()
.map(|rr| (rr.repo.clone(), rr.git.sha.clone()))
.collect()
})
.unwrap_or_default();
let mut current: BTreeMap<String, String> = BTreeMap::new();
for (name, facts) in &graph.facts {
let sha = crate::gitio::head_sha(&facts.root)
.with_context(|| format!("read HEAD of `{name}` (Verðandi)"))?;
current.insert(name.clone(), sha);
}
let changed = diff_changed(&recorded, ¤t);
let affected = graph.affected_by_change(&changed);
let build_order = graph.build_order().unwrap_or_default();
Ok(ChangeSet { changed, affected, build_order })
}
pub fn diff_changed(
recorded: &BTreeMap<String, String>,
current: &BTreeMap<String, String>,
) -> Vec<String> {
current
.iter()
.filter(|(name, sha)| recorded.get(*name).map_or(true, |prev| prev != *sha))
.map(|(name, _)| name.clone())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn map(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
#[test]
fn unchanged_repo_is_not_reported() {
let recorded = map(&[("a", "sha1"), ("b", "sha1")]);
let current = map(&[("a", "sha1"), ("b", "sha1")]);
assert!(diff_changed(&recorded, ¤t).is_empty());
}
#[test]
fn moved_sha_is_reported() {
let recorded = map(&[("a", "sha1"), ("b", "sha1")]);
let current = map(&[("a", "sha2"), ("b", "sha1")]);
assert_eq!(diff_changed(&recorded, ¤t), vec!["a".to_string()]);
}
#[test]
fn never_recorded_repo_counts_as_changed() {
let recorded = map(&[("a", "sha1")]);
let current = map(&[("a", "sha1"), ("b", "sha9")]);
assert_eq!(diff_changed(&recorded, ¤t), vec!["b".to_string()]);
}
#[test]
fn empty_history_means_everything_changed() {
let recorded = BTreeMap::new();
let current = map(&[("a", "s"), ("b", "s")]);
assert_eq!(
diff_changed(&recorded, ¤t),
vec!["a".to_string(), "b".to_string()]
);
}
#[test]
fn removed_repo_is_not_reported() {
let recorded = map(&[("a", "sha1"), ("gone", "sha1")]);
let current = map(&[("a", "sha1")]);
assert!(diff_changed(&recorded, ¤t).is_empty());
}
}