1use crate::git::GitRepo;
7use crate::log_debug;
8use anyhow::{Context, Result};
9use regex;
10use std::fs;
11use std::io::Write;
12use std::path::Path;
13use std::sync::Arc;
14
15pub struct ChangelogGenerator;
17
18impl ChangelogGenerator {
19 #[allow(clippy::too_many_lines)]
36 pub fn update_changelog_file(
41 changelog_content: &str,
42 changelog_path: &str,
43 git_repo: &Arc<GitRepo>,
44 to_ref: &str,
45 version_name: Option<String>,
46 ) -> Result<()> {
47 let path = Path::new(changelog_path);
48 let default_header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n";
49
50 let commit_date = match git_repo.get_commit_date(to_ref) {
52 Ok(date) => {
53 log_debug!("Got commit date for {}: {}", to_ref, date);
54 date
55 }
56 Err(e) => {
57 log_debug!("Failed to get commit date for {}: {}", to_ref, e);
58 chrono::Local::now().format("%Y-%m-%d").to_string()
59 }
60 };
61
62 let stripped_content = strip_ansi_codes(changelog_content);
64
65 let clean_content =
67 if stripped_content.starts_with("━") || stripped_content.starts_with('-') {
68 if let Some(pos) = stripped_content.find('\n') {
70 stripped_content[pos + 1..].to_string()
71 } else {
72 stripped_content
73 }
74 } else {
75 stripped_content
76 };
77
78 let mut version_content = if clean_content.contains("## [") {
80 let parts: Vec<&str> = clean_content.split("## [").collect();
81 if parts.len() > 1 {
82 format!("## [{}", parts[1])
83 } else {
84 clean_content
85 }
86 } else {
87 clean_content
88 };
89
90 if let Some(version) = version_name {
92 if version_content.contains("## [") {
93 let re = regex::Regex::new(r"## \[([^\]]+)\]")
94 .context("Failed to compile changelog version regex")?;
95 version_content = re
96 .replace(&version_content, &format!("## [{version}]"))
97 .to_string();
98 log_debug!("Replaced version with user-provided version: {}", version);
99 } else {
100 log_debug!("Could not find version header to replace in changelog content");
101 }
102 }
103
104 if version_content.contains(" - \n") {
106 version_content = version_content.replace(" - \n", &format!(" - {commit_date}\n"));
108 log_debug!("Replaced empty date with commit date: {}", commit_date);
109 } else if version_content.contains("] - ") && !version_content.contains("] - 20") {
110 let parts: Vec<&str> = version_content.splitn(2, "] - ").collect();
112 if parts.len() == 2 {
113 version_content = format!(
114 "{}] - {}\n{}",
115 parts[0],
116 commit_date,
117 parts[1].trim_start_matches(['\n', ' '])
118 );
119 log_debug!("Added commit date after dash: {}", commit_date);
120 }
121 } else if !version_content.contains("] - ") {
122 let line_end = version_content.find('\n').unwrap_or(version_content.len());
124 let version_line = &version_content[..line_end];
125
126 if version_line.contains("## [") && version_line.contains(']') {
127 if let Some(bracket_pos) = version_line.rfind(']') {
129 version_content = format!(
130 "{} - {}{}",
131 &version_content[..=bracket_pos],
132 commit_date,
133 &version_content[bracket_pos + 1..]
134 );
135 log_debug!("Added date to version line: {}", commit_date);
136 }
137 }
138 }
139
140 let separator =
142 "\n<!-- -------------------------------------------------------------- -->\n\n";
143 let version_content_with_separator = format!("{version_content}{separator}");
144
145 let updated_content = if path.exists() {
146 let existing_content = fs::read_to_string(path)
147 .with_context(|| format!("Failed to read changelog file: {changelog_path}"))?;
148
149 if existing_content.contains("# Changelog")
151 && existing_content.contains("Keep a Changelog")
152 {
153 if existing_content.contains("## [") {
155 let parts: Vec<&str> = existing_content.split("## [").collect();
156 let header = parts[0];
157
158 if parts.len() > 1 {
160 let existing_versions = parts[1..].join("## [");
161 format!("{header}{version_content_with_separator}## [{existing_versions}")
162 } else {
163 format!("{header}{version_content_with_separator}")
164 }
165 } else {
166 format!("{existing_content}{version_content_with_separator}")
168 }
169 } else {
170 format!("{default_header}{version_content_with_separator}")
172 }
173 } else {
174 format!("{default_header}{version_content_with_separator}")
176 };
177
178 let mut file = fs::File::create(path)
180 .with_context(|| format!("Failed to create changelog file: {changelog_path}"))?;
181
182 file.write_all(updated_content.as_bytes())
183 .with_context(|| format!("Failed to write to changelog file: {changelog_path}"))?;
184
185 Ok(())
186 }
187}
188
189fn strip_ansi_codes(s: &str) -> String {
191 let re = regex::Regex::new(r"\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[m|K]")
192 .expect("Failed to compile ANSI escape code regex");
193 re.replace_all(s, "").to_string()
194}