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    /// * `version_name` - Optional explicit version name to use instead of detecting from Git
27    ///
28    /// # Returns
29    ///
30    /// A Result containing the generated release notes as a String, or an error
31    pub async fn generate(
32        git_repo: Arc<GitRepo>,
33        from: &str,
34        to: &str,
35        config: &Config,
36        detail_level: DetailLevel,
37        version_name: Option<String>,
38    ) -> Result<String> {
39        let release_notes: ReleaseNotesResponse = generate_changes_content::<ReleaseNotesResponse>(
40            git_repo,
41            from,
42            to,
43            config,
44            detail_level,
45            prompt::create_release_notes_system_prompt,
46            prompt::create_release_notes_user_prompt,
47        )
48        .await?;
49
50        Ok(format_release_notes_response(
51            &release_notes,
52            version_name.as_deref(),
53        ))
54    }
55}
56
57/// Formats the `ReleaseNotesResponse` into human-readable release notes
58fn format_release_notes_response(
59    response: &ReleaseNotesResponse,
60    version_name: Option<&str>,
61) -> String {
62    let mut formatted = String::new();
63
64    // Add header
65    let version = match version_name {
66        Some(name) => name.to_string(),
67        None => response.version.clone().unwrap_or_default(),
68    };
69
70    formatted.push_str(&format!(
71        "# Release Notes - v{}\n\n",
72        version.bright_green().bold()
73    ));
74    formatted.push_str(&format!(
75        "Release Date: {}\n\n",
76        response.release_date.clone().unwrap_or_default().yellow()
77    ));
78
79    // Add summary
80    formatted.push_str(&format!("{}\n\n", response.summary.bright_cyan()));
81
82    // Add highlights
83    if !response.highlights.is_empty() {
84        formatted.push_str(&"## ✨ Highlights\n\n".bright_magenta().bold().to_string());
85        for highlight in &response.highlights {
86            formatted.push_str(&format_highlight(highlight));
87        }
88    }
89
90    // Add changes grouped by section
91    for section in &response.sections {
92        formatted.push_str(&format_section(section));
93    }
94
95    // Add breaking changes
96    if !response.breaking_changes.is_empty() {
97        formatted.push_str(&"## ⚠️ Breaking Changes\n\n".bright_red().bold().to_string());
98        for breaking_change in &response.breaking_changes {
99            formatted.push_str(&format_breaking_change(breaking_change));
100        }
101    }
102
103    // Add upgrade notes
104    if !response.upgrade_notes.is_empty() {
105        formatted.push_str(&"## 🔧 Upgrade Notes\n\n".yellow().bold().to_string());
106        for note in &response.upgrade_notes {
107            formatted.push_str(&format!("- {note}\n"));
108        }
109        formatted.push('\n');
110    }
111
112    // Add metrics
113    formatted.push_str(&"## 📊 Metrics\n\n".bright_blue().bold().to_string());
114    formatted.push_str(&format_metrics(&response.metrics));
115
116    formatted
117}
118
119/// Formats a highlight
120fn format_highlight(highlight: &Highlight) -> String {
121    format!(
122        "### {}\n\n{}\n\n",
123        highlight.title.bright_yellow().bold(),
124        highlight.description
125    )
126}
127
128/// Formats a section
129fn format_section(section: &Section) -> String {
130    let mut formatted = format!("## {}\n\n", section.title.bright_blue().bold());
131    for item in &section.items {
132        formatted.push_str(&format_section_item(item));
133    }
134    formatted.push('\n');
135    formatted
136}
137
138/// Formats a section item
139fn format_section_item(item: &SectionItem) -> String {
140    let mut formatted = format!("- {}", item.description);
141
142    if !item.associated_issues.is_empty() {
143        formatted.push_str(&format!(
144            " ({})",
145            item.associated_issues.join(", ").yellow()
146        ));
147    }
148
149    if let Some(pr) = &item.pull_request {
150        formatted.push_str(&format!(" [{}]", pr.bright_purple()));
151    }
152
153    formatted.push('\n');
154    formatted
155}
156
157/// Formats a breaking change
158fn format_breaking_change(breaking_change: &BreakingChange) -> String {
159    format!(
160        "- {} ({})\n",
161        breaking_change.description,
162        breaking_change.commit_hash.dimmed()
163    )
164}
165
166/// Formats the change metrics
167fn format_metrics(metrics: &ChangeMetrics) -> String {
168    format!(
169        "- Total Commits: {}\n- Files Changed: {}\n- Insertions: {}\n- Deletions: {}\n",
170        metrics.total_commits.to_string().green(),
171        metrics.files_changed.to_string().yellow(),
172        metrics.insertions.to_string().green(),
173        metrics.deletions.to_string().red()
174    )
175}