format: run cargo fmt
All checks were successful
CI / build (push) Successful in 1m44s

This commit is contained in:
2025-11-30 12:51:52 +01:00
parent c813823a1a
commit 2cfbb69fe7
5 changed files with 491 additions and 232 deletions

View File

@@ -1,14 +1,18 @@
use chrono::Local;
use git2::{Oid, Repository, Sort};
use regex::Regex;
use std::fs::File;
use std::io::{self, BufRead, Read, Write};
use std::path::Path;
use regex::Regex;
use chrono::Local;
use git2::{Repository, Sort, Oid};
/*
* 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>) -> Result<(), Box<dyn std::error::Error>> {
pub fn generate_entry(
changelog_file: &str,
cwd: Option<&Path>,
user_version: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let changelog_path = if let Some(path) = cwd {
path.join(changelog_file)
} else {
@@ -28,19 +32,7 @@ pub fn generate_entry(changelog_file: &str, cwd: Option<&Path>, user_version: Op
let repo = Repository::open(&repo_path)?;
// 3. Find commits since the tag corresponding to the version
// We assume the tag format is "debian/<version>" or just "<version>" or "v<version>"
// But usually for Debian packages it might be "debian/<version>"
// Let's try to find a tag that matches.
// For now, let's assume the tag is simply the version string, or debian/version
// If we can't find a tag, we might have to error out or take all commits?
// Let's try to find the tag.
// Actually, usually we want to generate an entry for a NEW version based on changes since the OLD version.
// The `changelog_file` passed here is the EXISTING changelog.
// So `version` is the PREVIOUS version.
// We want to find commits since `version`.
let commits = get_commits_since_version(&repo, &old_version)?;
if commits.is_empty() {
@@ -48,20 +40,6 @@ pub fn generate_entry(changelog_file: &str, cwd: Option<&Path>, user_version: Op
// return Ok(());
}
// 4. Format the new entry
// We don't know the NEW version yet, so we might use "UNRELEASED" or increment the version.
// For now, let's use "UNRELEASED" and let the user edit it, or maybe we can try to increment it.
// The requirement says "Automatically generate a changelog entry".
// Usually tools like `dch` add a new entry.
// Let's create a new entry with "UNRELEASED" distribution and incremented version?
// Or just append to the top.
// Let's assume we want to output the new entry to stdout or prepend to file?
// The function signature returns (), so maybe it modifies the file.
// Let's prepend to the file.
// Compute new version if needed, or use user-supplied one
let new_version = if let Some(version) = user_version {
version.to_string()
@@ -69,12 +47,19 @@ pub fn generate_entry(changelog_file: &str, cwd: Option<&Path>, user_version: Op
// TODO: Pass these flags from CLI
compute_new_version(&old_version, false, false, false)
};
let (maintainer_name, maintainer_email) = get_maintainer_info()?;
let new_entry = format_entry(&package, &new_version, &series, &commits, &maintainer_name, &maintainer_email);
let new_entry = format_entry(
&package,
&new_version,
&series,
&commits,
&maintainer_name,
&maintainer_email,
);
prepend_to_file(&changelog_path, &new_entry)?;
println!("Added new changelog entry to {}", changelog_path.display());
Ok(())
@@ -84,7 +69,12 @@ pub fn generate_entry(changelog_file: &str, cwd: Option<&Path>, user_version: Op
* 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) -> String {
fn compute_new_version(
old_version: &str,
is_ubuntu: bool,
is_rebuild: bool,
is_nmu: bool,
) -> String {
if is_ubuntu {
return increment_suffix(old_version, "ubuntu");
}
@@ -107,15 +97,15 @@ fn compute_new_version(old_version: &str, is_ubuntu: bool, is_rebuild: bool, is_
fn increment_suffix(version: &str, suffix: &str) -> String {
// 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();
let num: u32 = num_str.parse().unwrap();
@@ -124,7 +114,7 @@ fn increment_suffix(version: &str, suffix: &str) -> String {
new_ver.replace_range(range, &(num + 1).to_string());
return 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() {
@@ -137,7 +127,9 @@ fn increment_suffix(version: &str, suffix: &str) -> String {
/*
* Parse a changelog file first entry header, to obtain (package, version, series)
*/
fn parse_changelog_header(path: &Path) -> Result<(String, String, String), Box<dyn std::error::Error>> {
fn parse_changelog_header(
path: &Path,
) -> Result<(String, String, String), Box<dyn std::error::Error>> {
let file = File::open(path)?;
let mut reader = io::BufReader::new(file);
let mut first_line = String::new();
@@ -158,7 +150,10 @@ fn parse_changelog_header(path: &Path) -> Result<(String, String, String), Box<d
/*
* 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>> {
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)?;
@@ -205,7 +200,7 @@ fn get_commits_since_version(repo: &Repository, version: &str) -> Result<Vec<Str
let message = commit.message().unwrap_or("").trim();
let summary = message.lines().next().unwrap_or("").to_string();
if !summary.is_empty() {
commits.push(summary);
commits.push(summary);
}
}
@@ -216,12 +211,22 @@ fn get_commits_since_version(repo: &Repository, version: &str) -> Result<Vec<Str
* 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 {
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));
// 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));
@@ -229,11 +234,14 @@ fn format_entry(package: &str, version: &str, series: &str, changes: &[String],
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.push_str(&format!(
"\n -- {} <{}> {}\n\n",
maintainer_name, maintainer_email, date
));
return entry;
}
@@ -244,11 +252,11 @@ fn prepend_to_file(path: &Path, content: &str) -> Result<(), Box<dyn std::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(())
}
@@ -272,17 +280,45 @@ mod tests {
use tempfile::TempDir;
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();
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();
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();
Command::new("git")
.arg("tag")
.arg(name)
.current_dir(dir)
.output()
.unwrap();
}
#[test]
@@ -298,7 +334,12 @@ mod tests {
std::fs::write(&changelog_path, initial_content).unwrap();
// Commit and tag
Command::new("git").arg("add").arg(".").current_dir(repo_dir).output().unwrap();
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");
@@ -320,7 +361,7 @@ mod tests {
// 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"));
@@ -332,36 +373,69 @@ mod tests {
#[test]
fn test_compute_new_version() {
// Debian upload
assert_eq!(compute_new_version("15.2.0-8", false, false, false), "15.2.0-9");
assert_eq!(compute_new_version("15.2.0-9", false, false, false), "15.2.0-10");
assert_eq!(
compute_new_version("15.2.0-8", false, false, false),
"15.2.0-9"
);
assert_eq!(
compute_new_version("15.2.0-9", false, false, false),
"15.2.0-10"
);
// Ubuntu upload
assert_eq!(compute_new_version("15.2.0-9", true, false, false), "15.2.0-9ubuntu1");
assert_eq!(compute_new_version("15.2.0-9ubuntu1", true, false, false), "15.2.0-9ubuntu2");
assert_eq!(
compute_new_version("15.2.0-9", true, false, false),
"15.2.0-9ubuntu1"
);
assert_eq!(
compute_new_version("15.2.0-9ubuntu1", true, false, false),
"15.2.0-9ubuntu2"
);
// No change rebuild
assert_eq!(compute_new_version("15.2.0-9", false, true, false), "15.2.0-9build1");
assert_eq!(compute_new_version("15.2.0-9build1", false, true, false), "15.2.0-9build2");
assert_eq!(
compute_new_version("15.2.0-9", false, true, false),
"15.2.0-9build1"
);
assert_eq!(
compute_new_version("15.2.0-9build1", false, true, false),
"15.2.0-9build2"
);
// Rebuild of Ubuntu version
assert_eq!(compute_new_version("15.2.0-9ubuntu1", false, true, false), "15.2.0-9ubuntu1build1");
assert_eq!(
compute_new_version("15.2.0-9ubuntu1", false, true, false),
"15.2.0-9ubuntu1build1"
);
// NMU
// Native
assert_eq!(compute_new_version("1.0", false, false, true), "1.0+nmu1");
assert_eq!(compute_new_version("1.0+nmu1", false, false, true), "1.0+nmu2");
assert_eq!(
compute_new_version("1.0+nmu1", false, false, true),
"1.0+nmu2"
);
// Non-native
assert_eq!(compute_new_version("1.0-1", false, false, true), "1.0-1.1");
assert_eq!(compute_new_version("1.0-1.1", false, false, true), "1.0-1.2");
assert_eq!(
compute_new_version("1.0-1.1", false, false, true),
"1.0-1.2"
);
// NMU of NMU?
assert_eq!(compute_new_version("1.0-1.2", false, false, true), "1.0-1.3");
assert_eq!(
compute_new_version("1.0-1.2", false, false, true),
"1.0-1.3"
);
// Native package uploads
assert_eq!(compute_new_version("1.0", false, false, false), "1.1");
assert_eq!(compute_new_version("1.0.5", false, false, false), "1.0.6");
assert_eq!(compute_new_version("20241126", false, false, false), "20241127");
assert_eq!(
compute_new_version("20241126", false, false, false),
"20241127"
);
}
#[test]
@@ -375,7 +449,7 @@ mod tests {
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");