1use anyhow::Result;
2use std::path::Path;
3use std::sync::Arc;
4use tokio::sync::{RwLock, mpsc};
5
6use super::prompt::{create_system_prompt, create_user_prompt, process_commit_message};
7use super::review::GeneratedReview;
8use super::types::GeneratedMessage;
9use crate::config::Config;
10use crate::context::CommitContext;
11use crate::git::{CommitResult, GitRepo};
12use crate::instruction_presets::{PresetType, get_instruction_preset_library};
13use crate::llm;
14use crate::log_debug;
15use crate::token_optimizer::TokenOptimizer;
16
17pub struct IrisCommitService {
19 config: Config,
20 repo: Arc<GitRepo>,
21 provider_name: String,
22 use_gitmoji: bool,
23 verify: bool,
24 cached_context: Arc<RwLock<Option<CommitContext>>>,
25}
26
27impl IrisCommitService {
28 pub fn new(
43 config: Config,
44 _repo_path: &Path,
45 provider_name: &str,
46 use_gitmoji: bool,
47 verify: bool,
48 git_repo: GitRepo,
49 ) -> Result<Self> {
50 Ok(Self {
51 config,
52 repo: Arc::new(git_repo),
53 provider_name: provider_name.to_string(),
54 use_gitmoji,
55 verify,
56 cached_context: Arc::new(RwLock::new(None)),
57 })
58 }
59
60 pub fn is_remote_repository(&self) -> bool {
62 self.repo.is_remote()
63 }
64
65 pub fn check_environment(&self) -> Result<()> {
67 self.config.check_environment()
68 }
69
70 pub async fn get_git_info(&self) -> Result<CommitContext> {
72 {
73 let cached_context = self.cached_context.read().await;
74 if let Some(context) = &*cached_context {
75 return Ok(context.clone());
76 }
77 }
78
79 let context = self.repo.get_git_info(&self.config).await?;
80
81 {
82 let mut cached_context = self.cached_context.write().await;
83 *cached_context = Some(context.clone());
84 }
85 Ok(context)
86 }
87
88 pub async fn get_git_info_with_unstaged(
90 &self,
91 include_unstaged: bool,
92 ) -> Result<CommitContext> {
93 if !include_unstaged {
94 return self.get_git_info().await;
95 }
96
97 {
98 let cached_context = self.cached_context.read().await;
101 if let Some(context) = &*cached_context {
102 if !include_unstaged {
103 return Ok(context.clone());
104 }
105 }
106 }
107
108 let context = self
109 .repo
110 .get_git_info_with_unstaged(&self.config, include_unstaged)
111 .await?;
112
113 if !include_unstaged {
115 let mut cached_context = self.cached_context.write().await;
116 *cached_context = Some(context.clone());
117 }
118
119 Ok(context)
120 }
121
122 pub async fn get_git_info_for_commit(&self, commit_id: &str) -> Result<CommitContext> {
124 log_debug!("Getting git info for commit: {}", commit_id);
125
126 let context = self
127 .repo
128 .get_git_info_for_commit(&self.config, commit_id)
129 .await?;
130
131 Ok(context)
133 }
134
135 fn optimize_prompt<F>(
148 &self,
149 config_clone: &Config,
150 system_prompt: &str,
151 mut context: CommitContext,
152 create_user_prompt_fn: F,
153 ) -> (CommitContext, String)
154 where
155 F: Fn(&CommitContext) -> String,
156 {
157 let token_limit = config_clone
159 .providers
160 .get(&self.provider_name)
161 .and_then(|p| p.token_limit)
162 .unwrap_or({
163 match self.provider_name.as_str() {
164 "openai" => 16_000, "anthropic" => 100_000, "google" | "groq" => 32_000, _ => 8_000, }
169 });
170
171 let optimizer = TokenOptimizer::new(token_limit);
173 let system_tokens = optimizer.count_tokens(system_prompt);
174
175 log_debug!("Token limit: {}", token_limit);
176 log_debug!("System prompt tokens: {}", system_tokens);
177
178 let context_token_limit = token_limit.saturating_sub(system_tokens + 1000);
181 log_debug!("Available tokens for context: {}", context_token_limit);
182
183 let user_prompt_before = create_user_prompt_fn(&context);
185 let total_tokens_before = system_tokens + optimizer.count_tokens(&user_prompt_before);
186 log_debug!("Total tokens before optimization: {}", total_tokens_before);
187
188 context.optimize(context_token_limit);
190
191 let user_prompt = create_user_prompt_fn(&context);
192 let user_tokens = optimizer.count_tokens(&user_prompt);
193 let total_tokens = system_tokens + user_tokens;
194
195 log_debug!("User prompt tokens after optimization: {}", user_tokens);
196 log_debug!("Total tokens after optimization: {}", total_tokens);
197
198 let final_user_prompt = if total_tokens > token_limit {
201 log_debug!(
202 "Total tokens {} still exceeds limit {}, truncating user prompt",
203 total_tokens,
204 token_limit
205 );
206 let max_user_tokens = token_limit.saturating_sub(system_tokens + 100);
207 optimizer.truncate_string(&user_prompt, max_user_tokens)
208 } else {
209 user_prompt
210 };
211
212 let final_tokens = system_tokens + optimizer.count_tokens(&final_user_prompt);
213 log_debug!(
214 "Final total tokens after potential truncation: {}",
215 final_tokens
216 );
217
218 (context, final_user_prompt)
219 }
220
221 pub async fn generate_message(
232 &self,
233 preset: &str,
234 instructions: &str,
235 ) -> anyhow::Result<GeneratedMessage> {
236 let mut config_clone = self.config.clone();
237
238 if preset.is_empty() {
240 config_clone.instruction_preset = "default".to_string();
241 } else {
242 let library = get_instruction_preset_library();
243 if let Some(preset_info) = library.get_preset(preset) {
244 if preset_info.preset_type == PresetType::Review {
245 log_debug!(
246 "Warning: Preset '{}' is review-specific, not ideal for commits",
247 preset
248 );
249 }
250 config_clone.instruction_preset = preset.to_string();
251 } else {
252 log_debug!("Preset '{}' not found, using default", preset);
253 config_clone.instruction_preset = "default".to_string();
254 }
255 }
256
257 config_clone.instructions = instructions.to_string();
258
259 let context = self.get_git_info().await?;
260
261 let system_prompt = create_system_prompt(&config_clone)?;
263
264 let (_, final_user_prompt) =
266 self.optimize_prompt(&config_clone, &system_prompt, context, create_user_prompt);
267
268 let mut generated_message = llm::get_message::<GeneratedMessage>(
269 &config_clone,
270 &self.provider_name,
271 &system_prompt,
272 &final_user_prompt,
273 )
274 .await?;
275
276 if !self.use_gitmoji {
278 generated_message.emoji = None;
279 }
280
281 Ok(generated_message)
282 }
283
284 pub async fn generate_review_with_unstaged(
296 &self,
297 preset: &str,
298 instructions: &str,
299 include_unstaged: bool,
300 ) -> anyhow::Result<GeneratedReview> {
301 let mut config_clone = self.config.clone();
302
303 if preset.is_empty() {
305 config_clone.instruction_preset = "default".to_string();
306 } else {
307 let library = get_instruction_preset_library();
308 if let Some(preset_info) = library.get_preset(preset) {
309 if preset_info.preset_type == PresetType::Commit {
310 log_debug!(
311 "Warning: Preset '{}' is commit-specific, not ideal for reviews",
312 preset
313 );
314 }
315 config_clone.instruction_preset = preset.to_string();
316 } else {
317 log_debug!("Preset '{}' not found, using default", preset);
318 config_clone.instruction_preset = "default".to_string();
319 }
320 }
321
322 config_clone.instructions = instructions.to_string();
323
324 let context = self.get_git_info_with_unstaged(include_unstaged).await?;
326
327 let system_prompt = super::prompt::create_review_system_prompt(&config_clone)?;
329
330 let (_, final_user_prompt) = self.optimize_prompt(
332 &config_clone,
333 &system_prompt,
334 context,
335 super::prompt::create_review_user_prompt,
336 );
337
338 llm::get_message::<GeneratedReview>(
339 &config_clone,
340 &self.provider_name,
341 &system_prompt,
342 &final_user_prompt,
343 )
344 .await
345 }
346
347 pub async fn generate_review_for_commit(
359 &self,
360 preset: &str,
361 instructions: &str,
362 commit_id: &str,
363 ) -> anyhow::Result<GeneratedReview> {
364 let mut config_clone = self.config.clone();
365
366 if preset.is_empty() {
368 config_clone.instruction_preset = "default".to_string();
369 } else {
370 let library = get_instruction_preset_library();
371 if let Some(preset_info) = library.get_preset(preset) {
372 if preset_info.preset_type == PresetType::Commit {
373 log_debug!(
374 "Warning: Preset '{}' is commit-specific, not ideal for reviews",
375 preset
376 );
377 }
378 config_clone.instruction_preset = preset.to_string();
379 } else {
380 log_debug!("Preset '{}' not found, using default", preset);
381 config_clone.instruction_preset = "default".to_string();
382 }
383 }
384
385 config_clone.instructions = instructions.to_string();
386
387 let context = self.get_git_info_for_commit(commit_id).await?;
389
390 let system_prompt = super::prompt::create_review_system_prompt(&config_clone)?;
392
393 let (_, final_user_prompt) = self.optimize_prompt(
395 &config_clone,
396 &system_prompt,
397 context,
398 super::prompt::create_review_user_prompt,
399 );
400
401 llm::get_message::<GeneratedReview>(
402 &config_clone,
403 &self.provider_name,
404 &system_prompt,
405 &final_user_prompt,
406 )
407 .await
408 }
409
410 pub async fn generate_review(
421 &self,
422 preset: &str,
423 instructions: &str,
424 ) -> anyhow::Result<GeneratedReview> {
425 let mut config_clone = self.config.clone();
426
427 if preset.is_empty() {
429 config_clone.instruction_preset = "default".to_string();
430 } else {
431 let library = get_instruction_preset_library();
432 if let Some(preset_info) = library.get_preset(preset) {
433 if preset_info.preset_type == PresetType::Commit {
434 log_debug!(
435 "Warning: Preset '{}' is commit-specific, not ideal for reviews",
436 preset
437 );
438 }
439 config_clone.instruction_preset = preset.to_string();
440 } else {
441 log_debug!("Preset '{}' not found, using default", preset);
442 config_clone.instruction_preset = "default".to_string();
443 }
444 }
445
446 config_clone.instructions = instructions.to_string();
447
448 let context = self.get_git_info().await?;
449
450 let system_prompt = super::prompt::create_review_system_prompt(&config_clone)?;
452
453 let (_, final_user_prompt) = self.optimize_prompt(
455 &config_clone,
456 &system_prompt,
457 context,
458 super::prompt::create_review_user_prompt,
459 );
460
461 llm::get_message::<GeneratedReview>(
462 &config_clone,
463 &self.provider_name,
464 &system_prompt,
465 &final_user_prompt,
466 )
467 .await
468 }
469
470 pub fn perform_commit(&self, message: &str) -> Result<CommitResult> {
480 if self.is_remote_repository() {
482 return Err(anyhow::anyhow!("Cannot commit to a remote repository"));
483 }
484
485 let processed_message = process_commit_message(message.to_string(), self.use_gitmoji);
486 log_debug!("Performing commit with message: {}", processed_message);
487
488 if !self.verify {
489 log_debug!("Skipping pre-commit hook (verify=false)");
490 return self.repo.commit(&processed_message);
491 }
492
493 log_debug!("Executing pre-commit hook");
495 if let Err(e) = self.repo.execute_hook("pre-commit") {
496 log_debug!("Pre-commit hook failed: {}", e);
497 return Err(e);
498 }
499 log_debug!("Pre-commit hook executed successfully");
500
501 match self.repo.commit(&processed_message) {
503 Ok(result) => {
504 log_debug!("Executing post-commit hook");
506 if let Err(e) = self.repo.execute_hook("post-commit") {
507 log_debug!("Post-commit hook failed: {}", e);
508 }
510 log_debug!("Commit performed successfully");
511 Ok(result)
512 }
513 Err(e) => {
514 log_debug!("Commit failed: {}", e);
515 Err(e)
516 }
517 }
518 }
519
520 pub fn pre_commit(&self) -> Result<()> {
522 if self.is_remote_repository() {
524 log_debug!("Skipping pre-commit hook for remote repository");
525 return Ok(());
526 }
527
528 if self.verify {
529 self.repo.execute_hook("pre-commit")
530 } else {
531 Ok(())
532 }
533 }
534
535 pub fn create_message_channel(
537 &self,
538 ) -> (
539 mpsc::Sender<Result<GeneratedMessage>>,
540 mpsc::Receiver<Result<GeneratedMessage>>,
541 ) {
542 mpsc::channel(1)
543 }
544}