This commit is contained in:
230
src/changelog.rs
230
src/changelog.rs
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user