Skip to main content

nargo_changes/
change_set.rs

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/// Change set data structure.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ChangeSet {
14    /// The ID of the change set.
15    pub id: String,
16    /// The type of change.
17    pub r#type: ChangeType,
18    /// A summary of the change.
19    pub summary: String,
20    /// Detailed description of the change.
21    pub description: Option<String>,
22    /// The author of the change.
23    pub author: Option<String>,
24    /// The packages affected by the change.
25    pub packages: Vec<String>,
26    /// Whether the change is a prerelease.
27    pub prerelease: bool,
28}
29
30/// Change set template.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ChangeSetTemplate {
33    /// The name of the template.
34    pub name: String,
35    /// The default change type.
36    pub default_type: ChangeType,
37    /// The default description template.
38    pub description_template: Option<String>,
39    /// The default packages.
40    pub default_packages: Vec<String>,
41    /// Whether the change is a prerelease by default.
42    pub default_prerelease: bool,
43}
44
45/// Change set preset.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ChangeSetPreset {
48    /// The name of the preset.
49    pub name: String,
50    /// The description of the preset.
51    pub description: Option<String>,
52    /// The template to use for this preset.
53    pub template: String,
54    /// Additional metadata for the preset.
55    pub metadata: HashMap<String, String>,
56}
57
58/// Change set manager.
59pub struct ChangeSetManager {
60    /// The directory where change sets are stored.
61    pub changes_dir: PathBuf,
62}
63
64impl ChangeSetManager {
65    /// Creates a new change set manager.
66    pub fn new(changes_dir: &Path) -> Self {
67        Self { changes_dir: changes_dir.to_path_buf() }
68    }
69
70    /// Creates a new change set.
71    pub fn create_change_set(&self, change_set: &ChangeSet) -> Result<PathBuf> {
72        // Ensure the changes directory exists
73        fs::create_dir_all(&self.changes_dir)?;
74
75        // Generate the file path
76        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        // Write the change set to file
80        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    /// Reads all change sets from the changes directory.
88    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    /// Generates a changelog from the change sets.
101    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        // Group change sets by type
106        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        // Add breaking changes
121        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        // Add features
133        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        // Add fixes
145        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        // Add other changes
157        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    /// Clears all change sets after generating a changelog.
172    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    /// Merges multiple change sets into a single change set.
181    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        // Determine the highest priority change type
187        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        // Merge summaries
190        let merged_summary = change_sets.iter().map(|cs| cs.summary.clone()).collect::<Vec<_>>().join("; ");
191
192        // Merge descriptions
193        let merged_description = change_sets.iter().filter_map(|cs| cs.description.clone()).collect::<Vec<_>>().join("\n\n");
194
195        // Merge packages (dedup)
196        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        // Check if any change set is a prerelease
205        let merged_prerelease = change_sets.iter().any(|cs| cs.prerelease);
206
207        // Create the merged change set
208        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, // Merged change sets don't have a single author
214            packages: merged_packages,
215            prerelease: merged_prerelease,
216        };
217
218        Ok(merged_change_set)
219    }
220
221    /// Resolves conflicts between change sets.
222    pub fn resolve_conflicts(&self, change_sets: &[ChangeSet]) -> Result<Vec<ChangeSet>> {
223        // Simple conflict resolution: group by type and summary
224        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                // Merge packages
230                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                // Merge descriptions
239                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                // Set prerelease if any is true
249                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    /// Gets the priority of a change type for merging.
262    /// Higher priority change types override lower ones.
263    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    /// Loads a change set template.
278    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    /// Saves a change set template.
293    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    /// Loads a change set preset.
307    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    /// Saves a change set preset.
322    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    /// Creates a change set from a template.
336    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    /// Creates a change set from a preset.
345    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    /// Generates an enhanced changelog with more details.
351    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        // Group change sets by type
356        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        // Define the order of change types
362        let type_order = [ChangeType::Breaking, ChangeType::Feature, ChangeType::Fix, ChangeType::Perf, ChangeType::Refactor, ChangeType::Docs, ChangeType::Test, ChangeType::Build, ChangeType::Chore];
363
364        // Add changes in order
365        for change_type in &type_order {
366            if let Some(cs_list) = grouped.get(change_type) {
367                if !cs_list.is_empty() {
368                    // Add section header
369                    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                    // Add each change
383                    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}