chlog,cli,docs: offer suite-aliased series in the chlog selector
The chlog series selector only appeared when the changelog's current distribution resolved to a known series; a Debian package targeting 'unstable' (or 'stable', 'testing', ...) fell through to keeping the current series, silently, with no menu. Resolve the changelog distribution through the suite aliases first (unstable identifies the same series as sid, which resolves to the Debian series list). The selector offers an aliased series as '<suite> (<series>)' — 'unstable (sid)' — preselected, but selects the suite name: what a changelog distribution field expects, instead of the codename. Every other label and free-typed input selects itself, unchanged. Reflect the selector in the README roadmap checklist.
This commit is contained in:
@@ -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)
|
||||
|
||||
+186
-20
@@ -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 `<suite> (<series>)`
|
||||
/// for a series aliased by a changelog suite name (Debian's
|
||||
/// 'unstable (sid)').
|
||||
options: Vec<String>,
|
||||
/// 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<String>,
|
||||
/// 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<SeriesCandidates> {
|
||||
let (_package, _version, current) = parse_changelog_header(changelog_path).ok()?;
|
||||
|
||||
@@ -321,10 +330,13 @@ pub async fn series_candidates(changelog_path: &Path) -> Option<SeriesCandidates
|
||||
// Vendors keep original casing ("Ubuntu"), while the series data
|
||||
// keys are lowercase
|
||||
let dist = crate::build::env::current_vendor().to_lowercase();
|
||||
let mut options = vec![crate::distro_info::UNRELEASED.to_string()];
|
||||
match crate::distro_info::get_ordered_series_name(&dist).await {
|
||||
Ok(series_list) => {
|
||||
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<SeriesCandidates
|
||||
};
|
||||
Some(SeriesCandidates::Choose {
|
||||
options,
|
||||
values,
|
||||
default,
|
||||
fallback: current,
|
||||
})
|
||||
@@ -341,22 +354,81 @@ pub async fn series_candidates(changelog_path: &Path) -> Option<SeriesCandidates
|
||||
Err(_) => 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
|
||||
/// `<suite> (<series>)` 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<String>, Vec<String>, Option<String>) {
|
||||
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<dyn std::error::Error>> {
|
||||
@@ -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 <a@b.c> 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 <a@b.c> 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() {
|
||||
|
||||
+4
-1
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user