use anyhow::{Context, Result};
use chrono::Local;
use log::{debug, warn};
use regex::Regex;
use semver::Version;
use std::fs;
use std::path::{Path, PathBuf};
const CHANGELOG_PATTERNS: [&str; 3] = [
r"(?i)^changelog(\.md)?$", r"(?i)^changes(\.md)?$", r"(?i)^history(\.md)?$", ];
const UNRELEASED_PATTERNS: [&str; 3] = [
r"(?mi)^##\s*\[unreleased\]", r"(?mi)^##\s+unreleased", r"(?mi)^\[unreleased\](?:\s*)?(?:\n|$)", ];
pub fn find_changelog(dir: &str) -> Option<PathBuf> {
let dir_path = Path::new(dir);
if let Ok(entries) = fs::read_dir(dir_path) {
for entry in entries.filter_map(Result::ok) {
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
for pattern in &CHANGELOG_PATTERNS {
let re = Regex::new(pattern).unwrap();
if re.is_match(&file_name_str) {
let path = entry.path();
debug!("Found changelog: {}", path.display());
return Some(path);
}
}
}
}
None
}
pub fn update_changelog(path: &Path, version: &Version) -> Result<()> {
let content = fs::read_to_string(path).context("Failed to read changelog file")?;
let today = Local::now().format("%Y-%m-%d").to_string();
let mut new_content = content.clone();
let mut found = false;
let version_header = format!("## [{}] - {}", version, today);
for pattern in &UNRELEASED_PATTERNS {
let re = match Regex::new(pattern) {
Ok(re) => re,
Err(e) => {
warn!("Invalid regex pattern: {} - {}", pattern, e);
continue;
}
};
if re.is_match(&content) {
new_content = re.replace(&content, &version_header).to_string();
found = true;
debug!(
"Matched pattern: {} - replacing with: {}",
pattern, version_header
);
break;
}
}
if !found {
warn!(
"No unreleased section found in changelog at {}",
path.display()
);
} else {
fs::write(path, new_content).context("Failed to write updated changelog")?;
debug!(
"Updated unreleased section to version {} in {}",
version,
path.display()
);
}
Ok(())
}
pub fn dry_run_update_changelog(path: &Path, version: &Version) -> Result<String> {
let content = fs::read_to_string(path).context("Failed to read changelog file")?;
let today = Local::now().format("%Y-%m-%d").to_string();
let mut diff = String::new();
let mut found = false;
let version_header = format!("## [{}] - {}", version, today);
for pattern in &UNRELEASED_PATTERNS {
let re = match Regex::new(pattern) {
Ok(re) => re,
Err(e) => {
warn!("Invalid regex pattern: {} - {}", pattern, e);
continue;
}
};
if let Some(m) = re.find(&content) {
let header = m.as_str();
diff = format!(
"Would update changelog {}:\n {} → {}",
path.display(),
header.trim(),
version_header
);
found = true;
break;
}
}
if !found {
diff = format!(
"No unreleased section found in changelog {}",
path.display()
);
}
Ok(diff)
}