diff --git a/src/changelog.rs b/src/changelog.rs index 4afd58d..ed32e66 100644 --- a/src/changelog.rs +++ b/src/changelog.rs @@ -44,9 +44,12 @@ pub enum EntryKind { /// `1.0-1ubuntu1`. Library-only: the CLI has no flag selecting it, /// and targeting an Ubuntu series already picks this numbering. Ubuntu, - /// A backport: `1.0-1` becomes `1.0-1~bpo12+1`, where 12 is the Debian - /// release number of the target series (derived from it: the series - /// must therefore be a numbered Debian release, e.g. not sid) + /// A backport: numbered after the vendor of the target series — + /// Debian's backports scheme (`1.0-1` becomes `1.0-1~bpo12+1`) or + /// Ubuntu's per-release SRU scheme (`3.1-1ubuntu2` becomes + /// `3.1-1ubuntu2~24.04.1`). The release number is derived from the + /// target series, which must therefore be a numbered release (e.g. + /// not sid). Backport, } @@ -97,8 +100,8 @@ pub async fn generate_entry( EntryKind::Rebuild => compute_new_version(&old_version, Bump::Rebuild)?, EntryKind::Ubuntu => compute_new_version(&old_version, Bump::Ubuntu)?, EntryKind::Backport => { - let number = backport_series_number(&series).await?; - compute_new_version(&old_version, Bump::Backport(number))? + let suffix = backport_suffix_for_series(&series).await?; + compute_new_version(&old_version, Bump::Backport(suffix))? } } }; @@ -138,8 +141,10 @@ enum Bump { Nmu, /// No-change rebuild: `1.0-1` becomes `1.0-1build1` Rebuild, - /// Backport to the Debian release whose series number is carried - /// (`"12"` for bookworm): `1.0-1` becomes `1.0-1~bpo12+1` + /// Backport to the release whose version suffix is carried + /// (`"~bpo12+"` for Debian bookworm, `"~24.04."` for Ubuntu noble): + /// `1.0-1` becomes `1.0-1~bpo12+1`, `3.1-1ubuntu2` becomes + /// `3.1-1ubuntu2~24.04.1` Backport(String), } @@ -159,11 +164,12 @@ fn compute_new_version( increment_suffix(old_version, "+nmu") } } - // A re-backport of the same release reuses its `~bpoNN+M` counter, - // incrementing M: increment_suffix appends a fresh `~bpoNN+1` when - // the version carries no such suffix yet (including when it is a - // backport of another release, whose counter is left untouched) - Bump::Backport(number) => increment_suffix(old_version, &format!("~bpo{}+", number)), + // A re-backport of the same release reuses its suffix counter, + // incrementing the trailing number: increment_suffix appends the + // suffix with a fresh 1 when the version carries no such suffix + // yet (including when it is a backport of another release, whose + // counter is left untouched) + Bump::Backport(suffix) => increment_suffix(old_version, &suffix), Bump::Normal => increment_suffix(old_version, ""), } } @@ -180,28 +186,47 @@ async fn normal_bump_for_series(series: &str) -> Bump { } } -/// The Debian release number (e.g. `"12"` for bookworm) that backport -/// versions are numbered after (`~bpo12+1`), derived from the target series; -/// a backport suite name (`bookworm-backports`) is accepted too. Errors when -/// the series has no numeric Debian release number (sid, an Ubuntu series, -/// UNRELEASED, unknown): the numbering cannot be derived for it. -async fn backport_series_number(series: &str) -> Result> { +/// The version suffix that backport versions are numbered with, derived +/// from the vendor of the target series: Debian backports follow the +/// backports.debian.org scheme (`~bpo12+1`), Ubuntu backports the +/// per-release SRU scheme of the Ubuntu version-strings documentation +/// (`3.1-1ubuntu2~22.04.1` for a development version backported to 22.04). +/// A backport suite name (`bookworm-backports`) is accepted too. Errors +/// when the series has no usable release number (Debian sid or +/// experimental, unknown series): the numbering cannot be derived for it. +async fn backport_suffix_for_series(series: &str) -> Result> { let base = series .strip_suffix("-backports-sloppy") .or_else(|| series.strip_suffix("-backports")) .unwrap_or(series); - crate::distro_info::get_debian_series_number(base) - .await? - .filter(|number| !number.is_empty() && number.chars().all(|c| c.is_ascii_digit())) - .ok_or_else(|| { - format!( - "Could not determine the Debian release number of series '{series}', \ - needed to number the backport version (as in 1.0-1~bpo12+1). \ - Target a numbered Debian release (e.g. --series bookworm) \ - or pass the version explicitly with --version." - ) - .into() - }) + let unnumbered = || { + format!( + "Could not determine the release number of series '{series}', \ + needed to number the backport version (Debian ~bpo12+1, \ + Ubuntu ~24.04.1). Target a released Debian or Ubuntu series \ + or pass the version explicitly with --version." + ) + }; + let Some((dist, number)) = crate::distro_info::get_series_release_number(base).await? else { + return Err(unnumbered().into()); + }; + match dist.as_str() { + // backports.debian.org numbers with the plain integer release number + "debian" if !number.is_empty() && number.chars().all(|c| c.is_ascii_digit()) => { + Ok(format!("~bpo{}+", number)) + } + // Ubuntu numbers per release as YY.MM; the trailing `.N` counts + // the per-release SRU uploads + "ubuntu" + if number.split('.').count() == 2 + && number + .split('.') + .all(|part| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit())) => + { + Ok(format!("~{}.", number)) + } + _ => Err(unnumbered().into()), + } } /// Increment a version number by 1, for a given suffix @@ -572,6 +597,12 @@ mod tests { ); } + /// Serializes the tests that mutate the process-global DEBFULLNAME / + /// DEBEMAIL variables: run in parallel, they otherwise race each + /// other's identity reads and assertions. + static IDENTITY_LOCK: std::sync::LazyLock> = + std::sync::LazyLock::new(|| tokio::sync::Mutex::new(())); + fn setup_repo(dir: &Path) { Command::new("git") .arg("init") @@ -641,6 +672,7 @@ mod tests { commit(repo_dir, "Add feature B"); // Generate entry + let _identity = IDENTITY_LOCK.lock().await; unsafe { std::env::set_var("DEBFULLNAME", "Maintainer Maintainer"); std::env::set_var("DEBEMAIL", "maintainer@maintainer.com"); @@ -730,27 +762,47 @@ mod tests { "1.0-1.3" ); - // Backport + // Backport, Debian scheme assert_eq!( - compute_new_version("1.0-1", Bump::Backport("12".to_string())).unwrap(), + compute_new_version("1.0-1", Bump::Backport("~bpo12+".to_string())).unwrap(), "1.0-1~bpo12+1" ); // Re-backporting the same source reuses the counter assert_eq!( - compute_new_version("1.0-1~bpo12+1", Bump::Backport("12".to_string())).unwrap(), + compute_new_version("1.0-1~bpo12+1", Bump::Backport("~bpo12+".to_string())).unwrap(), "1.0-1~bpo12+2" ); // Native packages backport too assert_eq!( - compute_new_version("1.0", Bump::Backport("12".to_string())).unwrap(), + compute_new_version("1.0", Bump::Backport("~bpo12+".to_string())).unwrap(), "1.0~bpo12+1" ); // A version carrying another release's counter gains a fresh one assert_eq!( - compute_new_version("1.0-1~bpo11+1", Bump::Backport("12".to_string())).unwrap(), + compute_new_version("1.0-1~bpo11+1", Bump::Backport("~bpo12+".to_string())).unwrap(), "1.0-1~bpo11+1~bpo12+1" ); + // Backport, Ubuntu scheme (Ubuntu version-strings documentation) + assert_eq!( + compute_new_version("3.1-1ubuntu2", Bump::Backport("~22.04.".to_string())).unwrap(), + "3.1-1ubuntu2~22.04.1" + ); + // Subsequent per-release SRU uploads increment the counter + assert_eq!( + compute_new_version( + "3.1-1ubuntu2~22.04.1", + Bump::Backport("~22.04.".to_string()) + ) + .unwrap(), + "3.1-1ubuntu2~22.04.2" + ); + // Native packages + assert_eq!( + compute_new_version("3.1", Bump::Backport("~22.04.".to_string())).unwrap(), + "3.1~22.04.1" + ); + // Native package uploads assert_eq!(compute_new_version("1.0", Bump::Normal).unwrap(), "1.1"); assert_eq!(compute_new_version("1.0.5", Bump::Normal).unwrap(), "1.0.6"); @@ -794,6 +846,7 @@ mod tests { ) .unwrap(); + let _identity = IDENTITY_LOCK.lock().await; unsafe { std::env::set_var("DEBFULLNAME", "Maintainer Maintainer"); std::env::set_var("DEBEMAIL", "maintainer@maintainer.com"); @@ -818,9 +871,9 @@ mod tests { assert!(content.contains("mypackage (1.0-1~bpo12+1) bookworm; urgency=medium")); } - /// Backport numbering needs a numeric Debian release number: series - /// without one (Ubuntu series, sid, ...) are rejected before anything - /// is written, pointing at --series/--version instead. + /// Backport numbering needs a usable release number: Debian's rolling + /// series (sid) carry none and are rejected before anything is written, + /// pointing at --series/--version instead. #[tokio::test] async fn test_generate_entry_backport_requires_numbered_series() { let temp_dir = TempDir::new().unwrap(); @@ -837,7 +890,7 @@ mod tests { "debian/changelog", Some(repo_dir), None, - Some("noble"), + Some("sid"), EntryKind::Backport, ) .await; @@ -848,6 +901,40 @@ mod tests { assert!(content.starts_with("mypackage (1.0-1) unstable")); } + /// Ubuntu backports follow the per-release SRU scheme of the Ubuntu + /// version-strings documentation: the development release's version + /// with ~YY.MM.1 appended, independently of what the target release + /// carries. Relies on the host distro-info data mapping noble to + /// 24.04 (see the distro_info tests). + #[tokio::test] + async fn test_generate_entry_backport_ubuntu_numbering() { + let temp_dir = TempDir::new().unwrap(); + let repo_dir = temp_dir.path(); + setup_repo(repo_dir); + let changelog_path = repo_dir.join("debian/changelog"); + std::fs::create_dir_all(repo_dir.join("debian")).unwrap(); + std::fs::write( + &changelog_path, + "mypackage (3.1-1ubuntu2) questing; urgency=medium\n\n * Initial release\n\n -- Maintainer Wed, 01 Jan 2020 00:00:00 +0000\n", + ) + .unwrap(); + + let entry = generate_entry( + "debian/changelog", + Some(repo_dir), + None, + Some("noble"), + EntryKind::Backport, + ) + .await + .unwrap(); + + assert_eq!(entry.new_version, "3.1-1ubuntu2~24.04.1"); + assert_eq!(entry.series, "noble"); + let content = std::fs::read_to_string(&changelog_path).unwrap(); + assert!(content.contains("mypackage (3.1-1ubuntu2~24.04.1) noble; urgency=medium")); + } + /// A regular upload is numbered after the vendor of its target series: /// a Debian-style version uploaded to an Ubuntu series gains the /// ubuntu1 suffix, and re-bumping the now-Ubuntu changelog increments @@ -895,6 +982,7 @@ mod tests { #[test] fn test_get_maintainer_info() { + let _identity = IDENTITY_LOCK.blocking_lock(); // Test with env vars unsafe { std::env::set_var("DEBFULLNAME", "Env Name"); diff --git a/src/distro_info.rs b/src/distro_info.rs index 76ef70a..11b15ac 100644 --- a/src/distro_info.rs +++ b/src/distro_info.rs @@ -712,6 +712,30 @@ pub async fn get_debian_series_number(series: &str) -> Result, Bo Ok(None) } +/// The release number of a distribution series, paired with the dist it +/// belongs to: the version column of the series data, stripped to its +/// leading token ("12" for Debian bookworm, "26.04" out of Ubuntu +/// resolute's "26.04 LTS"). `None` when the series carries no version at +/// all (Debian's rolling sid/experimental have an empty column; +/// pseudo-versions like "unstable" pass through, callers validate per +/// vendor). Errors when no known distribution carries the series. +pub async fn get_series_release_number( + series: &str, +) -> Result, Box> { + let dist = get_dist_from_series(series).await?; + for info in get_ordered_series(&dist).await? { + if info.series == series { + let number = info + .version + .as_deref() + .and_then(|version| version.split_whitespace().next()) + .map(str::to_string); + return Ok(number.map(|number| (dist, number))); + } + } + Ok(None) +} + #[cfg(test)] mod tests { use super::*; @@ -1033,6 +1057,25 @@ mod tests { assert!(unknown_number.is_none()); } + #[tokio::test] + async fn test_get_series_release_number() { + let (dist, bookworm) = get_series_release_number("bookworm") + .await + .unwrap() + .unwrap(); + assert_eq!(dist, "debian"); + assert_eq!(bookworm, "12"); + + // Ubuntu LTS rows carry a " LTS" decoration: only the leading + // YY.MM token is the release number + let (dist, noble) = get_series_release_number("noble").await.unwrap().unwrap(); + assert_eq!(dist, "ubuntu"); + assert_eq!(noble, "24.04"); + + // No known dist carries the series + assert!(get_series_release_number("not-a-series").await.is_err()); + } + #[tokio::test] async fn test_get_keyring_urls_sid() { // Test that 'sid' returns keyrings from the 3 latest released versions diff --git a/src/main.rs b/src/main.rs index e59691f..3f1aeaa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -176,7 +176,7 @@ fn main() { Command::new("chlog") .about("Auto-generate changelog entry, editing it, committing it afterwards") .arg(arg!(-s --series "Target distribution series").required(false)) - .arg(arg!(--backport "Number the entry as a backport of the target series (1.0-1 becomes 1.0-1~bpo12+1)").required(false) + .arg(arg!(--backport "Number the entry as a backport of the target series (Debian: 1.0-1 becomes 1.0-1~bpo12+1; Ubuntu: 3.1-1ubuntu2 becomes 3.1-1ubuntu2~24.04.1)").required(false) .conflicts_with_all(["nmu", "rebuild"])) .arg(arg!(--nmu "Number the entry as a non-maintainer upload (1.0-1 becomes 1.0-1.1, native 1.0 becomes 1.0+nmu1)").required(false) .conflicts_with("rebuild"))