1use crate::token_optimizer::TokenOptimizer;
2use colored::Colorize;
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use textwrap::wrap;
7
8#[derive(Serialize, Debug, Clone)]
9pub struct CommitContext {
10 pub branch: String,
11 pub recent_commits: Vec<RecentCommit>,
12 pub staged_files: Vec<StagedFile>,
13 pub project_metadata: ProjectMetadata,
14 pub user_name: String,
15 pub user_email: String,
16}
17
18#[derive(Serialize, Debug, Clone)]
19pub struct RecentCommit {
20 pub hash: String,
21 pub message: String,
22 pub author: String,
23 pub timestamp: String,
24}
25
26#[derive(Serialize, Debug, Clone)]
27pub struct StagedFile {
28 pub path: String,
29 pub change_type: ChangeType,
30 pub diff: String,
31 pub analysis: Vec<String>,
32 pub content: Option<String>,
33 pub content_excluded: bool,
34}
35
36#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
38pub struct GeneratedMessage {
39 pub emoji: Option<String>,
41 pub title: String,
43 pub message: String,
45}
46
47#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
49pub struct GeneratedReview {
50 pub summary: String,
52 pub code_quality: String,
54 pub suggestions: Vec<String>,
56 pub issues: Vec<String>,
58 pub positive_aspects: Vec<String>,
60}
61
62impl From<String> for GeneratedMessage {
63 fn from(s: String) -> Self {
64 match serde_json::from_str(&s) {
65 Ok(message) => message,
66 Err(e) => {
67 eprintln!("Failed to parse JSON: {e}\nInput was: {s}");
68 Self {
69 emoji: None,
70 title: "Error parsing commit message".to_string(),
71 message: "There was an error parsing the commit message from the AI. Please try again.".to_string(),
72 }
73 }
74 }
75 }
76}
77
78impl From<String> for GeneratedReview {
79 fn from(s: String) -> Self {
80 match serde_json::from_str(&s) {
81 Ok(review) => review,
82 Err(e) => {
83 crate::log_debug!("Failed to parse review JSON: {}", e);
84 crate::log_debug!("Input was: {}", s);
85 Self {
86 summary: "Error parsing code review".to_string(),
87 code_quality: "There was an error parsing the code review from the AI."
88 .to_string(),
89 suggestions: vec!["Please try again.".to_string()],
90 issues: vec![],
91 positive_aspects: vec![],
92 }
93 }
94 }
95 }
96}
97
98#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
99pub enum ChangeType {
100 Added,
101 Modified,
102 Deleted,
103}
104
105impl fmt::Display for ChangeType {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 match self {
108 Self::Added => write!(f, "Added"),
109 Self::Modified => write!(f, "Modified"),
110 Self::Deleted => write!(f, "Deleted"),
111 }
112 }
113}
114
115#[derive(Serialize, Debug, Clone, Default)]
116pub struct ProjectMetadata {
117 pub language: Option<String>,
118 pub framework: Option<String>,
119 pub dependencies: Vec<String>,
120 pub version: Option<String>,
121 pub build_system: Option<String>,
122 pub test_framework: Option<String>,
123 pub plugins: Vec<String>,
124}
125
126impl ProjectMetadata {
127 pub fn merge(&mut self, new: ProjectMetadata) {
128 if let Some(new_lang) = new.language {
129 match &mut self.language {
130 Some(lang) if !lang.contains(&new_lang) => {
131 lang.push_str(", ");
132 lang.push_str(&new_lang);
133 }
134 None => self.language = Some(new_lang),
135 _ => {}
136 }
137 }
138 self.dependencies.extend(new.dependencies.clone());
139 self.framework = self.framework.take().or(new.framework);
140 self.version = self.version.take().or(new.version);
141 self.build_system = self.build_system.take().or(new.build_system);
142 self.test_framework = self.test_framework.take().or(new.test_framework);
143 self.plugins.extend(new.plugins);
144 self.dependencies.sort();
145 self.dependencies.dedup();
146 }
147}
148
149impl CommitContext {
150 pub fn new(
151 branch: String,
152 recent_commits: Vec<RecentCommit>,
153 staged_files: Vec<StagedFile>,
154 project_metadata: ProjectMetadata,
155 user_name: String,
156 user_email: String,
157 ) -> Self {
158 Self {
159 branch,
160 recent_commits,
161 staged_files,
162 project_metadata,
163 user_name,
164 user_email,
165 }
166 }
167 pub fn optimize(&mut self, max_tokens: usize) {
168 let optimizer = TokenOptimizer::new(max_tokens);
169 optimizer.optimize_context(self);
170 }
171}
172
173pub fn format_commit_message(response: &GeneratedMessage) -> String {
175 let mut message = String::new();
176
177 if let Some(emoji) = &response.emoji {
178 message.push_str(&format!("{emoji} "));
179 }
180
181 message.push_str(&response.title);
182 message.push_str("\n\n");
183
184 let wrapped_message = wrap(&response.message, 78);
185 for line in wrapped_message {
186 message.push_str(&line);
187 message.push('\n');
188 }
189
190 message
191}
192
193impl GeneratedReview {
194 pub fn format(&self) -> String {
196 let mut formatted = String::new();
197
198 formatted.push_str(&format!(
199 "{}\n\n{}\n\n",
200 "✨ Code Review Summary ✨".bright_magenta().bold(),
201 self.summary.bright_white()
202 ));
203
204 formatted.push_str(&format!(
205 "{}\n\n{}\n\n",
206 "🔍 Code Quality Assessment".bright_cyan().bold(),
207 self.code_quality.bright_white()
208 ));
209
210 if !self.positive_aspects.is_empty() {
211 formatted.push_str(&format!("{}\n\n", "✅ Positive Aspects".green().bold()));
212 for (i, aspect) in self.positive_aspects.iter().enumerate() {
213 formatted.push_str(&format!("{}. {}\n", i + 1, aspect.green()));
214 }
215 formatted.push('\n');
216 }
217
218 if !self.issues.is_empty() {
219 formatted.push_str(&format!("{}\n\n", "❌ Issues Identified".yellow().bold()));
220 for (i, issue) in self.issues.iter().enumerate() {
221 formatted.push_str(&format!("{}. {}\n", i + 1, issue.yellow()));
222 }
223 formatted.push('\n');
224 }
225
226 if !self.suggestions.is_empty() {
227 formatted.push_str(&format!(
228 "{}\n\n",
229 "💡 Suggestions for Improvement".bright_blue().bold()
230 ));
231 for (i, suggestion) in self.suggestions.iter().enumerate() {
232 formatted.push_str(&format!("{}. {}\n", i + 1, suggestion.bright_blue()));
233 }
234 }
235
236 formatted
237 }
238}