lift main.rs business logic into the library
The chlog target-series resolution becomes changelog::series_candidates (UNRELEASED pinning, development-series default and fallbacks modeled by SeriesCandidates), PPA references get package_info::split_ppa (shared by pull and deb, now also rejecting empty parts), and SSH endpoints get context::ContextConfig::from_endpoint — so a library consumer can resolve series, validate PPAs and build context configurations without reimplementing the CLI's rules. All three carry unit tests; main.rs keeps only parsing of flags and error handling.
This commit is contained in:
@@ -139,6 +139,78 @@ pub fn parse_changelog_header(
|
||||
Ok((entry.source, entry.version.full(), entry.distribution))
|
||||
}
|
||||
|
||||
/// What a new changelog entry may target as series, derived from the
|
||||
/// current changelog ([`series_candidates`]).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SeriesCandidates {
|
||||
/// Offer `options` with `default` preselected; when the selection
|
||||
/// cannot be made (cancelled, no interactive user) `fallback` — the
|
||||
/// changelog's current series — is used instead.
|
||||
Choose {
|
||||
/// Series names to offer.
|
||||
options: Vec<String>,
|
||||
/// Preselected series.
|
||||
default: String,
|
||||
/// Series to fall back to when nothing can be selected.
|
||||
fallback: String,
|
||||
},
|
||||
/// Nothing to choose: the series list was unavailable, keep the
|
||||
/// current series.
|
||||
Keep(String),
|
||||
}
|
||||
|
||||
/// Derive the candidate series for a new changelog entry from the changelog
|
||||
/// at `changelog_path`.
|
||||
///
|
||||
/// 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).
|
||||
pub async fn series_candidates(changelog_path: &Path) -> Option<SeriesCandidates> {
|
||||
let (_package, _version, current) = parse_changelog_header(changelog_path).ok()?;
|
||||
|
||||
if crate::distro_info::is_unreleased(¤t) {
|
||||
// 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);
|
||||
// Default to the development series (the first real entry),
|
||||
// not to the pinned UNRELEASED entry itself
|
||||
let default = if options.len() > 1 {
|
||||
options[1].clone()
|
||||
} else {
|
||||
current.clone()
|
||||
};
|
||||
Some(SeriesCandidates::Choose {
|
||||
options,
|
||||
default,
|
||||
fallback: current,
|
||||
})
|
||||
}
|
||||
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 {
|
||||
Ok(options) if !options.is_empty() => Some(SeriesCandidates::Choose {
|
||||
options,
|
||||
default: current.clone(),
|
||||
fallback: current,
|
||||
}),
|
||||
// An empty list offers nothing to choose from
|
||||
_ => Some(SeriesCandidates::Keep(current)),
|
||||
}
|
||||
}
|
||||
Err(_) => Some(SeriesCandidates::Keep(current)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
@@ -306,6 +378,82 @@ mod tests {
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// An UNRELEASED changelog offers UNRELEASED pinned first, the
|
||||
/// development series as the default, and itself as the fallback.
|
||||
#[tokio::test]
|
||||
async fn series_candidates_unreleased_pins_entry_and_defaults_to_dev() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("changelog");
|
||||
std::fs::write(
|
||||
&path,
|
||||
"hello (1.0-1) UNRELEASED; 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,
|
||||
default,
|
||||
fallback,
|
||||
}) => {
|
||||
assert_eq!(options[0], "UNRELEASED");
|
||||
assert!(options.len() > 1, "the vendor series list is offered");
|
||||
assert_eq!(default, options[1]);
|
||||
assert_eq!(fallback, "UNRELEASED");
|
||||
}
|
||||
other => panic!("expected Choose, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A released changelog offers its distribution's series with the
|
||||
/// current one preselected. Uses a series of the host vendor so the
|
||||
/// test only relies on the local distro-info data.
|
||||
#[tokio::test]
|
||||
async fn series_candidates_released_defaults_to_current_series() {
|
||||
let dist = crate::build::env::current_vendor().to_lowercase();
|
||||
let vendor_series = crate::distro_info::get_ordered_series_name(&dist)
|
||||
.await
|
||||
.expect("the host vendor's series data resolves");
|
||||
// Any released series of the vendor works; the changelog names it.
|
||||
let current = vendor_series.last().expect("non-empty series list");
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("changelog");
|
||||
std::fs::write(
|
||||
&path,
|
||||
format!(
|
||||
"hello (1.0-1) {current}; 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,
|
||||
default,
|
||||
fallback,
|
||||
}) => {
|
||||
assert_eq!(options, vendor_series);
|
||||
assert_eq!(default, *current);
|
||||
assert_eq!(fallback, *current);
|
||||
}
|
||||
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() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
assert!(
|
||||
series_candidates(&dir.path().join("changelog"))
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
fn setup_repo(dir: &Path) {
|
||||
Command::new("git")
|
||||
.arg("init")
|
||||
|
||||
Reference in New Issue
Block a user