git_iris/changes/
changelog.rs

1use 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
11/// Struct responsible for generating changelogs
12pub struct ChangelogGenerator;
13
14impl ChangelogGenerator {
15    /// Generates a changelog for the specified range of commits.
16    ///
17    /// # Arguments
18    ///
19    /// * `git_repo` - `GitRepo` instance
20    /// * `from` - Starting point for the changelog (e.g., a commit hash or tag)
21    /// * `to` - Ending point for the changelog (e.g., a commit hash, tag, or "HEAD")
22    /// * `config` - Configuration object containing LLM settings
23    /// * `detail_level` - Level of detail for the changelog (Minimal, Standard, or Detailed)
24    ///
25    /// # Returns
26    ///
27    /// A Result containing the generated changelog as a String, or an error
28    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
50/// Formats the `ChangelogResponse` into a human-readable changelog
51fn format_changelog_response(response: &ChangelogResponse) -> String {
52    let mut formatted = String::new();
53
54    // Add header
55    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    // Add version and release date
63    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    // Add changes grouped by type
75    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    // Add breaking changes
86    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    // Add metrics
100    formatted.push_str(&"### 📊 Metrics\n\n".bright_magenta().bold().to_string());
101    formatted.push_str(&format_metrics(&response.metrics));
102
103    formatted
104}
105
106/// Formats a change type with an appropriate emoji
107fn 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
119/// Formats a single change entry
120fn 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
139/// Formats a breaking change
140fn format_breaking_change(breaking_change: &BreakingChange) -> String {
141    format!(
142        "- {} ({})\n",
143        breaking_change.description,
144        breaking_change.commit_hash.dimmed()
145    )
146}
147
148/// Formats the change metrics
149fn 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}