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(
37 changelog_content: &str,
38 changelog_path: &str,
39 git_repo: &Arc<GitRepo>,
40 to_ref: &str,
41 version_name: Option<String>,
42 ) -> Result<()> {
43 let path = Path::new(changelog_path);
44 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";
45
46 let commit_date = match git_repo.get_commit_date(to_ref) {
48 Ok(date) => {
49 log_debug!("Got commit date for {}: {}", to_ref, date);
50 date
51 }
52 Err(e) => {
53 log_debug!("Failed to get commit date for {}: {}", to_ref, e);
54 chrono::Local::now().format("%Y-%m-%d").to_string()
55 }
56 };
57
58 let stripped_content = strip_ansi_codes(changelog_content);
60
61 let clean_content =
63 if stripped_content.starts_with("━") || stripped_content.starts_with('-') {
64 if let Some(pos) = stripped_content.find('\n') {
66 stripped_content[pos + 1..].to_string()
67 } else {
68 stripped_content
69 }
70 } else {
71 stripped_content
72 };
73
74 let mut version_content = if clean_content.contains("## [") {
76 let parts: Vec<&str> = clean_content.split("## [").collect();
77 if parts.len() > 1 {
78 format!("## [{}", parts[1])
79 } else {
80 clean_content
81 }
82 } else {
83 clean_content
84 };
85
86 if let Some(version) = version_name {
88 if version_content.contains("## [") {
89 let re = regex::Regex::new(r"## \[([^\]]+)\]").expect("Failed to compile regex");
90 version_content = re
91 .replace(&version_content, &format!("## [{version}]"))
92 .to_string();
93 log_debug!("Replaced version with user-provided version: {}", version);
94 } else {
95 log_debug!("Could not find version header to replace in changelog content");
96 }
97 }
98
99 if version_content.contains(" - \n") {
101 version_content = version_content.replace(" - \n", &format!(" - {commit_date}\n"));
103 log_debug!("Replaced empty date with commit date: {}", commit_date);
104 } else if version_content.contains("] - ") && !version_content.contains("] - 20") {
105 let parts: Vec<&str> = version_content.splitn(2, "] - ").collect();
107 if parts.len() == 2 {
108 version_content = format!(
109 "{}] - {}\n{}",
110 parts[0],
111 commit_date,
112 parts[1].trim_start_matches(['\n', ' '])
113 );
114 log_debug!("Added commit date after dash: {}", commit_date);
115 }
116 } else if !version_content.contains("] - ") {
117 let line_end = version_content.find('\n').unwrap_or(version_content.len());
119 let version_line = &version_content[..line_end];
120
121 if version_line.contains("## [") && version_line.contains(']') {
122 let bracket_pos = version_line
124 .rfind(']')
125 .expect("Failed to find closing bracket in version line");
126 version_content = format!(
127 "{} - {}{}",
128 &version_content[..=bracket_pos],
129 commit_date,
130 &version_content[bracket_pos + 1..]
131 );
132 log_debug!("Added date to version line: {}", commit_date);
133 }
134 }
135
136 let separator =
138 "\n<!-- -------------------------------------------------------------- -->\n\n";
139 let version_content_with_separator = format!("{version_content}{separator}");
140
141 let updated_content = if path.exists() {
142 let existing_content = fs::read_to_string(path)
143 .with_context(|| format!("Failed to read changelog file: {changelog_path}"))?;
144
145 if existing_content.contains("# Changelog")
147 && existing_content.contains("Keep a Changelog")
148 {
149 if existing_content.contains("## [") {
151 let parts: Vec<&str> = existing_content.split("## [").collect();
152 let header = parts[0];
153
154 if parts.len() > 1 {
156 let existing_versions = parts[1..].join("## [");
157 format!("{header}{version_content_with_separator}## [{existing_versions}")
158 } else {
159 format!("{header}{version_content_with_separator}")
160 }
161 } else {
162 format!("{existing_content}{version_content_with_separator}")
164 }
165 } else {
166 format!("{default_header}{version_content_with_separator}")
168 }
169 } else {
170 format!("{default_header}{version_content_with_separator}")
172 };
173
174 let mut file = fs::File::create(path)
176 .with_context(|| format!("Failed to create changelog file: {changelog_path}"))?;
177
178 file.write_all(updated_content.as_bytes())
179 .with_context(|| format!("Failed to write to changelog file: {changelog_path}"))?;
180
181 Ok(())
182 }
183}
184
185fn strip_ansi_codes(s: &str) -> String {
187 let re = regex::Regex::new(r"\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[m|K]")
188 .expect("Failed to compile ANSI escape code regex");
189 re.replace_all(s, "").to_string()
190}