use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Choice {
Enact,
Give(String),
Uncull(Option<String>),
Skip,
Quit,
}
pub fn menu(
allow_enact: bool,
allow_give: bool,
enact_word: &'static str,
) -> Vec<sandogasa_review::Choice> {
use sandogasa_review::{Arg, Choice};
let mut m = Vec::new();
if allow_enact {
m.push(Choice::new('y', enact_word));
}
if allow_give {
m.push(Choice::with('g', "give", Arg::Required("user")));
}
m.push(Choice::with('u', "uncull", Arg::Optional("inventory")));
m.push(Choice::new('s', "skip"));
m.push(Choice::new('q', "quit"));
m
}
pub fn from_answer(answer: sandogasa_review::Answer) -> Choice {
use sandogasa_review::Answer;
match answer {
Answer::Pick { key: 'y', .. } => Choice::Enact,
Answer::Pick { key: 'g', arg } => Choice::Give(arg.unwrap_or_default()),
Answer::Pick { key: 'u', arg } => Choice::Uncull(arg),
Answer::Pick { key: 'q', .. } | Answer::Quit => Choice::Quit,
Answer::Pick { .. } | Answer::All => Choice::Skip,
}
}
pub fn probe_branches(git_branches: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for b in git_branches {
let normalized = match b.split_once('.') {
Some((head, _)) if head.starts_with("epel") => head,
_ => b.as_str(),
};
let epel = normalized
.strip_prefix("epel")
.is_some_and(|v| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()));
if (normalized == "rawhide" || epel) && !out.iter().any(|o| o == normalized) {
out.push(normalized.to_string());
}
}
out
}
#[derive(Debug, Serialize)]
pub enum Probe {
Absent,
Dependents(Vec<Dependent>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Dependent {
pub source: String,
pub feature_only: bool,
}
pub fn resolve_dependents(package: &str, requirers: Vec<(String, String)>) -> Vec<Dependent> {
let mut hard: std::collections::BTreeMap<String, bool> = std::collections::BTreeMap::new();
for (binary, source) in requirers {
if binary == "(none)" || source == package {
continue;
}
let is_feature = binary.contains('+');
let entry = hard.entry(source).or_insert(false);
*entry = *entry || !is_feature;
}
hard.into_iter()
.map(|(source, had_hard)| Dependent {
source,
feature_only: !had_hard,
})
.collect()
}
pub fn probe_dependents(branch: &str, package: &str) -> Result<Probe, String> {
let fedrq = sandogasa_fedrq::Fedrq {
branch: Some(branch.to_string()),
repo: None,
};
let binaries = fedrq
.subpkgs_names(package)
.map_err(|e| format!("subpackages of {package} on {branch}: {e}"))?;
if binaries.is_empty() {
return Ok(Probe::Absent);
}
let requirers = fedrq
.whatrequires_binaries(&binaries)
.map_err(|e| format!("whatrequires for {package} on {branch}: {e}"))?;
Ok(Probe::Dependents(resolve_dependents(package, requirers)))
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(line: &str, allow_enact: bool, allow_give: bool) -> Option<Choice> {
let choices = menu(allow_enact, allow_give, "orphan");
let m = sandogasa_review::Menu {
choices: &choices,
default: Some('s'),
all: false,
quit: false,
default_arg: None,
};
sandogasa_review::parse_answer(line, &m).map(from_answer)
}
#[test]
fn skip_is_the_default_and_junk_reasks() {
for line in ["", " ", "s", "Skip"] {
assert_eq!(parse(line, true, true), Some(Choice::Skip));
}
assert_eq!(parse("y", true, true), Some(Choice::Enact));
assert_eq!(parse("q", false, false), Some(Choice::Quit));
assert_eq!(parse("x", true, true), None);
assert_eq!(parse("orphan it", true, true), None);
assert_eq!(parse("y", false, false), None);
}
#[test]
fn give_names_a_user_and_only_where_giving_applies() {
assert_eq!(
parse("g decathorpe", true, true),
Some(Choice::Give("decathorpe".to_string()))
);
assert_eq!(
parse("G someone ", true, true),
Some(Choice::Give("someone".to_string()))
);
assert_eq!(parse("g", true, true), Some(Choice::Give(String::new())));
assert_eq!(parse("g someone", true, false), None);
}
#[test]
fn uncull_is_always_open_and_optionally_files_as_essential() {
assert_eq!(parse("u", true, true), Some(Choice::Uncull(None)));
assert_eq!(
parse("u keep-tools.toml", false, false),
Some(Choice::Uncull(Some("keep-tools.toml".to_string())))
);
assert_eq!(parse("uncull", false, false), Some(Choice::Uncull(None)));
}
#[test]
fn probe_branches_keep_rawhide_and_the_packages_own_epels() {
let git: Vec<String> = [
"rawhide", "f45", "f44", "epel8", "epel10.1", "epel10.0", "main",
]
.map(String::from)
.into();
assert_eq!(probe_branches(&git), ["rawhide", "epel8", "epel10"]);
let epel_only: Vec<String> = ["epel8".to_string()].into();
assert_eq!(probe_branches(&epel_only), ["epel8"]);
assert!(probe_branches(&["epel-playground".to_string(), "main".to_string()]).is_empty());
}
#[test]
fn dependents_resolve_to_sources_and_flag_feature_only_edges() {
let requirers = vec![
("(none)".to_string(), "(none)".to_string()),
("breezy".to_string(), "breezy".to_string()),
(
"python3-dulwich+merge".to_string(),
"python-dulwich".to_string(),
),
];
let deps = resolve_dependents("python-merge3", requirers);
assert_eq!(
deps,
[
Dependent {
source: "breezy".into(),
feature_only: false
},
Dependent {
source: "python-dulwich".into(),
feature_only: true
},
]
);
}
#[test]
fn a_source_with_both_edges_is_a_hard_dependency() {
let deps = resolve_dependents(
"libx",
vec![
("app".into(), "app".into()),
("app+extra".into(), "app".into()),
],
);
assert_eq!(
deps,
[Dependent {
source: "app".into(),
feature_only: false
}]
);
}
#[test]
fn own_subpackages_and_none_rows_are_not_dependents() {
let deps = resolve_dependents(
"rust-buf-min",
vec![
("(none)".into(), "(none)".into()),
("rust-buf-min".into(), "rust-buf-min".into()),
("rust-buf-min+std".into(), "rust-buf-min".into()),
(
"rust-v_htmlescape+default".into(),
"rust-v_htmlescape".into(),
),
],
);
assert_eq!(
deps,
[Dependent {
source: "rust-v_htmlescape".into(),
feature_only: true
}]
);
}
}