project_version/
changelog.rs1use anyhow::{Context, Result};
2use chrono::Local;
3use log::{debug, warn};
4use regex::Regex;
5use semver::Version;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9const CHANGELOG_PATTERNS: [&str; 3] = [
11 r"(?i)^changelog(\.md)?$", r"(?i)^changes(\.md)?$", r"(?i)^history(\.md)?$", ];
15
16const UNRELEASED_PATTERNS: [&str; 3] = [
19 r"(?mi)^##\s*\[unreleased\]", r"(?mi)^##\s+unreleased", r"(?mi)^\[unreleased\](?:\s*)?(?:\n|$)", ];
23
24pub fn find_changelog(dir: &str) -> Option<PathBuf> {
26 let dir_path = Path::new(dir);
27
28 if let Ok(entries) = fs::read_dir(dir_path) {
29 for entry in entries.filter_map(Result::ok) {
30 let file_name = entry.file_name();
31 let file_name_str = file_name.to_string_lossy();
32
33 for pattern in &CHANGELOG_PATTERNS {
34 let re = Regex::new(pattern).unwrap();
35 if re.is_match(&file_name_str) {
36 let path = entry.path();
37 debug!("Found changelog: {}", path.display());
38 return Some(path);
39 }
40 }
41 }
42 }
43
44 None
45}
46
47pub fn update_changelog(path: &Path, version: &Version) -> Result<()> {
49 let content = fs::read_to_string(path).context("Failed to read changelog file")?;
50
51 let today = Local::now().format("%Y-%m-%d").to_string();
52
53 let mut new_content = content.clone();
55 let mut found = false;
56
57 let version_header = format!("## [{}] - {}", version, today);
59
60 for pattern in &UNRELEASED_PATTERNS {
62 let re = match Regex::new(pattern) {
64 Ok(re) => re,
65 Err(e) => {
66 warn!("Invalid regex pattern: {} - {}", pattern, e);
67 continue;
68 }
69 };
70
71 if re.is_match(&content) {
73 new_content = re.replace(&content, &version_header).to_string();
75 found = true;
76 debug!(
77 "Matched pattern: {} - replacing with: {}",
78 pattern, version_header
79 );
80 break;
81 }
82 }
83
84 if !found {
85 warn!(
86 "No unreleased section found in changelog at {}",
87 path.display()
88 );
89 } else {
91 fs::write(path, new_content).context("Failed to write updated changelog")?;
93 debug!(
94 "Updated unreleased section to version {} in {}",
95 version,
96 path.display()
97 );
98 }
99
100 Ok(())
101}
102
103pub fn dry_run_update_changelog(path: &Path, version: &Version) -> Result<String> {
105 let content = fs::read_to_string(path).context("Failed to read changelog file")?;
106
107 let today = Local::now().format("%Y-%m-%d").to_string();
108 let mut diff = String::new();
109 let mut found = false;
110
111 let version_header = format!("## [{}] - {}", version, today);
112
113 for pattern in &UNRELEASED_PATTERNS {
115 let re = match Regex::new(pattern) {
117 Ok(re) => re,
118 Err(e) => {
119 warn!("Invalid regex pattern: {} - {}", pattern, e);
120 continue;
121 }
122 };
123
124 if let Some(m) = re.find(&content) {
125 let header = m.as_str();
126
127 diff = format!(
128 "Would update changelog {}:\n {} → {}",
129 path.display(),
130 header.trim(),
131 version_header
132 );
133 found = true;
134 break;
135 }
136 }
137
138 if !found {
139 diff = format!(
140 "No unreleased section found in changelog {}",
141 path.display()
142 );
143 }
144
145 Ok(diff)
146}