1#![warn(missing_docs)]
2
3use chrono::Utc;
4use nargo_types::{Error, Result, Span};
5use serde::{Deserialize, Serialize};
6use std::{collections::HashMap, fs::{self, File}, io::Write, path::{Path, PathBuf}};
7use walkdir::WalkDir;
8
9use crate::types::ChangeType;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ChangeSet {
14 pub id: String,
16 pub r#type: ChangeType,
18 pub summary: String,
20 pub description: Option<String>,
22 pub author: Option<String>,
24 pub packages: Vec<String>,
26 pub prerelease: bool,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ChangeSetTemplate {
33 pub name: String,
35 pub default_type: ChangeType,
37 pub description_template: Option<String>,
39 pub default_packages: Vec<String>,
41 pub default_prerelease: bool,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ChangeSetPreset {
48 pub name: String,
50 pub description: Option<String>,
52 pub template: String,
54 pub metadata: HashMap<String, String>,
56}
57
58pub struct ChangeSetManager {
60 pub changes_dir: PathBuf,
62}
63
64impl ChangeSetManager {
65 pub fn new(changes_dir: &Path) -> Self {
67 Self { changes_dir: changes_dir.to_path_buf() }
68 }
69
70 pub fn create_change_set(&self, change_set: &ChangeSet) -> Result<PathBuf> {
72 fs::create_dir_all(&self.changes_dir)?;
74
75 let file_name = format!("{}-{}.json", change_set.id, change_set.r#type.as_str());
77 let file_path = self.changes_dir.join(file_name);
78
79 let mut file = File::create(&file_path)?;
81 let content = serde_json::to_string_pretty(change_set).map_err(|e| Error::external_error("json".to_string(), e.to_string(), Span::unknown()))?;
82 file.write_all(content.as_bytes())?;
83
84 Ok(file_path)
85 }
86
87 pub fn read_change_sets(&self) -> Result<Vec<ChangeSet>> {
89 let mut change_sets = Vec::new();
90
91 for entry in WalkDir::new(&self.changes_dir).into_iter().filter_map(|e| e.ok()).filter(|e| e.file_type().is_file()).filter(|e| e.path().extension().map(|ext| ext == "json").unwrap_or(false)) {
92 let content = fs::read_to_string(entry.path())?;
93 let change_set: ChangeSet = serde_json::from_str(&content).map_err(|e| Error::external_error("json".to_string(), e.to_string(), Span::unknown()))?;
94 change_sets.push(change_set);
95 }
96
97 Ok(change_sets)
98 }
99
100 pub fn generate_changelog(&self, version: &str, date: &str) -> Result<String> {
102 let change_sets = self.read_change_sets()?;
103 let mut changelog = format!("# Changelog\n\n## [{}] - {}\n\n", version, date);
104
105 let mut breaking = Vec::new();
107 let mut features = Vec::new();
108 let mut fixes = Vec::new();
109 let mut others = Vec::new();
110
111 for change_set in &change_sets {
112 match change_set.r#type {
113 ChangeType::Breaking => breaking.push(change_set),
114 ChangeType::Feature => features.push(change_set),
115 ChangeType::Fix => fixes.push(change_set),
116 _ => others.push(change_set),
117 }
118 }
119
120 if !breaking.is_empty() {
122 changelog.push_str("### Breaking Changes\n\n");
123 for change in &breaking {
124 changelog.push_str(&format!("- {}\n", change.summary));
125 if let Some(desc) = &change.description {
126 changelog.push_str(&format!(" {}\n", desc));
127 }
128 }
129 changelog.push_str("\n");
130 }
131
132 if !features.is_empty() {
134 changelog.push_str("### Features\n\n");
135 for change in &features {
136 changelog.push_str(&format!("- {}\n", change.summary));
137 if let Some(desc) = &change.description {
138 changelog.push_str(&format!(" {}\n", desc));
139 }
140 }
141 changelog.push_str("\n");
142 }
143
144 if !fixes.is_empty() {
146 changelog.push_str("### Bug Fixes\n\n");
147 for change in &fixes {
148 changelog.push_str(&format!("- {}\n", change.summary));
149 if let Some(desc) = &change.description {
150 changelog.push_str(&format!(" {}\n", desc));
151 }
152 }
153 changelog.push_str("\n");
154 }
155
156 if !others.is_empty() {
158 changelog.push_str("### Other Changes\n\n");
159 for change in &others {
160 changelog.push_str(&format!("- [{}] {}\n", change.r#type.as_str(), change.summary));
161 if let Some(desc) = &change.description {
162 changelog.push_str(&format!(" {}\n", desc));
163 }
164 }
165 changelog.push_str("\n");
166 }
167
168 Ok(changelog)
169 }
170
171 pub fn clear_change_sets(&self) -> Result<()> {
173 for entry in WalkDir::new(&self.changes_dir).into_iter().filter_map(|e| e.ok()).filter(|e| e.file_type().is_file()).filter(|e| e.path().extension().map(|ext| ext == "json").unwrap_or(false)) {
174 fs::remove_file(entry.path())?;
175 }
176
177 Ok(())
178 }
179
180 pub fn merge_change_sets(&self, change_sets: &[ChangeSet]) -> Result<ChangeSet> {
182 if change_sets.is_empty() {
183 return Err(Error::external_error("changes".to_string(), "No change sets to merge".to_string(), Span::unknown()));
184 }
185
186 let merged_type = change_sets.iter().max_by(|a, b| Self::change_type_priority(a.r#type).cmp(&Self::change_type_priority(b.r#type))).unwrap().r#type.clone();
188
189 let merged_summary = change_sets.iter().map(|cs| cs.summary.clone()).collect::<Vec<_>>().join("; ");
191
192 let merged_description = change_sets.iter().filter_map(|cs| cs.description.clone()).collect::<Vec<_>>().join("\n\n");
194
195 let mut merged_packages = HashMap::new();
197 for cs in change_sets {
198 for pkg in &cs.packages {
199 merged_packages.insert(pkg.clone(), ());
200 }
201 }
202 let merged_packages = merged_packages.keys().cloned().collect::<Vec<_>>();
203
204 let merged_prerelease = change_sets.iter().any(|cs| cs.prerelease);
206
207 let merged_change_set = ChangeSet {
209 id: format!("merged-{}", chrono::Utc::now().timestamp()),
210 r#type: merged_type,
211 summary: merged_summary,
212 description: if merged_description.is_empty() { None } else { Some(merged_description) },
213 author: None, packages: merged_packages,
215 prerelease: merged_prerelease,
216 };
217
218 Ok(merged_change_set)
219 }
220
221 pub fn resolve_conflicts(&self, change_sets: &[ChangeSet]) -> Result<Vec<ChangeSet>> {
223 let mut resolved: HashMap<(ChangeType, String), ChangeSet> = HashMap::new();
225
226 for cs in change_sets {
227 let key = (cs.r#type.clone(), cs.summary.clone());
228 if let Some(existing) = resolved.get_mut(&key) {
229 let mut packages = existing.packages.clone();
231 for pkg in &cs.packages {
232 if !packages.contains(pkg) {
233 packages.push(pkg.clone());
234 }
235 }
236 existing.packages = packages;
237
238 if let Some(desc) = &cs.description {
240 if let Some(existing_desc) = &existing.description {
241 existing.description = Some(format!("{}\n\n{}", existing_desc, desc));
242 }
243 else {
244 existing.description = Some(desc.clone());
245 }
246 }
247
248 if cs.prerelease {
250 existing.prerelease = true;
251 }
252 }
253 else {
254 resolved.insert(key, cs.clone());
255 }
256 }
257
258 Ok(resolved.values().cloned().collect())
259 }
260
261 fn change_type_priority(change_type: ChangeType) -> u8 {
264 match change_type {
265 ChangeType::Breaking => 10,
266 ChangeType::Feature => 8,
267 ChangeType::Fix => 6,
268 ChangeType::Perf => 5,
269 ChangeType::Refactor => 4,
270 ChangeType::Docs => 3,
271 ChangeType::Test => 2,
272 ChangeType::Build => 1,
273 ChangeType::Chore => 0,
274 }
275 }
276
277 pub fn load_template(&self, template_name: &str) -> Result<ChangeSetTemplate> {
279 let template_dir = self.changes_dir.join("templates");
280 let template_path = template_dir.join(format!("{}.json", template_name));
281
282 if !template_path.exists() {
283 return Err(Error::external_error("changes".to_string(), format!("Template not found: {}", template_name), Span::unknown()));
284 }
285
286 let content = fs::read_to_string(template_path)?;
287 let template: ChangeSetTemplate = serde_json::from_str(&content).map_err(|e| Error::external_error("json".to_string(), e.to_string(), Span::unknown()))?;
288
289 Ok(template)
290 }
291
292 pub fn save_template(&self, template: &ChangeSetTemplate) -> Result<()> {
294 let template_dir = self.changes_dir.join("templates");
295 fs::create_dir_all(&template_dir)?;
296
297 let template_path = template_dir.join(format!("{}.json", template.name));
298 let content = serde_json::to_string_pretty(template).map_err(|e| Error::external_error("json".to_string(), e.to_string(), Span::unknown()))?;
299
300 let mut file = File::create(template_path)?;
301 file.write_all(content.as_bytes())?;
302
303 Ok(())
304 }
305
306 pub fn load_preset(&self, preset_name: &str) -> Result<ChangeSetPreset> {
308 let preset_dir = self.changes_dir.join("presets");
309 let preset_path = preset_dir.join(format!("{}.json", preset_name));
310
311 if !preset_path.exists() {
312 return Err(Error::external_error("changes".to_string(), format!("Preset not found: {}", preset_name), Span::unknown()));
313 }
314
315 let content = fs::read_to_string(preset_path)?;
316 let preset: ChangeSetPreset = serde_json::from_str(&content).map_err(|e| Error::external_error("json".to_string(), e.to_string(), Span::unknown()))?;
317
318 Ok(preset)
319 }
320
321 pub fn save_preset(&self, preset: &ChangeSetPreset) -> Result<()> {
323 let preset_dir = self.changes_dir.join("presets");
324 fs::create_dir_all(&preset_dir)?;
325
326 let preset_path = preset_dir.join(format!("{}.json", preset.name));
327 let content = serde_json::to_string_pretty(preset).map_err(|e| Error::external_error("json".to_string(), e.to_string(), Span::unknown()))?;
328
329 let mut file = File::create(preset_path)?;
330 file.write_all(content.as_bytes())?;
331
332 Ok(())
333 }
334
335 pub fn create_change_set_from_template(&self, template_name: &str, summary: &str, author: Option<String>) -> Result<PathBuf> {
337 let template = self.load_template(template_name)?;
338
339 let change_set = ChangeSet { id: format!("{}", chrono::Utc::now().timestamp()), r#type: template.default_type, summary: summary.to_string(), description: template.description_template, author, packages: template.default_packages, prerelease: template.default_prerelease };
340
341 self.create_change_set(&change_set)
342 }
343
344 pub fn create_change_set_from_preset(&self, preset_name: &str, summary: &str, author: Option<String>) -> Result<PathBuf> {
346 let preset = self.load_preset(preset_name)?;
347 self.create_change_set_from_template(&preset.template, summary, author)
348 }
349
350 pub fn generate_enhanced_changelog(&self, version: &str, date: &str, include_authors: bool) -> Result<String> {
352 let change_sets = self.read_change_sets()?;
353 let mut changelog = format!("# Changelog\n\n## [{}] - {}\n\n", version, date);
354
355 let mut grouped = HashMap::new();
357 for cs in &change_sets {
358 grouped.entry(cs.r#type.clone()).or_insert_with(Vec::new).push(cs);
359 }
360
361 let type_order = [ChangeType::Breaking, ChangeType::Feature, ChangeType::Fix, ChangeType::Perf, ChangeType::Refactor, ChangeType::Docs, ChangeType::Test, ChangeType::Build, ChangeType::Chore];
363
364 for change_type in &type_order {
366 if let Some(cs_list) = grouped.get(change_type) {
367 if !cs_list.is_empty() {
368 let section_title = match change_type {
370 ChangeType::Breaking => "Breaking Changes",
371 ChangeType::Feature => "Features",
372 ChangeType::Fix => "Bug Fixes",
373 ChangeType::Perf => "Performance Improvements",
374 ChangeType::Refactor => "Code Refactoring",
375 ChangeType::Docs => "Documentation",
376 ChangeType::Test => "Tests",
377 ChangeType::Build => "Build System",
378 ChangeType::Chore => "Chores",
379 };
380 changelog.push_str(&format!("### {}\n\n", section_title));
381
382 for cs in cs_list {
384 changelog.push_str(&format!("- {}\n", cs.summary));
385 if let Some(desc) = &cs.description {
386 changelog.push_str(&format!(" {}\n", desc));
387 }
388 if include_authors && cs.author.is_some() {
389 changelog.push_str(&format!(" **Author:** {}\n", cs.author.as_ref().unwrap()));
390 }
391 if !cs.packages.is_empty() {
392 changelog.push_str(&format!(" **Packages:** {}\n", cs.packages.join(", ")));
393 }
394 changelog.push_str("\n");
395 }
396 }
397 }
398 }
399
400 Ok(changelog)
401 }
402}