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:
2026-09-18 20:51:36 +02:00
parent 0421a91e01
commit c2dae4f3f9
5 changed files with 325 additions and 120 deletions
+31 -78
View File
@@ -353,15 +353,14 @@ fn main() {
let (pb, progress_callback) = pkh::ui::create_progress_bar(&multi);
// Convert PPA to base URL if provided
let base_url = ppa.map(|ppa_str| {
// PPA format: user/ppa_name
let parts: Vec<&str> = ppa_str.split('/').collect();
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
error!("Invalid PPA format: '{}'. Expected: user/ppa_name", ppa_str);
let base_url = match ppa.map(pkh::package_info::split_ppa) {
Some(Ok((user, name))) => Some(pkh::package_info::ppa_to_base_url(user, name)),
Some(Err(e)) => {
error!("{e}");
std::process::exit(1);
}
pkh::package_info::ppa_to_base_url(parts[0], parts[1])
});
None => None,
};
// Since pull is async, we need to block on it
if let Err(e) = rt.block_on(async {
@@ -395,62 +394,27 @@ fn main() {
let target_series = if let Some(s) = cli_series {
Some(s.to_string())
} else {
// Parse current changelog to determine the default series
let changelog_path = cwd.join("debian/changelog");
match pkh::changelog::parse_changelog_header(&changelog_path) {
Ok((_pkg, _ver, current_series)) => {
// UNRELEASED is not a real series: offer it as a
// pinned first entry (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.
match rt.block_on(async {
if pkh::distro_info::is_unreleased(&current_series) {
// Vendors keep original casing ("Ubuntu"),
// while the series data keys are lowercase
let dist = pkh::build::env::current_vendor().to_lowercase();
let mut series_list =
vec![pkh::distro_info::UNRELEASED.to_string()];
series_list.extend(
pkh::distro_info::get_ordered_series_name(&dist).await?,
);
Ok(series_list)
} else {
let dist =
pkh::distro_info::get_dist_from_series(&current_series).await?;
pkh::distro_info::get_ordered_series_name(&dist).await
}
}) {
Ok(series_list) => {
// Default to the development series (the
// first real entry) when the changelog is
// UNRELEASED, not to the pinned entry itself
let default = if pkh::distro_info::is_unreleased(&current_series)
&& series_list.len() > 1
{
series_list[1].clone()
} else {
current_series.clone()
};
match pkh::ui::select_series(&series_list, &default) {
Ok(selected) => Some(selected),
Err(e) => {
error!(
"Series selection failed: {}. Using current series '{}' instead.",
e, current_series
);
Some(current_series)
}
}
}
Err(_) => {
// Could not fetch series list, use current series as default
Some(current_series)
}
match rt.block_on(pkh::changelog::series_candidates(&changelog_path)) {
Some(pkh::changelog::SeriesCandidates::Choose {
options,
default,
fallback,
}) => match pkh::ui::select_series(&options, &default) {
Ok(selected) => Some(selected),
Err(e) => {
error!(
"Series selection failed: {}. Using current series '{}' instead.",
e, fallback
);
Some(fallback)
}
}
Err(_) => None,
},
// Could not fetch the series list: use the current series
Some(pkh::changelog::SeriesCandidates::Keep(current)) => Some(current),
// No parsable changelog: leave the series decision to
// generate_entry
None => None,
}
};
@@ -668,24 +632,13 @@ fn main() {
std::process::exit(1);
});
// Parse host, user, port from endpoint
// Formats: [ssh://][user@]host[:port]
let endpoint_re = regex::Regex::new(r"^(?:ssh://)?(?:(?P<user>[^@]+)@)?(?P<host>[^:/]+)(?::(?P<port>\d+))?$").unwrap();
let endpoint_cap = endpoint_re.captures(endpoint).unwrap_or_else(|| {
error!("Invalid endpoint format: '{}'. Expected [ssh://][user@]host[:port]", endpoint);
std::process::exit(1);
});
let host = endpoint_cap.name("host").unwrap().as_str().to_string();
let user = endpoint_cap.name("user").map(|m| m.as_str().to_string());
let port = endpoint_cap.name("port").map(|m| {
m.as_str().parse::<u16>().unwrap_or_else(|_| {
error!("Invalid port number");
match pkh::context::ContextConfig::from_endpoint(endpoint) {
Ok(config) => config,
Err(e) => {
error!("{e}");
std::process::exit(1);
})
});
ContextConfig::Ssh { host, user, port }
}
}
}
_ => {
error!("Unknown context type: {}", type_str);