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.
665 lines
22 KiB
Rust
665 lines
22 KiB
Rust
use chrono::Local;
|
|
use git2::{Oid, Repository, Sort};
|
|
use regex::Regex;
|
|
use std::fs::File;
|
|
use std::io::{Read, Write};
|
|
use std::path::Path;
|
|
|
|
/// Automatically generate a changelog entry from a commit history and previous changelog
|
|
pub fn generate_entry(
|
|
changelog_file: &str,
|
|
cwd: Option<&Path>,
|
|
user_version: Option<&str>,
|
|
target_series: Option<&str>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let changelog_path = if let Some(path) = cwd {
|
|
path.join(changelog_file)
|
|
} else {
|
|
Path::new(changelog_file).to_path_buf()
|
|
};
|
|
|
|
// Parse existing changelog to get current (old) version
|
|
let (package, old_version, series) = parse_changelog_header(&changelog_path)?;
|
|
log::info!("Found package: {}, version: {}", package, old_version);
|
|
|
|
// Open git repo, and find commits since last version tag
|
|
let repo_path = if let Some(path) = cwd {
|
|
path.to_path_buf()
|
|
} else {
|
|
std::env::current_dir()?
|
|
};
|
|
let commits = match Repository::open(&repo_path) {
|
|
Ok(repo) => get_commits_since_version(&repo, &old_version)?,
|
|
// If there is no git repo (e.g. package downloaded from archive),
|
|
// we just generate an empty list of changes
|
|
Err(_e) => Vec::new(),
|
|
};
|
|
|
|
// Compute new version if needed, or use user-supplied one
|
|
let new_version = if let Some(version) = user_version {
|
|
version.to_string()
|
|
} else {
|
|
// TODO: Pass these flags from CLI
|
|
compute_new_version(&old_version, false, false, false)?
|
|
};
|
|
|
|
let (maintainer_name, maintainer_email) = get_maintainer_info()?;
|
|
let series = target_series.unwrap_or(&series).to_string();
|
|
let new_entry = format_entry(
|
|
&package,
|
|
&new_version,
|
|
&series,
|
|
&commits,
|
|
&maintainer_name,
|
|
&maintainer_email,
|
|
);
|
|
|
|
prepend_to_file(&changelog_path, &new_entry)?;
|
|
|
|
log::info!("Added new changelog entry to {}", changelog_path.display());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Compute the next (most probable) version number of a package, from old version and
|
|
/// conditions on changes (is ubuntu upload, is a no change rebuild, is a non-maintainer upload)
|
|
fn compute_new_version(
|
|
old_version: &str,
|
|
is_ubuntu: bool,
|
|
is_rebuild: bool,
|
|
is_nmu: bool,
|
|
) -> Result<String, Box<dyn std::error::Error>> {
|
|
if is_ubuntu {
|
|
return increment_suffix(old_version, "ubuntu");
|
|
}
|
|
if is_rebuild {
|
|
return increment_suffix(old_version, "build");
|
|
}
|
|
if is_nmu {
|
|
if !old_version.contains('-') {
|
|
return increment_suffix(old_version, "+nmu");
|
|
} else {
|
|
return increment_suffix(old_version, ".");
|
|
}
|
|
}
|
|
increment_suffix(old_version, "")
|
|
}
|
|
|
|
/// Increment a version number by 1, for a given suffix
|
|
fn increment_suffix(version: &str, suffix: &str) -> Result<String, Box<dyn std::error::Error>> {
|
|
// If suffix is empty, we just look for trailing digits
|
|
// If suffix is not empty, we look for suffix followed by digits
|
|
|
|
let pattern = if suffix.is_empty() {
|
|
r"(\d+)$".to_string()
|
|
} else {
|
|
format!(r"{}(\d+)$", regex::escape(suffix))
|
|
};
|
|
|
|
let re = Regex::new(&pattern).unwrap();
|
|
|
|
if let Some(caps) = re.captures(version) {
|
|
let num_str = caps.get(1).unwrap().as_str();
|
|
// Parse as u64 so that large trailing numbers (e.g. date-based
|
|
// versions like '1.0-20250123123456', which do not fit in a u32)
|
|
// still increment normally
|
|
let num: u64 = num_str.parse().map_err(|_| {
|
|
format!(
|
|
"Cannot increment version '{version}': trailing number '{num_str}' \
|
|
is too large to be incremented. Specify a version explicitly instead."
|
|
)
|
|
})?;
|
|
let range = caps.get(1).unwrap().range();
|
|
let new_num = num.checked_add(1).ok_or_else(|| {
|
|
format!(
|
|
"Cannot increment version '{version}': trailing number {num} \
|
|
is too large to be incremented. Specify a version explicitly instead."
|
|
)
|
|
})?;
|
|
let mut new_ver = version.to_string();
|
|
new_ver.replace_range(range, &new_num.to_string());
|
|
return Ok(new_ver);
|
|
}
|
|
|
|
// If pattern not found, append suffix + "1"
|
|
// But if suffix is empty, we default to appending "-1" (standard Debian revision start)
|
|
if suffix.is_empty() {
|
|
Ok(format!("{}-1", version))
|
|
} else {
|
|
Ok(format!("{}{}{}", version, suffix, 1))
|
|
}
|
|
}
|
|
|
|
/// Parse a changelog file first entry header
|
|
/// Returns (package, version, series) tuple from the last modification entry
|
|
pub fn parse_changelog_header(
|
|
path: &Path,
|
|
) -> Result<(String, String, String), Box<dyn std::error::Error>> {
|
|
let entry = crate::debian::parse_changelog_entry(path)?;
|
|
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>> {
|
|
let entry = crate::debian::parse_changelog_entry(path)?;
|
|
Ok((entry.maintainer_name, entry.maintainer_email))
|
|
}
|
|
|
|
/*
|
|
* Obtain all commit messages as a list since a tagged version in a git repository
|
|
*/
|
|
fn get_commits_since_version(
|
|
repo: &Repository,
|
|
version: &str,
|
|
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
|
let mut revwalk = repo.revwalk()?;
|
|
revwalk.set_sorting(Sort::TIME)?;
|
|
|
|
// We are looking for debian version numbers
|
|
// Depending on the repo, they could be tagged with the following formats
|
|
let tag_names = vec![
|
|
format!("debian/{}", version),
|
|
format!("debian/v{}", version),
|
|
format!("v{}", version),
|
|
version.to_string(),
|
|
];
|
|
|
|
// Look for the different tag formats in the git repository
|
|
let mut tag_id: Option<Oid> = None;
|
|
for tag_name in tag_names {
|
|
if let Ok(r) = repo.revparse_single(&tag_name) {
|
|
// If we found either a commit or a tag matching the name,
|
|
// we have found our version
|
|
if let Some(commit) = r.as_commit() {
|
|
tag_id = Some(commit.id());
|
|
break;
|
|
} else if let Some(tag) = r.as_tag() {
|
|
tag_id = Some(tag.target_id());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(tid) = tag_id {
|
|
revwalk.push_head()?;
|
|
revwalk.hide(tid)?;
|
|
} else {
|
|
// Tag not found...
|
|
// We return an empty list.
|
|
// TODO: Can we do better?
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
// Add all commit messages from that tagged version to head in the list
|
|
let mut commits = Vec::new();
|
|
for id in revwalk {
|
|
let id = id?;
|
|
let commit = repo.find_commit(id)?;
|
|
let message = commit.message().unwrap_or("").trim();
|
|
let summary = message.lines().next().unwrap_or("").to_string();
|
|
if !summary.is_empty() {
|
|
commits.push(summary);
|
|
}
|
|
}
|
|
|
|
Ok(commits)
|
|
}
|
|
|
|
/*
|
|
* Create a changelog entry from information, i.e. format that information
|
|
* into a changelog entry
|
|
*/
|
|
fn format_entry(
|
|
package: &str,
|
|
version: &str,
|
|
series: &str,
|
|
changes: &[String],
|
|
maintainer_name: &str,
|
|
maintainer_email: &str,
|
|
) -> String {
|
|
let mut entry = String::new();
|
|
|
|
// Header: package, version and distribution series
|
|
entry.push_str(&format!(
|
|
"{} ({}) {}; urgency=medium\n\n",
|
|
package, version, series
|
|
));
|
|
|
|
// Changes
|
|
for change in changes {
|
|
entry.push_str(&format!(" * {}\n", change));
|
|
}
|
|
if changes.is_empty() {
|
|
entry.push_str(" * \n");
|
|
}
|
|
|
|
// Footer: date, maintainer
|
|
let date = Local::now().format("%a, %d %b %Y %H:%M:%S %z").to_string();
|
|
entry.push_str(&format!(
|
|
"\n -- {} <{}> {}\n\n",
|
|
maintainer_name, maintainer_email, date
|
|
));
|
|
|
|
entry
|
|
}
|
|
|
|
/*
|
|
* Add content to the beginning of a file
|
|
*/
|
|
fn prepend_to_file(path: &Path, content: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut file = File::open(path)?;
|
|
let mut existing_content = String::new();
|
|
file.read_to_string(&mut existing_content)?;
|
|
|
|
let mut file = File::create(path)?;
|
|
file.write_all(content.as_bytes())?;
|
|
file.write_all(existing_content.as_bytes())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Discover the maintainer identity for changelog entries and package
|
|
/// scaffolding: `$DEBFULLNAME`/`$DEBEMAIL` when both are set, else the git
|
|
/// configuration (`user.name`/`user.email`).
|
|
///
|
|
/// Returns `(name, email)`, with a pointed error telling the user how to
|
|
/// configure the missing piece.
|
|
pub fn get_maintainer_info() -> Result<(String, String), Box<dyn std::error::Error>> {
|
|
// From environment variables
|
|
if let (Ok(name), Ok(email)) = (std::env::var("DEBFULLNAME"), std::env::var("DEBEMAIL")) {
|
|
return Ok((name, email));
|
|
}
|
|
|
|
// From git config
|
|
let config = git2::Config::open_default().map_err(|e| {
|
|
format!(
|
|
"Could not determine maintainer information. \
|
|
Neither $DEBFULLNAME/$DEBEMAIL nor git configuration is available: {}. \
|
|
Set the DEBFULLNAME and DEBEMAIL environment variables, \
|
|
or configure git with `git config --global user.name` and `git config --global user.email`.",
|
|
e
|
|
)
|
|
})?;
|
|
let name = config.get_string("user.name").map_err(|e| {
|
|
format!(
|
|
"Could not find git 'user.name' configuration: {}. \
|
|
Set it with `git config --global user.name \"Your Name\"` \
|
|
or define the DEBFULLNAME environment variable.",
|
|
e
|
|
)
|
|
})?;
|
|
let email = config.get_string("user.email").map_err(|e| {
|
|
format!(
|
|
"Could not find git 'user.email' configuration: {}. \
|
|
Set it with `git config --global user.email \"you@example.com\"` \
|
|
or define the DEBEMAIL environment variable.",
|
|
e
|
|
)
|
|
})?;
|
|
Ok((name, email))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
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")
|
|
.current_dir(dir)
|
|
.output()
|
|
.unwrap();
|
|
Command::new("git")
|
|
.arg("config")
|
|
.arg("user.email")
|
|
.arg("you@example.com")
|
|
.current_dir(dir)
|
|
.output()
|
|
.unwrap();
|
|
Command::new("git")
|
|
.arg("config")
|
|
.arg("user.name")
|
|
.arg("Your Name")
|
|
.current_dir(dir)
|
|
.output()
|
|
.unwrap();
|
|
}
|
|
|
|
fn commit(dir: &Path, message: &str) {
|
|
Command::new("git")
|
|
.arg("commit")
|
|
.arg("--allow-empty")
|
|
.arg("-m")
|
|
.arg(message)
|
|
.current_dir(dir)
|
|
.output()
|
|
.unwrap();
|
|
}
|
|
|
|
fn tag(dir: &Path, name: &str) {
|
|
Command::new("git")
|
|
.arg("tag")
|
|
.arg(name)
|
|
.current_dir(dir)
|
|
.output()
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_entry() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let repo_dir = temp_dir.path();
|
|
setup_repo(repo_dir);
|
|
|
|
// Create initial changelog
|
|
let changelog_path = repo_dir.join("debian/changelog");
|
|
std::fs::create_dir_all(repo_dir.join("debian")).unwrap();
|
|
let initial_content = "mypackage (0.1.0-1) unstable; urgency=medium\n\n * Initial release\n\n -- Maintainer <m@e.com> Wed, 01 Jan 2020 00:00:00 +0000\n";
|
|
std::fs::write(&changelog_path, initial_content).unwrap();
|
|
|
|
// Commit and tag
|
|
Command::new("git")
|
|
.arg("add")
|
|
.arg(".")
|
|
.current_dir(repo_dir)
|
|
.output()
|
|
.unwrap();
|
|
commit(repo_dir, "Initial commit");
|
|
tag(repo_dir, "debian/0.1.0-1");
|
|
|
|
// New commits
|
|
commit(repo_dir, "Fix bug A");
|
|
commit(repo_dir, "Add feature B");
|
|
|
|
// Generate entry
|
|
unsafe {
|
|
std::env::set_var("DEBFULLNAME", "Maintainer Maintainer");
|
|
std::env::set_var("DEBEMAIL", "maintainer@maintainer.com");
|
|
}
|
|
generate_entry("debian/changelog", Some(repo_dir), None, None).unwrap();
|
|
unsafe {
|
|
std::env::remove_var("DEBFULLNAME");
|
|
std::env::remove_var("DEBEMAIL");
|
|
}
|
|
|
|
// Verify content
|
|
let content = std::fs::read_to_string(&changelog_path).unwrap();
|
|
println!("{}", content);
|
|
|
|
assert!(content.contains("mypackage (0.1.0-2) unstable; urgency=medium"));
|
|
assert!(content.contains("* Fix bug A"));
|
|
assert!(content.contains("* Add feature B"));
|
|
assert!(content.contains(" -- Maintainer Maintainer <maintainer@maintainer.com> "));
|
|
// Should still contain old content
|
|
assert!(content.contains("mypackage (0.1.0-1) unstable; urgency=medium"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_compute_new_version() {
|
|
// Debian upload
|
|
assert_eq!(
|
|
compute_new_version("15.2.0-8", false, false, false).unwrap(),
|
|
"15.2.0-9"
|
|
);
|
|
assert_eq!(
|
|
compute_new_version("15.2.0-9", false, false, false).unwrap(),
|
|
"15.2.0-10"
|
|
);
|
|
|
|
// Ubuntu upload
|
|
assert_eq!(
|
|
compute_new_version("15.2.0-9", true, false, false).unwrap(),
|
|
"15.2.0-9ubuntu1"
|
|
);
|
|
assert_eq!(
|
|
compute_new_version("15.2.0-9ubuntu1", true, false, false).unwrap(),
|
|
"15.2.0-9ubuntu2"
|
|
);
|
|
|
|
// No change rebuild
|
|
assert_eq!(
|
|
compute_new_version("15.2.0-9", false, true, false).unwrap(),
|
|
"15.2.0-9build1"
|
|
);
|
|
assert_eq!(
|
|
compute_new_version("15.2.0-9build1", false, true, false).unwrap(),
|
|
"15.2.0-9build2"
|
|
);
|
|
|
|
// Rebuild of Ubuntu version
|
|
assert_eq!(
|
|
compute_new_version("15.2.0-9ubuntu1", false, true, false).unwrap(),
|
|
"15.2.0-9ubuntu1build1"
|
|
);
|
|
|
|
// NMU
|
|
// Native
|
|
assert_eq!(
|
|
compute_new_version("1.0", false, false, true).unwrap(),
|
|
"1.0+nmu1"
|
|
);
|
|
assert_eq!(
|
|
compute_new_version("1.0+nmu1", false, false, true).unwrap(),
|
|
"1.0+nmu2"
|
|
);
|
|
|
|
// Non-native
|
|
assert_eq!(
|
|
compute_new_version("1.0-1", false, false, true).unwrap(),
|
|
"1.0-1.1"
|
|
);
|
|
assert_eq!(
|
|
compute_new_version("1.0-1.1", false, false, true).unwrap(),
|
|
"1.0-1.2"
|
|
);
|
|
|
|
// NMU of NMU?
|
|
assert_eq!(
|
|
compute_new_version("1.0-1.2", false, false, true).unwrap(),
|
|
"1.0-1.3"
|
|
);
|
|
|
|
// Native package uploads
|
|
assert_eq!(
|
|
compute_new_version("1.0", false, false, false).unwrap(),
|
|
"1.1"
|
|
);
|
|
assert_eq!(
|
|
compute_new_version("1.0.5", false, false, false).unwrap(),
|
|
"1.0.6"
|
|
);
|
|
assert_eq!(
|
|
compute_new_version("20241126", false, false, false).unwrap(),
|
|
"20241127"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compute_new_version_large_trailing_number() {
|
|
// Date-based versions with a trailing number larger than u32::MAX
|
|
// must increment normally (they fit in a u64)
|
|
assert_eq!(
|
|
compute_new_version("1.0-20250123123456", false, false, false).unwrap(),
|
|
"1.0-20250123123457"
|
|
);
|
|
|
|
// A number that does not even fit in a u64 yields a clear error
|
|
// instead of panicking
|
|
let err = compute_new_version("1.0-99999999999999999999999999", false, false, false);
|
|
assert!(err.is_err());
|
|
|
|
// u64::MAX itself cannot be incremented
|
|
let err = compute_new_version("1.0-18446744073709551615", false, false, false);
|
|
assert!(err.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_maintainer_info() {
|
|
// Test with env vars
|
|
unsafe {
|
|
std::env::set_var("DEBFULLNAME", "Env Name");
|
|
std::env::set_var("DEBEMAIL", "env@example.com");
|
|
}
|
|
|
|
let (name, email) = get_maintainer_info().unwrap();
|
|
assert_eq!(name, "Env Name");
|
|
assert_eq!(email, "env@example.com");
|
|
|
|
unsafe {
|
|
std::env::remove_var("DEBFULLNAME");
|
|
std::env::remove_var("DEBEMAIL");
|
|
}
|
|
}
|
|
}
|