git_iris/changes/
releasenotes.rs

1use super::common::generate_changes_content;
2use super::models::{
3    BreakingChange, ChangeMetrics, Highlight, ReleaseNotesResponse, Section, SectionItem,
4};
5use super::prompt;
6use crate::common::DetailLevel;
7use crate::config::Config;
8use crate::git::GitRepo;
9use anyhow::Result;
10use colored::Colorize;
11use std::sync::Arc;
12
13/// Struct responsible for generating release notes
14pub struct ReleaseNotesGenerator;
15
16impl ReleaseNotesGenerator {
17    /// Generates release notes for the specified range of commits.
18    ///
19    /// # Arguments
20    ///
21    /// * `git_repo` - Arc<GitRepo> instance
22    /// * `from` - Starting point for the release notes (e.g., a commit hash or tag)
23    /// * `to` - Ending point for the release notes (e.g., a commit hash, tag, or "HEAD")
24    /// * `config` - Configuration object containing LLM settings
25    /// * `detail_level` - Level of detail for the release notes (Minimal, Standard, or Detailed)
26    ///
27    /// # Returns
28    ///
29    /// A Result containing the generated release notes as a String, or an error
30    pub async fn generate(
31        git_repo: Arc<GitRepo>,
32        from: &str,
33        to: &str,
34        config: &Config,
35        detail_level: DetailLevel,
36    ) -> Result<String> {
37        let release_notes: ReleaseNotesResponse = generate_changes_content::<ReleaseNotesResponse>(
38            git_repo,
39            from,
40            to,
41            config,
42            detail_level,
43            prompt::create_release_notes_system_prompt,
44            prompt::create_release_notes_user_prompt,
45        )
46        .await?;
47
48        Ok(format_release_notes_response(&release_notes))
49    }
50}
51
52/// Formats the `ReleaseNotesResponse` into human-readable release notes
53fn format_release_notes_response(response: &ReleaseNotesResponse) -> String {
54    let mut formatted = String::new();
55
56    // Add header
57    formatted.push_str(&format!(
58        "# Release Notes - v{}\n\n",
59        response
60            .version
61            .clone()
62            .unwrap_or_default()
63            .bright_green()
64            .bold()
65    ));
66    formatted.push_str(&format!(
67        "Release Date: {}\n\n",
68        response.release_date.clone().unwrap_or_default().yellow()
69    ));
70
71    // Add summary
72    formatted.push_str(&format!("{}\n\n", response.summary.bright_cyan()));
73
74    // Add highlights
75    if !response.highlights.is_empty() {
76        formatted.push_str(&"## ✨ Highlights\n\n".bright_magenta().bold().to_string());
77        for highlight in &response.highlights {
78            formatted.push_str(&format_highlight(highlight));
79        }
80    }
81
82    // Add changes grouped by section
83    for section in &response.sections {
84        formatted.push_str(&format_section(section));
85    }
86
87    // Add breaking changes
88    if !response.breaking_changes.is_empty() {
89        formatted.push_str(&"## ⚠️ Breaking Changes\n\n".bright_red().bold().to_string());
90        for breaking_change in &response.breaking_changes {
91            formatted.push_str(&format_breaking_change(breaking_change));
92        }
93    }
94
95    // Add upgrade notes
96    if !response.upgrade_notes.is_empty() {
97        formatted.push_str(&"## 🔧 Upgrade Notes\n\n".yellow().bold().to_string());
98        for note in &response.upgrade_notes {
99            formatted.push_str(&format!("- {note}\n"));
100        }
101        formatted.push('\n');
102    }
103
104    // Add metrics
105    formatted.push_str(&"## 📊 Metrics\n\n".bright_blue().bold().to_string());
106    formatted.push_str(&format_metrics(&response.metrics));
107
108    formatted
109}
110
111/// Formats a highlight
112fn format_highlight(highlight: &Highlight) -> String {
113    format!(
114        "### {}\n\n{}\n\n",
115        highlight.title.bright_yellow().bold(),
116        highlight.description
117    )
118}
119
120/// Formats a section
121fn format_section(section: &Section) -> String {
122    let mut formatted = format!("## {}\n\n", section.title.bright_blue().bold());
123    for item in &section.items {
124        formatted.push_str(&format_section_item(item));
125    }
126    formatted.push('\n');
127    formatted
128}
129
130/// Formats a section item
131fn format_section_item(item: &SectionItem) -> String {
132    let mut formatted = format!("- {}", item.description);
133
134    if !item.associated_issues.is_empty() {
135        formatted.push_str(&format!(
136            " ({})",
137            item.associated_issues.join(", ").yellow()
138        ));
139    }
140
141    if let Some(pr) = &item.pull_request {
142        formatted.push_str(&format!(" [{}]", pr.bright_purple()));
143    }
144
145    formatted.push('\n');
146    formatted
147}
148
149/// Formats a breaking change
150fn format_breaking_change(breaking_change: &BreakingChange) -> String {
151    format!(
152        "- {} ({})\n",
153        breaking_change.description,
154        breaking_change.commit_hash.dimmed()
155    )
156}
157
158/// Formats the change metrics
159fn format_metrics(metrics: &ChangeMetrics) -> String {
160    format!(
161        "- Total Commits: {}\n- Files Changed: {}\n- Insertions: {}\n- Deletions: {}\n",
162        metrics.total_commits.to_string().green(),
163        metrics.files_changed.to_string().yellow(),
164        metrics.insertions.to_string().green(),
165        metrics.deletions.to_string().red()
166    )
167}