git_iris/changes/
changelog.rs1use super::common::generate_changes_content;
2use super::models::{BreakingChange, ChangeEntry, ChangeMetrics, ChangelogResponse, ChangelogType};
3use super::prompt;
4use crate::common::DetailLevel;
5use crate::config::Config;
6use crate::git::GitRepo;
7use anyhow::Result;
8use colored::Colorize;
9use std::sync::Arc;
10
11pub struct ChangelogGenerator;
13
14impl ChangelogGenerator {
15 pub async fn generate(
29 git_repo: Arc<GitRepo>,
30 from: &str,
31 to: &str,
32 config: &Config,
33 detail_level: DetailLevel,
34 ) -> Result<String> {
35 let changelog: ChangelogResponse = generate_changes_content::<ChangelogResponse>(
36 git_repo,
37 from,
38 to,
39 config,
40 detail_level,
41 prompt::create_changelog_system_prompt,
42 prompt::create_changelog_user_prompt,
43 )
44 .await?;
45
46 Ok(format_changelog_response(&changelog))
47 }
48}
49
50fn format_changelog_response(response: &ChangelogResponse) -> String {
52 let mut formatted = String::new();
53
54 formatted.push_str(&"# Changelog\n\n".bright_cyan().bold().to_string());
56 formatted.push_str("All notable changes to this project will be documented in this file.\n\n");
57 formatted.push_str(
58 "The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\n",
59 );
60 formatted.push_str("and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n");
61
62 formatted.push_str(&format!(
64 "## [{}] - {}\n\n",
65 response
66 .version
67 .clone()
68 .unwrap_or_default()
69 .bright_green()
70 .bold(),
71 response.release_date.clone().unwrap_or_default().yellow()
72 ));
73
74 for (change_type, entries) in &response.sections {
76 if !entries.is_empty() {
77 formatted.push_str(&format_change_type(change_type));
78 for entry in entries {
79 formatted.push_str(&format_change_entry(entry));
80 }
81 formatted.push('\n');
82 }
83 }
84
85 if !response.breaking_changes.is_empty() {
87 formatted.push_str(
88 &"### ⚠️ Breaking Changes\n\n"
89 .bright_red()
90 .bold()
91 .to_string(),
92 );
93 for breaking_change in &response.breaking_changes {
94 formatted.push_str(&format_breaking_change(breaking_change));
95 }
96 formatted.push('\n');
97 }
98
99 formatted.push_str(&"### 📊 Metrics\n\n".bright_magenta().bold().to_string());
101 formatted.push_str(&format_metrics(&response.metrics));
102
103 formatted
104}
105
106fn format_change_type(change_type: &ChangelogType) -> String {
108 let (emoji, text) = match change_type {
109 ChangelogType::Added => ("✨", "Added"),
110 ChangelogType::Changed => ("🔄", "Changed"),
111 ChangelogType::Deprecated => ("⚠️", "Deprecated"),
112 ChangelogType::Removed => ("🗑️", "Removed"),
113 ChangelogType::Fixed => ("🐛", "Fixed"),
114 ChangelogType::Security => ("🔒", "Security"),
115 };
116 format!("### {} {}\n\n", emoji, text.bright_blue().bold())
117}
118
119fn format_change_entry(entry: &ChangeEntry) -> String {
121 let mut formatted = format!("- {}", entry.description);
122
123 if !entry.associated_issues.is_empty() {
124 formatted.push_str(&format!(
125 " ({})",
126 entry.associated_issues.join(", ").yellow()
127 ));
128 }
129
130 if let Some(pr) = &entry.pull_request {
131 formatted.push_str(&format!(" [{}]", pr.bright_purple()));
132 }
133
134 formatted.push_str(&format!(" ({})\n", entry.commit_hashes.join(", ").dimmed()));
135
136 formatted
137}
138
139fn format_breaking_change(breaking_change: &BreakingChange) -> String {
141 format!(
142 "- {} ({})\n",
143 breaking_change.description,
144 breaking_change.commit_hash.dimmed()
145 )
146}
147
148fn format_metrics(metrics: &ChangeMetrics) -> String {
150 format!(
151 "- Total Commits: {}\n- Files Changed: {}\n- Insertions: {}\n- Deletions: {}\n",
152 metrics.total_commits.to_string().green(),
153 metrics.files_changed.to_string().yellow(),
154 metrics.insertions.to_string().green(),
155 metrics.deletions.to_string().red()
156 )
157}