1use crate::token_optimizer::TokenOptimizer;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use textwrap::wrap;
6
7#[derive(Serialize, Debug, Clone)]
8pub struct CommitContext {
9 pub branch: String,
10 pub recent_commits: Vec<RecentCommit>,
11 pub staged_files: Vec<StagedFile>,
12 pub project_metadata: ProjectMetadata,
13 pub user_name: String,
14 pub user_email: String,
15}
16
17#[derive(Serialize, Debug, Clone)]
18pub struct RecentCommit {
19 pub hash: String,
20 pub message: String,
21 pub author: String,
22 pub timestamp: String,
23}
24
25#[derive(Serialize, Debug, Clone)]
26pub struct StagedFile {
27 pub path: String,
28 pub change_type: ChangeType,
29 pub diff: String,
30 pub analysis: Vec<String>,
31 pub content: Option<String>,
32 pub content_excluded: bool,
33}
34
35#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
36pub struct GeneratedMessage {
37 pub emoji: Option<String>,
38 pub title: String,
39 pub message: String,
40}
41
42impl From<String> for GeneratedMessage {
43 #[allow(clippy::unwrap_used)] fn from(value: String) -> Self {
45 serde_json::from_str(&value).unwrap()
46 }
47}
48
49#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
50pub enum ChangeType {
51 Added,
52 Modified,
53 Deleted,
54}
55
56impl fmt::Display for ChangeType {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 match self {
59 Self::Added => write!(f, "Added"),
60 Self::Modified => write!(f, "Modified"),
61 Self::Deleted => write!(f, "Deleted"),
62 }
63 }
64}
65
66#[derive(Serialize, Debug, Clone, Default)]
67pub struct ProjectMetadata {
68 pub language: Option<String>,
69 pub framework: Option<String>,
70 pub dependencies: Vec<String>,
71 pub version: Option<String>,
72 pub build_system: Option<String>,
73 pub test_framework: Option<String>,
74 pub plugins: Vec<String>,
75}
76
77impl ProjectMetadata {
78 pub fn merge(&mut self, new: ProjectMetadata) {
79 if let Some(new_lang) = new.language {
80 match &mut self.language {
81 Some(lang) if !lang.contains(&new_lang) => {
82 lang.push_str(", ");
83 lang.push_str(&new_lang);
84 }
85 None => self.language = Some(new_lang),
86 _ => {}
87 }
88 }
89 self.dependencies.extend(new.dependencies.clone());
90 self.framework = self.framework.take().or(new.framework);
91 self.version = self.version.take().or(new.version);
92 self.build_system = self.build_system.take().or(new.build_system);
93 self.test_framework = self.test_framework.take().or(new.test_framework);
94 self.plugins.extend(new.plugins);
95 self.dependencies.sort();
96 self.dependencies.dedup();
97 }
98}
99
100impl CommitContext {
101 pub fn new(
102 branch: String,
103 recent_commits: Vec<RecentCommit>,
104 staged_files: Vec<StagedFile>,
105 project_metadata: ProjectMetadata,
106 user_name: String,
107 user_email: String,
108 ) -> Self {
109 Self {
110 branch,
111 recent_commits,
112 staged_files,
113 project_metadata,
114 user_name,
115 user_email,
116 }
117 }
118 pub fn optimize(&mut self, max_tokens: usize) {
119 let optimizer = TokenOptimizer::new(max_tokens);
120 optimizer.optimize_context(self);
121 }
122}
123
124pub fn format_commit_message(response: &GeneratedMessage) -> String {
125 let mut message = String::new();
126
127 if let Some(emoji) = &response.emoji {
128 message.push_str(&format!("{emoji} "));
129 }
130
131 message.push_str(&response.title);
132 message.push_str("\n\n");
133
134 let wrapped_message = wrap(&response.message, 78);
135 for line in wrapped_message {
136 message.push_str(&line);
137 message.push('\n');
138 }
139
140 message
141}