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