diff --git a/README.md b/README.md index c7a0e79..1936205 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ Missing features: - [ ] `pkh chlog` - [x] Auto-generate changelog entry - [x] Extra flags: backport, non-maintainer upload, no change rebuild, ... + - [x] Select the target series, matching changelog suite names with their series (unstable ≡ sid) - [ ] Commit changelog entry - [x] `pkh new` - [x] Scaffold a new Debian source package (interactive, multiple languages) diff --git a/src/changelog.rs b/src/changelog.rs index 1d96b1b..10578a5 100644 --- a/src/changelog.rs +++ b/src/changelog.rs @@ -294,9 +294,16 @@ pub enum SeriesCandidates { /// cannot be made (cancelled, no interactive user) `fallback` — the /// changelog's current series — is used instead. Choose { - /// Series names to offer. + /// Selector labels: each series name, or ` ()` + /// for a series aliased by a changelog suite name (Debian's + /// 'unstable (sid)'). options: Vec, - /// Preselected series. + /// Changelog distribution each label of `options` maps to, + /// parallel to it: an aliased label selects its suite name — + /// what a changelog distribution field expects — while every + /// other label selects itself. + values: Vec, + /// Preselected label. default: String, /// Series to fall back to when nothing can be selected. fallback: String, @@ -312,8 +319,10 @@ pub enum SeriesCandidates { /// An UNRELEASED entry offers itself as a pinned first option (selecting it /// keeps the changelog unreleased) on top of the current vendor's series /// list, defaulting to the development series; any other series resolves -/// through the series list of its own distribution. `None` when the -/// changelog cannot be parsed (no default to derive at all). +/// through the series list of its own distribution. A changelog suite name +/// (Debian's 'unstable') identifies the same series as its alias's codename +/// ('sid') and resolves to that dist's list. `None` when the changelog +/// cannot be parsed (no default to derive at all). pub async fn series_candidates(changelog_path: &Path) -> Option { let (_package, _version, current) = parse_changelog_header(changelog_path).ok()?; @@ -321,10 +330,13 @@ pub async fn series_candidates(changelog_path: &Path) -> Option { - options.extend(series_list); + let (labels, series_values, _) = selector_options(&dist, &series_list, ""); + let mut options = vec![crate::distro_info::UNRELEASED.to_string()]; + let mut values = vec![crate::distro_info::UNRELEASED.to_string()]; + options.extend(labels); + values.extend(series_values); // Default to the development series (the first real entry), // not to the pinned UNRELEASED entry itself let default = if options.len() > 1 { @@ -334,6 +346,7 @@ pub async fn series_candidates(changelog_path: &Path) -> Option Option Some(SeriesCandidates::Keep(current)), } } else { - match crate::distro_info::get_dist_from_series(¤t).await { - Ok(dist) => match crate::distro_info::get_ordered_series_name(&dist).await { - // Even an empty list goes through the selector: its - // fallback prints and takes the default, like it always has - Ok(options) => Some(SeriesCandidates::Choose { - options, - default: current.clone(), - fallback: current, - }), - Err(_) => Some(SeriesCandidates::Keep(current)), - }, - Err(_) => Some(SeriesCandidates::Keep(current)), + // The changelog may target a suite name instead of a series + // codename: Debian conventionally writes 'unstable' where the + // series data carries 'sid'. The two identify the same series: + // the alias resolves to its codename's dist for the lookup. + let resolved = match crate::distro_info::get_dist_from_series(¤t).await { + Ok(dist) => Some((dist, current.clone())), + Err(_) => crate::distro_info::resolve_suite_alias(¤t), + }; + match resolved { + Some((dist, canonical)) => { + match crate::distro_info::get_ordered_series_name(&dist).await { + // Even an empty list goes through the selector: its + // fallback prints and takes the default, like it always has + Ok(series_list) => { + let (options, values, default) = + selector_options(&dist, &series_list, &canonical); + Some(SeriesCandidates::Choose { + options, + values, + // A stale alias whose codename left the series + // list offers the raw name instead + default: default.unwrap_or_else(|| canonical.clone()), + fallback: current, + }) + } + Err(_) => Some(SeriesCandidates::Keep(current)), + } + } + None => Some(SeriesCandidates::Keep(current)), } } } +/// The selector entries for a dist's series list: (labels, changelog +/// targets, label of `current`'s entry). A series aliased by a +/// changelog suite name (Debian's 'unstable' for 'sid') is offered as +/// ` ()` but targets the suite name — what a changelog +/// distribution field expects — while every other series is offered, +/// and targeted, under its own name. `current` may be a name that is +/// not in the list at all (e.g. UNRELEASED), in which case no default +/// is returned. +fn selector_options( + dist: &str, + series: &[String], + current: &str, +) -> (Vec, Vec, Option) { + let mut labels = Vec::with_capacity(series.len()); + let mut values = Vec::with_capacity(series.len()); + let mut default = None; + for s in series { + let (label, value) = match crate::distro_info::series_suite_alias(dist, s) { + Some(suite) => (format!("{suite} ({s})"), suite), + None => (s.clone(), s.clone()), + }; + if s == current { + default = Some(label.clone()); + } + labels.push(label); + values.push(value); + } + (labels, values, default) +} + +/// The changelog distribution a selected series-selector label maps to +/// ([`SeriesCandidates::Choose`]): an aliased entry ('unstable (sid)') +/// targets its suite name, any other label targets itself, and a +/// free-typed series the selector does not offer is its own target. +pub fn selected_series(options: &[String], values: &[String], selected: String) -> String { + options + .iter() + .position(|o| *o == selected) + .and_then(|idx| values.get(idx).cloned()) + .unwrap_or(selected) +} + /// Parse a changelog file footer to extract maintainer information /// Returns (name, email) tuple from the last modification entry pub fn parse_changelog_footer(path: &Path) -> Result<(String, String), Box> { @@ -639,12 +711,18 @@ mod tests { match series_candidates(&path).await { Some(SeriesCandidates::Choose { options, + values, default, fallback, }) => { assert_eq!(options[0], "UNRELEASED"); + assert_eq!(values[0], "UNRELEASED"); assert!(options.len() > 1, "the vendor series list is offered"); assert_eq!(default, options[1]); + assert_eq!( + selected_series(&options, &values, default.clone()), + values[1] + ); assert_eq!(fallback, "UNRELEASED"); } other => panic!("expected Choose, got {other:?}"), @@ -677,17 +755,105 @@ mod tests { match series_candidates(&path).await { Some(SeriesCandidates::Choose { options, + values, default, fallback, }) => { - assert_eq!(options, vendor_series); - assert_eq!(default, *current); + // The current series is preselected through its label, and + // selecting it targets the name the changelog already carries + let idx = values + .iter() + .position(|v| v == current) + .expect("the current series is offered"); + assert_eq!(default, options[idx]); + assert_eq!( + selected_series(&options, &values, options[idx].clone()), + *current + ); assert_eq!(fallback, *current); } other => panic!("expected Choose, got {other:?}"), } } + /// A changelog targeting Debian's 'unstable' suite — the conventional + /// Debian development distribution, which the series data knows as the + /// codename 'sid' — resolves to the Debian series list: the selector + /// offers the aliased entry as 'unstable (sid)', preselected, and + /// selecting it targets 'unstable' itself. + #[tokio::test] + async fn series_candidates_suite_alias_matches_unstable_and_sid() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("changelog"); + std::fs::write( + &path, + "hello (1.0-1) unstable; urgency=medium\n\n * Something.\n\n \ + -- A B Mon, 01 Jan 2024 00:00:00 +0000\n", + ) + .unwrap(); + + match series_candidates(&path).await { + Some(SeriesCandidates::Choose { + options, + values, + default, + fallback, + }) => { + let idx = options + .iter() + .position(|o| o == "unstable (sid)") + .expect("sid is offered as its suite alias"); + assert_eq!(values[idx], "unstable"); + assert_eq!(default, "unstable (sid)"); + assert_eq!(fallback, "unstable"); + // Selecting the aliased entry targets the suite name + assert_eq!( + selected_series(&options, &values, options[idx].clone()), + "unstable" + ); + // A free-typed series the selector does not offer is its own + // target + assert_eq!( + selected_series(&options, &values, "trixie".to_string()), + "trixie" + ); + } + other => panic!("expected Choose, got {other:?}"), + } + } + + /// A changelog naming the codename ('sid') resolves to the same + /// selector entry as the suite alias ('unstable'): the two identify + /// the same series. + #[tokio::test] + async fn series_candidates_sid_defaults_to_the_suite_alias_label() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("changelog"); + std::fs::write( + &path, + "hello (1.0-1) sid; urgency=medium\n\n * Something.\n\n \ + -- A B Mon, 01 Jan 2024 00:00:00 +0000\n", + ) + .unwrap(); + + match series_candidates(&path).await { + Some(SeriesCandidates::Choose { + options, + values, + default, + fallback, + }) => { + assert_eq!(default, "unstable (sid)"); + assert_eq!( + selected_series(&options, &values, default.clone()), + "unstable" + ); + assert_eq!(fallback, "sid"); + } + other => panic!("expected Choose, got {other:?}"), + } + } + /// Without a parsable changelog there is no candidate at all. #[tokio::test] async fn series_candidates_none_without_changelog() { diff --git a/src/main.rs b/src/main.rs index 359e4cb..7aabb16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -445,10 +445,13 @@ fn main() { match rt.block_on(pkh::changelog::series_candidates(&changelog_path)) { Some(pkh::changelog::SeriesCandidates::Choose { options, + values, default, fallback, }) => match pkh::ui::select_series(&options, &default) { - Ok(selected) => Some(selected), + Ok(selected) => { + Some(pkh::changelog::selected_series(&options, &values, selected)) + } Err(e) => { error!( "Series selection failed: {}. Using current series '{}' instead.",