1use super::prompt::{create_system_prompt, create_user_prompt};
2use super::review::GeneratedReview;
3use super::types::GeneratedMessage;
4use crate::config::Config;
5use crate::core::context::CommitContext;
6use crate::core::llm;
7use crate::core::token_optimizer::TokenOptimizer;
8use crate::debug;
9use crate::git::{CommitResult, GitRepo};
10use crate::instruction_presets::{PresetType, get_instruction_preset_library};
11
12use anyhow::Result;
13use std::path::Path;
14use std::sync::Arc;
15use tokio::sync::{RwLock, mpsc};
16
17pub struct CommitService {
19 config: Config,
20 repo: Arc<GitRepo>,
21 provider_name: String,
22 use_emoji: bool,
23 verify: bool,
24 cached_context: Arc<RwLock<Option<CommitContext>>>,
25}
26
27impl CommitService {
28 pub fn new(
43 config: Config,
44 _repo_path: &Path,
45 provider_name: &str,
46 use_emoji: 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_emoji,
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 && !include_unstaged
103 {
104 return Ok(context.clone());
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 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,
165 "anthropic" => 100_000,
166 "groq" => 32_000,
167 "openrouter" => 2_000_000,
168 "google" => 1_000_000,
169 _ => 8_000,
170 }
171 });
172
173 let optimizer = TokenOptimizer::new(token_limit).expect("Failed to create TokenOptimizer");
175 let system_tokens = optimizer.count_tokens(system_prompt);
176
177 debug!("Token limit: {}", token_limit);
178 debug!("System prompt tokens: {}", system_tokens);
179
180 let context_token_limit = token_limit.saturating_sub(system_tokens + 1000);
183 debug!("Available tokens for context: {}", context_token_limit);
184
185 let user_prompt_before = create_user_prompt_fn(&context);
187 let total_tokens_before = system_tokens + optimizer.count_tokens(&user_prompt_before);
188 debug!("Total tokens before optimization: {}", total_tokens_before);
189
190 context.optimize(context_token_limit);
192
193 let user_prompt = create_user_prompt_fn(&context);
194 let user_tokens = optimizer.count_tokens(&user_prompt);
195 let total_tokens = system_tokens + user_tokens;
196
197 debug!("User prompt tokens after optimization: {}", user_tokens);
198 debug!("Total tokens after optimization: {}", total_tokens);
199
200 let final_user_prompt = if total_tokens > token_limit {
203 debug!(
204 "Total tokens {} still exceeds limit {}, truncating user prompt",
205 total_tokens, token_limit
206 );
207 let max_user_tokens = token_limit.saturating_sub(system_tokens + 100);
208 optimizer
209 .truncate_string(&user_prompt, max_user_tokens)
210 .expect("Failed to truncate user prompt")
211 } else {
212 user_prompt
213 };
214
215 let final_tokens = system_tokens + optimizer.count_tokens(&final_user_prompt);
216 debug!(
217 "Final total tokens after potential truncation: {}",
218 final_tokens
219 );
220
221 (context, final_user_prompt)
222 }
223
224 pub async fn generate_message(
235 &self,
236 preset: &str,
237 instructions: &str,
238 ) -> anyhow::Result<GeneratedMessage> {
239 let mut config_clone = self.config.clone();
240
241 let effective_preset = if preset.is_empty() {
243 config_clone.instruction_preset = "default".to_string();
244 "default"
245 } else {
246 let library = get_instruction_preset_library();
247 if let Some(preset_info) = library.get_preset(preset) {
248 if preset_info.preset_type == PresetType::Review {
249 debug!(
250 "Warning: Preset '{}' is review-specific, not ideal for commits",
251 preset
252 );
253 }
254 config_clone.instruction_preset = preset.to_string();
255 preset
256 } else {
257 debug!("Preset '{}' not found, using default", preset);
258 config_clone.instruction_preset = "default".to_string();
259 "default"
260 }
261 };
262
263 config_clone.instructions = instructions.to_string();
264
265 let context = self.get_git_info().await?;
266
267 let system_prompt = create_system_prompt(&config_clone)?;
269
270 let (_, final_user_prompt) =
272 self.optimize_prompt(&config_clone, &system_prompt, context, create_user_prompt);
273
274 let mut generated_message = llm::get_message::<GeneratedMessage>(
275 &config_clone,
276 &self.provider_name,
277 &system_prompt,
278 &final_user_prompt,
279 )
280 .await?;
281
282 let should_use_emoji = self.use_emoji && effective_preset != "conventional";
284 if !should_use_emoji {
285 generated_message.emoji = None;
286 }
287
288 Ok(generated_message)
289 }
290
291 pub async fn generate_review_with_unstaged(
303 &self,
304 preset: &str,
305 instructions: &str,
306 include_unstaged: bool,
307 ) -> anyhow::Result<GeneratedReview> {
308 let mut config_clone = self.config.clone();
309
310 if preset.is_empty() {
312 config_clone.instruction_preset = "default".to_string();
313 } else {
314 let library = get_instruction_preset_library();
315 if let Some(preset_info) = library.get_preset(preset) {
316 if preset_info.preset_type == PresetType::Commit {
317 debug!(
318 "Warning: Preset '{}' is commit-specific, not ideal for reviews",
319 preset
320 );
321 }
322 config_clone.instruction_preset = preset.to_string();
323 } else {
324 debug!("Preset '{}' not found, using default", preset);
325 config_clone.instruction_preset = "default".to_string();
326 }
327 }
328
329 config_clone.instructions = instructions.to_string();
330
331 let context = self.get_git_info_with_unstaged(include_unstaged).await?;
333
334 let system_prompt = super::prompt::create_review_system_prompt(&config_clone)?;
336
337 let (_, final_user_prompt) = self.optimize_prompt(
339 &config_clone,
340 &system_prompt,
341 context,
342 super::prompt::create_review_user_prompt,
343 );
344
345 llm::get_message::<GeneratedReview>(
346 &config_clone,
347 &self.provider_name,
348 &system_prompt,
349 &final_user_prompt,
350 )
351 .await
352 }
353
354 pub async fn generate_review_for_commit(
366 &self,
367 preset: &str,
368 instructions: &str,
369 commit_id: &str,
370 ) -> anyhow::Result<GeneratedReview> {
371 let mut config_clone = self.config.clone();
372
373 if preset.is_empty() {
375 config_clone.instruction_preset = "default".to_string();
376 } else {
377 let library = get_instruction_preset_library();
378 if let Some(preset_info) = library.get_preset(preset) {
379 if preset_info.preset_type == PresetType::Commit {
380 debug!(
381 "Warning: Preset '{}' is commit-specific, not ideal for reviews",
382 preset
383 );
384 }
385 config_clone.instruction_preset = preset.to_string();
386 } else {
387 debug!("Preset '{}' not found, using default", preset);
388 config_clone.instruction_preset = "default".to_string();
389 }
390 }
391
392 config_clone.instructions = instructions.to_string();
393
394 let context = self.get_git_info_for_commit(commit_id).await?;
396
397 let system_prompt = super::prompt::create_review_system_prompt(&config_clone)?;
399
400 let (_, final_user_prompt) = self.optimize_prompt(
402 &config_clone,
403 &system_prompt,
404 context,
405 super::prompt::create_review_user_prompt,
406 );
407
408 llm::get_message::<GeneratedReview>(
409 &config_clone,
410 &self.provider_name,
411 &system_prompt,
412 &final_user_prompt,
413 )
414 .await
415 }
416
417 pub async fn generate_review_for_branch_diff(
430 &self,
431 preset: &str,
432 instructions: &str,
433 base_branch: &str,
434 target_branch: &str,
435 ) -> anyhow::Result<GeneratedReview> {
436 let mut config_clone = self.config.clone();
437
438 if preset.is_empty() {
440 config_clone.instruction_preset = "default".to_string();
441 } else {
442 let library = get_instruction_preset_library();
443 if let Some(preset_info) = library.get_preset(preset) {
444 if preset_info.preset_type == PresetType::Commit {
445 debug!(
446 "Warning: Preset '{}' is commit-specific, not ideal for reviews",
447 preset
448 );
449 }
450 config_clone.instruction_preset = preset.to_string();
451 } else {
452 debug!("Preset '{}' not found, using default", preset);
453 config_clone.instruction_preset = "default".to_string();
454 }
455 }
456
457 config_clone.instructions = instructions.to_string();
458
459 let context = self
461 .repo
462 .get_git_info_for_branch_diff(&self.config, base_branch, target_branch)
463 .await?;
464
465 let system_prompt = super::prompt::create_review_system_prompt(&config_clone)?;
467
468 let (_, final_user_prompt) = self.optimize_prompt(
470 &config_clone,
471 &system_prompt,
472 context,
473 super::prompt::create_review_user_prompt,
474 );
475
476 llm::get_message::<GeneratedReview>(
477 &config_clone,
478 &self.provider_name,
479 &system_prompt,
480 &final_user_prompt,
481 )
482 .await
483 }
484
485 pub async fn generate_review(
496 &self,
497 preset: &str,
498 instructions: &str,
499 ) -> anyhow::Result<GeneratedReview> {
500 let mut config_clone = self.config.clone();
501
502 if preset.is_empty() {
504 config_clone.instruction_preset = "default".to_string();
505 } else {
506 let library = get_instruction_preset_library();
507 if let Some(preset_info) = library.get_preset(preset) {
508 if preset_info.preset_type == PresetType::Commit {
509 debug!(
510 "Warning: Preset '{}' is commit-specific, not ideal for reviews",
511 preset
512 );
513 }
514 config_clone.instruction_preset = preset.to_string();
515 } else {
516 debug!("Preset '{}' not found, using default", preset);
517 config_clone.instruction_preset = "default".to_string();
518 }
519 }
520
521 config_clone.instructions = instructions.to_string();
522
523 let context = self.get_git_info().await?;
524
525 let system_prompt = super::prompt::create_review_system_prompt(&config_clone)?;
527
528 let (_, final_user_prompt) = self.optimize_prompt(
530 &config_clone,
531 &system_prompt,
532 context,
533 super::prompt::create_review_user_prompt,
534 );
535
536 llm::get_message::<GeneratedReview>(
537 &config_clone,
538 &self.provider_name,
539 &system_prompt,
540 &final_user_prompt,
541 )
542 .await
543 }
544
545 pub async fn generate_pr_for_commit_range(
558 &self,
559 preset: &str,
560 instructions: &str,
561 from: &str,
562 to: &str,
563 ) -> anyhow::Result<super::types::GeneratedPullRequest> {
564 let mut config_clone = self.config.clone();
565
566 if preset.is_empty() {
568 config_clone.instruction_preset = "default".to_string();
569 } else {
570 let library = get_instruction_preset_library();
571 if let Some(preset_info) = library.get_preset(preset) {
572 if preset_info.preset_type == PresetType::Commit {
573 debug!(
574 "Warning: Preset '{}' is commit-specific, may not be ideal for PRs",
575 preset
576 );
577 }
578 config_clone.instruction_preset = preset.to_string();
579 } else {
580 debug!("Preset '{}' not found, using default", preset);
581 config_clone.instruction_preset = "default".to_string();
582 }
583 }
584
585 config_clone.instructions = instructions.to_string();
586
587 let context = self
589 .repo
590 .get_git_info_for_commit_range(&self.config, from, to)
591 .await?;
592
593 let commit_messages = self.repo.get_commits_for_pr(from, to)?;
595
596 let system_prompt = super::prompt::create_pr_system_prompt(&config_clone)?;
598
599 let (_, final_user_prompt) =
601 self.optimize_prompt(&config_clone, &system_prompt, context, |ctx| {
602 super::prompt::create_pr_user_prompt(ctx, &commit_messages)
603 });
604
605 let mut generated_pr = llm::get_message::<super::types::GeneratedPullRequest>(
606 &config_clone,
607 &self.provider_name,
608 &system_prompt,
609 &final_user_prompt,
610 )
611 .await?;
612
613 if !self.use_emoji {
615 generated_pr.emoji = None;
616 }
617
618 Ok(generated_pr)
619 }
620
621 pub async fn generate_pr_for_branch_diff(
634 &self,
635 preset: &str,
636 instructions: &str,
637 base_branch: &str,
638 target_branch: &str,
639 ) -> anyhow::Result<super::types::GeneratedPullRequest> {
640 let mut config_clone = self.config.clone();
641
642 if preset.is_empty() {
644 config_clone.instruction_preset = "default".to_string();
645 } else {
646 let library = get_instruction_preset_library();
647 if let Some(preset_info) = library.get_preset(preset) {
648 if preset_info.preset_type == PresetType::Commit {
649 debug!(
650 "Warning: Preset '{}' is commit-specific, may not be ideal for PRs",
651 preset
652 );
653 }
654 config_clone.instruction_preset = preset.to_string();
655 } else {
656 debug!("Preset '{}' not found, using default", preset);
657 config_clone.instruction_preset = "default".to_string();
658 }
659 }
660
661 config_clone.instructions = instructions.to_string();
662
663 let context = self
665 .repo
666 .get_git_info_for_branch_diff(&self.config, base_branch, target_branch)
667 .await?;
668
669 let commit_messages = self.repo.get_commits_for_pr(base_branch, target_branch)?;
671
672 let system_prompt = super::prompt::create_pr_system_prompt(&config_clone)?;
674
675 let (_, final_user_prompt) =
677 self.optimize_prompt(&config_clone, &system_prompt, context, |ctx| {
678 super::prompt::create_pr_user_prompt(ctx, &commit_messages)
679 });
680
681 let mut generated_pr = llm::get_message::<super::types::GeneratedPullRequest>(
682 &config_clone,
683 &self.provider_name,
684 &system_prompt,
685 &final_user_prompt,
686 )
687 .await?;
688
689 if !self.use_emoji {
691 generated_pr.emoji = None;
692 }
693
694 Ok(generated_pr)
695 }
696
697 pub fn perform_commit(&self, message: &str) -> Result<CommitResult> {
707 if self.is_remote_repository() {
709 return Err(anyhow::anyhow!("Cannot commit to a remote repository"));
710 }
711
712 debug!("Performing commit with message: {}", message);
713
714 if !self.verify {
715 debug!("Skipping pre-commit hook (verify=false)");
716 return self.repo.commit(message);
717 }
718
719 debug!("Executing pre-commit hook");
721 if let Err(e) = self.repo.execute_hook("pre-commit") {
722 debug!("Pre-commit hook failed: {}", e);
723 return Err(e);
724 }
725 debug!("Pre-commit hook executed successfully");
726
727 match self.repo.commit(message) {
729 Ok(result) => {
730 debug!("Executing post-commit hook");
732 if let Err(e) = self.repo.execute_hook("post-commit") {
733 debug!("Post-commit hook failed: {}", e);
734 }
736 debug!("Commit performed successfully");
737 Ok(result)
738 }
739 Err(e) => {
740 debug!("Commit failed: {}", e);
741 Err(e)
742 }
743 }
744 }
745
746 pub fn pre_commit(&self) -> Result<()> {
748 if self.is_remote_repository() {
750 debug!("Skipping pre-commit hook for remote repository");
751 return Ok(());
752 }
753
754 if self.verify {
755 self.repo.execute_hook("pre-commit")
756 } else {
757 Ok(())
758 }
759 }
760
761 pub fn create_message_channel(
763 &self,
764 ) -> (
765 mpsc::Sender<Result<GeneratedMessage>>,
766 mpsc::Receiver<Result<GeneratedMessage>>,
767 ) {
768 mpsc::channel(1)
769 }
770}