use std::collections::{BTreeMap, HashSet};
use crate::prune_retired::apply_marker;
use sandogasa_inventory::Inventory;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sig {
Hyperscale,
ProposedUpdates,
}
impl Sig {
pub fn tag_prefix(self) -> &'static str {
match self {
Sig::Hyperscale => "hyperscale",
Sig::ProposedUpdates => "proposed_updates",
}
}
pub fn ships_rhel(self) -> bool {
matches!(self, Sig::Hyperscale)
}
pub fn from_source(preset: Option<&str>, group_url: &str) -> Option<Sig> {
let hay = preset.unwrap_or(group_url).to_lowercase();
if hay.contains("hyperscale") {
Some(Sig::Hyperscale)
} else if hay.contains("proposed") {
Some(Sig::ProposedUpdates)
} else {
None
}
}
}
pub fn release_tag_glob(sig: Sig) -> String {
format!("{}*-release", sig.tag_prefix())
}
pub fn release_tag_matches(tag: &str, sig: Sig, releases: &[u32]) -> bool {
let Some(rest) = tag.strip_prefix(sig.tag_prefix()) else {
return false;
};
if !tag.ends_with("-release") {
return false;
}
let Some(version_token) = rest.split('-').next() else {
return false;
};
let (digits, is_stream) = match version_token.strip_suffix('s') {
Some(d) => (d, true),
None => (version_token, false),
};
let Ok(major) = digits.parse::<u32>() else {
return false;
};
if !releases.contains(&major) {
return false;
}
is_stream || sig.ships_rhel()
}
pub struct Classification<'a> {
pub unshipped: BTreeMap<&'a str, String>,
pub archived_builds: BTreeMap<&'a str, String>,
}
pub fn classify<'a>(
synced: &'a [String],
archived: &HashSet<String>,
shipped: &HashSet<String>,
) -> Classification<'a> {
let mut c = Classification {
unshipped: BTreeMap::new(),
archived_builds: BTreeMap::new(),
};
for name in synced {
if !archived.contains(name) {
continue;
}
if shipped.contains(name) {
c.archived_builds.insert(
name.as_str(),
"archived in GitLab; CBS builds remain (run hs-relmon to prune)".to_string(),
);
} else {
c.unshipped.insert(
name.as_str(),
"archived in GitLab, no released CBS build".to_string(),
);
}
}
c
}
pub fn shipped_packages(
sig: Sig,
releases: &[u32],
verbose: bool,
) -> Result<HashSet<String>, String> {
let glob = release_tag_glob(sig);
let tags: Vec<String> = sandogasa_koji::list_tags(&glob, Some("cbs"))?
.into_iter()
.filter(|t| release_tag_matches(t, sig, releases))
.collect();
if verbose {
eprintln!(
"[poi-tracker] {} release tag(s) for releases {releases:?}: {}",
tags.len(),
tags.join(", ")
);
}
let mut shipped = HashSet::new();
for tag in &tags {
if verbose {
eprintln!("[poi-tracker] listing packages in {tag}");
}
for name in sandogasa_koji::list_tagged_package_names(tag, Some("cbs"))? {
shipped.insert(name);
}
}
Ok(shipped)
}
pub struct MarkOutcome {
pub unshipped: Vec<String>,
pub archived_builds: Vec<String>,
pub changed: usize,
}
pub fn mark(
inventory: &mut Inventory,
synced: &[String],
archived: &HashSet<String>,
shipped: &HashSet<String>,
) -> MarkOutcome {
let c = classify(synced, archived, shipped);
let changed = apply_marker(inventory, synced, &c.unshipped, |p| &mut p.unshipped)
+ apply_marker(inventory, synced, &c.archived_builds, |p| {
&mut p.archived_builds
});
MarkOutcome {
unshipped: c.unshipped.keys().map(|s| s.to_string()).collect(),
archived_builds: c.archived_builds.keys().map(|s| s.to_string()).collect(),
changed,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn set(items: &[&str]) -> HashSet<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn sig_from_preset_and_url() {
assert_eq!(
Sig::from_source(Some("hyperscale"), ""),
Some(Sig::Hyperscale)
);
assert_eq!(
Sig::from_source(Some("proposed-updates"), ""),
Some(Sig::ProposedUpdates)
);
assert_eq!(
Sig::from_source(None, "https://gitlab.com/CentOS/Hyperscale/rpms"),
Some(Sig::Hyperscale)
);
assert_eq!(
Sig::from_source(None, "https://gitlab.com/CentOS/proposed_updates/rpms"),
Some(Sig::ProposedUpdates)
);
assert_eq!(Sig::from_source(Some("centos-stream"), ""), None);
}
#[test]
fn hyperscale_counts_rhel_and_stream() {
let r = &[9, 10];
assert!(release_tag_matches(
"hyperscale10s-packages-main-release",
Sig::Hyperscale,
r
));
assert!(release_tag_matches(
"hyperscale9-packages-facebook-release",
Sig::Hyperscale,
r
));
assert!(!release_tag_matches(
"hyperscale8s-packages-main-release",
Sig::Hyperscale,
r
));
assert!(!release_tag_matches(
"hyperscale10s-packages-main-testing",
Sig::Hyperscale,
r
));
}
#[test]
fn proposed_updates_is_stream_only() {
let r = &[9, 10];
assert!(release_tag_matches(
"proposed_updates9s-packages-main-release",
Sig::ProposedUpdates,
r
));
assert!(!release_tag_matches(
"proposed_updates9-packages-main-release",
Sig::ProposedUpdates,
r
));
assert!(!release_tag_matches(
"hyperscale9s-packages-main-release",
Sig::ProposedUpdates,
r
));
}
#[test]
fn classify_splits_unshipped_from_archived_builds() {
let synced = vec![
"live-shipped".to_string(), "archived-shipped".to_string(), "archived-gone".to_string(), "live-unshipped".to_string(), ];
let archived = set(&["archived-shipped", "archived-gone"]);
let shipped = set(&["live-shipped", "archived-shipped", "live-unshipped"]);
let c = classify(&synced, &archived, &shipped);
assert_eq!(
c.unshipped.keys().collect::<Vec<_>>(),
vec![&"archived-gone"]
);
assert_eq!(
c.archived_builds.keys().collect::<Vec<_>>(),
vec![&"archived-shipped"]
);
}
}