gitai/features/commit/
review.rs

1use super::service::CommitService;
2use crate::common::CommonParams;
3use crate::config::Config;
4use crate::core::messages;
5use crate::git::GitRepo;
6use crate::instruction_presets::PresetType;
7use crate::ui;
8
9use anyhow::{Context, Result};
10use colored::Colorize;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use std::fmt::Write;
14use std::sync::Arc;
15use textwrap;
16
17/// Width in characters for wrapping explanations in code reviews
18const EXPLANATION_WRAP_WIDTH: usize = 80;
19
20/// Represents a specific issue found during code review
21#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
22pub struct CodeIssue {
23    /// Brief description of the issue
24    pub description: String,
25    /// Severity level of the issue (Critical, High, Medium, Low)
26    pub severity: String,
27    /// Location of the issue, preferably in "`filename.rs:line_numbers`" format
28    /// or "`path/to/file.rs:line_numbers`" format for better readability
29    pub location: String,
30    /// Detailed explanation of why this is problematic
31    pub explanation: String,
32    /// Specific suggestion to address the issue
33    pub recommendation: String,
34}
35
36/// Represents analysis for a specific code quality dimension
37#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
38pub struct DimensionAnalysis {
39    /// Whether issues were found in this dimension
40    pub issues_found: bool,
41    /// List of specific issues identified in this dimension
42    pub issues: Vec<CodeIssue>,
43}
44
45/// Represents the different dimensions of code quality analysis
46#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
47pub enum QualityDimension {
48    /// Unnecessary complexity in algorithms, abstractions, or control flow
49    Complexity,
50    /// Poor or inappropriate abstractions, design patterns or separation of concerns
51    Abstraction,
52    /// Unintended deletion of code or functionality without proper replacement
53    Deletion,
54    /// References to non-existent components, APIs, or behaviors
55    Hallucination,
56    /// Inconsistencies in code style, naming, or formatting
57    Style,
58    /// Security vulnerabilities or insecure coding practices
59    Security,
60    /// Inefficient algorithms, operations, or resource usage
61    Performance,
62    /// Repeated logic, functionality, or copy-pasted code
63    Duplication,
64    /// Insufficient or improper error handling and recovery
65    ErrorHandling,
66    /// Gaps in test coverage or tests that miss critical scenarios
67    Testing,
68    /// Violations of established best practices or coding standards
69    BestPractices,
70}
71
72impl QualityDimension {
73    /// Get all quality dimensions
74    pub fn all() -> &'static [QualityDimension] {
75        &[
76            QualityDimension::Complexity,
77            QualityDimension::Abstraction,
78            QualityDimension::Deletion,
79            QualityDimension::Hallucination,
80            QualityDimension::Style,
81            QualityDimension::Security,
82            QualityDimension::Performance,
83            QualityDimension::Duplication,
84            QualityDimension::ErrorHandling,
85            QualityDimension::Testing,
86            QualityDimension::BestPractices,
87        ]
88    }
89
90    /// Get the display name for a dimension
91    pub fn display_name(&self) -> &'static str {
92        match self {
93            QualityDimension::Complexity => "Complexity",
94            QualityDimension::Abstraction => "Abstraction",
95            QualityDimension::Deletion => "Unintended Deletion",
96            QualityDimension::Hallucination => "Hallucinated Components",
97            QualityDimension::Style => "Style Inconsistencies",
98            QualityDimension::Security => "Security Vulnerabilities",
99            QualityDimension::Performance => "Performance Issues",
100            QualityDimension::Duplication => "Code Duplication",
101            QualityDimension::ErrorHandling => "Error Handling",
102            QualityDimension::Testing => "Test Coverage",
103            QualityDimension::BestPractices => "Best Practices",
104        }
105    }
106
107    /// Get the description for a dimension
108    #[allow(clippy::too_many_lines)]
109    pub fn description(&self) -> &'static str {
110        match self {
111            QualityDimension::Complexity => {
112                "
113            **Unnecessary Complexity**
114            - Overly complex algorithms or functions
115            - Unnecessary abstraction layers
116            - Convoluted control flow
117            - Functions/methods that are too long or have too many parameters
118            - Nesting levels that are too deep
119            "
120            }
121            QualityDimension::Abstraction => {
122                "
123            **Poor Abstractions**
124            - Inappropriate use of design patterns
125            - Missing abstractions where needed
126            - Leaky abstractions that expose implementation details
127            - Overly generic abstractions that add complexity
128            - Unclear separation of concerns
129            "
130            }
131            QualityDimension::Deletion => {
132                "
133            **Unintended Code Deletion**
134            - Critical functionality removed without replacement
135            - Incomplete removal of deprecated code
136            - Breaking changes to public APIs
137            - Removed error handling or validation
138            - Missing edge case handling present in original code
139            "
140            }
141            QualityDimension::Hallucination => {
142                "
143            **Hallucinated Components**
144            - References to non-existent functions, classes, or modules
145            - Assumptions about available libraries or APIs
146            - Inconsistent or impossible behavior expectations
147            - References to frameworks or patterns not used in the project
148            - Creation of interfaces that don't align with the codebase
149            "
150            }
151            QualityDimension::Style => {
152                "
153            **Style Inconsistencies**
154            - Deviation from project coding standards
155            - Inconsistent naming conventions
156            - Inconsistent formatting or indentation
157            - Inconsistent comment styles or documentation
158            - Mixing of different programming paradigms
159            "
160            }
161            QualityDimension::Security => {
162                "
163            **Security Vulnerabilities**
164            - Injection vulnerabilities (SQL, Command, etc.)
165            - Insecure data handling or storage
166            - Authentication or authorization flaws
167            - Exposure of sensitive information
168            - Unsafe dependencies or API usage
169            "
170            }
171            QualityDimension::Performance => {
172                "
173            **Performance Issues**
174            - Inefficient algorithms or data structures
175            - Unnecessary computations or operations
176            - Resource leaks (memory, file handles, etc.)
177            - Excessive network or disk operations
178            - Blocking operations in asynchronous code
179            "
180            }
181            QualityDimension::Duplication => {
182                "
183            **Code Duplication**
184            - Repeated logic or functionality
185            - Copy-pasted code with minor variations
186            - Duplicate functionality across different modules
187            - Redundant validation or error handling
188            - Parallel hierarchies or structures
189            "
190            }
191            QualityDimension::ErrorHandling => {
192                "
193            **Incomplete Error Handling**
194            - Missing try-catch blocks for risky operations
195            - Overly broad exception handling
196            - Swallowed exceptions without proper logging
197            - Unclear error messages or codes
198            - Inconsistent error recovery strategies
199            "
200            }
201            QualityDimension::Testing => {
202                "
203            **Test Coverage Gaps**
204            - Missing unit tests for critical functionality
205            - Uncovered edge cases or error paths
206            - Brittle tests that make inappropriate assumptions
207            - Missing integration or system tests
208            - Tests that don't verify actual requirements
209            "
210            }
211            QualityDimension::BestPractices => {
212                "
213            **Best Practices Violations**
214            - Not following language-specific idioms and conventions
215            - Violation of SOLID principles or other design guidelines
216            - Anti-patterns or known problematic implementation approaches
217            - Ignored compiler/linter warnings
218            - Outdated or deprecated APIs and practices
219            "
220            }
221        }
222    }
223}
224
225/// Model for code review generation results
226#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
227pub struct GeneratedReview {
228    /// Brief summary of the code changes and overall review
229    pub summary: String,
230    /// Detailed assessment of the overall code quality
231    pub code_quality: String,
232    /// List of specific suggestions for improving the code
233    pub suggestions: Vec<String>,
234    /// List of identified issues or problems in the code
235    pub issues: Vec<String>,
236    /// List of positive aspects or good practices in the code
237    pub positive_aspects: Vec<String>,
238    /// Analysis of unnecessary complexity issues
239    pub complexity: Option<DimensionAnalysis>,
240    /// Analysis of abstraction quality issues
241    pub abstraction: Option<DimensionAnalysis>,
242    /// Analysis of unintended code deletion
243    pub deletion: Option<DimensionAnalysis>,
244    /// Analysis of hallucinated components that don't exist
245    pub hallucination: Option<DimensionAnalysis>,
246    /// Analysis of style inconsistencies
247    pub style: Option<DimensionAnalysis>,
248    /// Analysis of security vulnerabilities
249    pub security: Option<DimensionAnalysis>,
250    /// Analysis of performance issues
251    pub performance: Option<DimensionAnalysis>,
252    /// Analysis of code duplication
253    pub duplication: Option<DimensionAnalysis>,
254    /// Analysis of error handling completeness
255    pub error_handling: Option<DimensionAnalysis>,
256    /// Analysis of test coverage gaps
257    pub testing: Option<DimensionAnalysis>,
258    /// Analysis of best practices violations
259    pub best_practices: Option<DimensionAnalysis>,
260}
261
262impl GeneratedReview {
263    /// Validates if the location string is parseable for better error handling
264    pub fn format_location(location: &str) -> String {
265        // If it already contains keywords like "line", "file", or "in", return as-is
266        if location.to_lowercase().contains("line")
267            || location.to_lowercase().contains("file")
268            || location.to_lowercase().contains(" in ")
269        {
270            return location.to_string();
271        }
272
273        // If it looks like a file path (contains "/" or "\\" and ":"), return as-is
274        if location.contains(':') && (location.contains('/') || location.contains('\\')) {
275            location.to_string()
276        } else if location.contains(':') {
277            // Treat as file:line_numbers format without path separators
278            format!("in {location}")
279        } else if location.contains('.')
280            && location
281                .split('.')
282                .next_back()
283                .is_some_and(|ext| !ext.is_empty())
284        {
285            // Looks like a filename with extension, return as-is
286            location.to_string()
287        } else {
288            // Treat as just line numbers - explicitly mention it's line numbers
289            format!("Line(s) {location}")
290        }
291    }
292
293    /// Formats the review into a readable string with colors and emojis for terminal display
294    pub fn format(&self) -> String {
295        let mut formatted = String::new();
296
297        Self::format_header(&mut formatted, &self.summary, &self.code_quality);
298        Self::format_positive_aspects(&mut formatted, &self.positive_aspects);
299        Self::format_issues(&mut formatted, &self.issues);
300        Self::format_all_dimension_analyses(&mut formatted, self);
301        Self::format_suggestions(&mut formatted, &self.suggestions);
302
303        formatted
304    }
305
306    /// Formats the header section with title, summary, and quality assessment
307    fn format_header(formatted: &mut String, summary: &str, code_quality: &str) {
308        write!(
309            formatted,
310            "{}\n\n{}\n\n",
311            "✧・゚: *✧・゚ CODE REVIEW ✧・゚: *✧・゚".bright_magenta().bold(),
312            summary.bright_white()
313        )
314        .expect("write to string should not fail");
315
316        write!(
317            formatted,
318            "{}\n\n{}\n\n",
319            "◤ QUALITY ASSESSMENT ◢".bright_cyan().bold(),
320            code_quality.bright_white()
321        )
322        .expect("write to string should not fail");
323    }
324
325    /// Formats the positive aspects section
326    fn format_positive_aspects(formatted: &mut String, positive_aspects: &[String]) {
327        if !positive_aspects.is_empty() {
328            write!(formatted, "{}\n\n", "✅ STRENGTHS //".green().bold())
329                .expect("write to string should not fail");
330            for aspect in positive_aspects {
331                writeln!(formatted, "  {} {}", "•".bright_green(), aspect.green())
332                    .expect("write to string should not fail");
333            }
334            formatted.push('\n');
335        }
336    }
337
338    /// Formats the issues section
339    fn format_issues(formatted: &mut String, issues: &[String]) {
340        if !issues.is_empty() {
341            write!(formatted, "{}\n\n", "⚠️ CORE ISSUES //".yellow().bold())
342                .expect("write to string should not fail");
343            for issue in issues {
344                writeln!(formatted, "  {} {}", "•".bright_yellow(), issue.yellow())
345                    .expect("write to string should not fail");
346            }
347            formatted.push('\n');
348        }
349    }
350
351    /// Formats all dimension-specific analyses
352    fn format_all_dimension_analyses(formatted: &mut String, review: &GeneratedReview) {
353        Self::format_dimension_analysis(
354            formatted,
355            QualityDimension::Complexity,
356            review.complexity.as_ref(),
357        );
358        Self::format_dimension_analysis(
359            formatted,
360            QualityDimension::Abstraction,
361            review.abstraction.as_ref(),
362        );
363        Self::format_dimension_analysis(
364            formatted,
365            QualityDimension::Deletion,
366            review.deletion.as_ref(),
367        );
368        Self::format_dimension_analysis(
369            formatted,
370            QualityDimension::Hallucination,
371            review.hallucination.as_ref(),
372        );
373        Self::format_dimension_analysis(formatted, QualityDimension::Style, review.style.as_ref());
374        Self::format_dimension_analysis(
375            formatted,
376            QualityDimension::Security,
377            review.security.as_ref(),
378        );
379        Self::format_dimension_analysis(
380            formatted,
381            QualityDimension::Performance,
382            review.performance.as_ref(),
383        );
384        Self::format_dimension_analysis(
385            formatted,
386            QualityDimension::Duplication,
387            review.duplication.as_ref(),
388        );
389        Self::format_dimension_analysis(
390            formatted,
391            QualityDimension::ErrorHandling,
392            review.error_handling.as_ref(),
393        );
394        Self::format_dimension_analysis(
395            formatted,
396            QualityDimension::Testing,
397            review.testing.as_ref(),
398        );
399        Self::format_dimension_analysis(
400            formatted,
401            QualityDimension::BestPractices,
402            review.best_practices.as_ref(),
403        );
404    }
405
406    /// Formats the suggestions section
407    fn format_suggestions(formatted: &mut String, suggestions: &[String]) {
408        if !suggestions.is_empty() {
409            write!(
410                formatted,
411                "{}\n\n",
412                "💡 SUGGESTIONS //".bright_blue().bold()
413            )
414            .expect("write to string should not fail");
415            for suggestion in suggestions {
416                writeln!(
417                    formatted,
418                    "  {} {}",
419                    "•".bright_cyan(),
420                    suggestion.bright_blue()
421                )
422                .expect("write to string should not fail");
423            }
424        }
425    }
426
427    /// Helper method to format a single dimension analysis
428    fn format_dimension_analysis(
429        formatted: &mut String,
430        dimension: QualityDimension,
431        analysis: Option<&DimensionAnalysis>,
432    ) {
433        if let Some(dim) = analysis
434            && dim.issues_found
435            && !dim.issues.is_empty()
436        {
437            // Choose emoji based on the dimension
438            let (emoji, color_fn) = match dimension {
439                QualityDimension::Complexity => ("🧠", "bright_magenta"),
440                QualityDimension::Abstraction => ("🏗️", "bright_cyan"),
441                QualityDimension::Deletion => ("🗑️", "bright_white"),
442                QualityDimension::Hallucination => ("👻", "bright_magenta"),
443                QualityDimension::Style => ("🎨", "bright_blue"),
444                QualityDimension::Security => ("🔒", "bright_red"),
445                QualityDimension::Performance => ("⚡", "bright_yellow"),
446                QualityDimension::Duplication => ("🔄", "bright_cyan"),
447                QualityDimension::ErrorHandling => ("🧯", "bright_red"),
448                QualityDimension::Testing => ("🧪", "bright_green"),
449                QualityDimension::BestPractices => ("📐", "bright_blue"),
450            };
451
452            let title = dimension.display_name();
453            let header = match color_fn {
454                "bright_magenta" => format!("◤ {emoji} {title} ◢").bright_magenta().bold(),
455                "bright_cyan" => format!("◤ {emoji} {title} ◢").bright_cyan().bold(),
456                "bright_white" => format!("◤ {emoji} {title} ◢").bright_white().bold(),
457                "bright_blue" => format!("◤ {emoji} {title} ◢").bright_blue().bold(),
458                "bright_red" => format!("◤ {emoji} {title} ◢").bright_red().bold(),
459                "bright_yellow" => format!("◤ {emoji} {title} ◢").bright_yellow().bold(),
460                "bright_green" => format!("◤ {emoji} {title} ◢").bright_green().bold(),
461                _ => format!("◤ {emoji} {title} ◢").normal().bold(),
462            };
463
464            write!(formatted, "{header}\n\n").expect("write to string should not fail");
465
466            for (i, issue) in dim.issues.iter().enumerate() {
467                // Severity badge with custom styling based on severity
468                let severity_badge = match issue.severity.as_str() {
469                    "Critical" => format!("[{}]", "CRITICAL".bright_red().bold()),
470                    "High" => format!("[{}]", "HIGH".red().bold()),
471                    "Medium" => format!("[{}]", "MEDIUM".yellow().bold()),
472                    "Low" => format!("[{}]", "LOW".bright_yellow().bold()),
473                    _ => format!("[{}]", issue.severity.normal().bold()),
474                };
475
476                writeln!(
477                    formatted,
478                    "  {} {} {}",
479                    format!("{:02}", i + 1).bright_white().bold(),
480                    severity_badge,
481                    issue.description.bright_white()
482                )
483                .expect("write to string should not fail");
484
485                let formatted_location = Self::format_location(&issue.location).bright_white();
486                writeln!(
487                    formatted,
488                    "     {}: {}",
489                    "LOCATION".bright_cyan().bold(),
490                    formatted_location
491                )
492                .expect("write to string should not fail");
493
494                // Format explanation with some spacing for readability
495                let explanation_lines = textwrap::wrap(&issue.explanation, EXPLANATION_WRAP_WIDTH);
496                write!(formatted, "     {}: ", "DETAIL".bright_cyan().bold())
497                    .expect("write to string should not fail");
498                for (i, line) in explanation_lines.iter().enumerate() {
499                    if i == 0 {
500                        writeln!(formatted, "{line}").expect("write to string should not fail");
501                    } else {
502                        writeln!(formatted, "            {line}")
503                            .expect("write to string should not fail");
504                    }
505                }
506
507                // Format recommendation with a different style
508                write!(
509                    formatted,
510                    "     {}: {}\n\n",
511                    "FIX".bright_green().bold(),
512                    issue.recommendation.bright_green()
513                )
514                .expect("write to string should not fail");
515            }
516        }
517    }
518}
519
520/// Handles the review command which generates an AI code review of staged changes
521/// with comprehensive analysis across multiple dimensions of code quality
522pub async fn handle_review_command(
523    common: CommonParams,
524    _print: bool,
525    repository_url: Option<String>,
526    include_unstaged: bool,
527    commit_id: Option<String>,
528    from: Option<String>,
529    to: Option<String>,
530) -> Result<()> {
531    // Check if the preset is appropriate for code reviews
532    validate_preset_for_review(&common);
533
534    // Validate parameter combinations
535    validate_review_parameters(
536        commit_id.as_ref(),
537        from.as_ref(),
538        to.as_ref(),
539        include_unstaged,
540    )?;
541
542    let mut config = Config::load()?;
543    common.apply_to_config(&mut config)?;
544
545    // Setup the service
546    let service = setup_review_service(&common, repository_url, &config)?;
547
548    // Generate the review
549    let review = generate_review_based_on_parameters(
550        service,
551        common,
552        config,
553        include_unstaged,
554        commit_id,
555        from,
556        to,
557    )
558    .await?;
559
560    // Print the review to stdout
561    println!("{}", review.format());
562
563    Ok(())
564}
565
566/// Validates that the preset is appropriate for code reviews
567fn validate_preset_for_review(common: &CommonParams) {
568    if !common.is_valid_preset_for_type(PresetType::Review) {
569        ui::print_warning(
570            "The specified preset may not be suitable for code reviews. Consider using a review or general preset instead.",
571        );
572        ui::print_info("Run 'git presets' to see available presets for reviews.");
573    }
574}
575
576/// Validates the parameter combinations for review command
577fn validate_review_parameters(
578    commit_id: Option<&String>,
579    from: Option<&String>,
580    to: Option<&String>,
581    include_unstaged: bool,
582) -> Result<()> {
583    if from.is_some() && to.is_none() {
584        return Err(anyhow::anyhow!(
585            "When using --from, you must also specify --to for branch comparison reviews"
586        ));
587    }
588
589    if commit_id.is_some() && (from.is_some() || to.is_some()) {
590        return Err(anyhow::anyhow!(
591            "Cannot use --commit with --from/--to. These are mutually exclusive options"
592        ));
593    }
594
595    if include_unstaged && (from.is_some() || to.is_some()) {
596        return Err(anyhow::anyhow!(
597            "Cannot use --include-unstaged with --from/--to. Branch comparison reviews don't include working directory changes"
598        ));
599    }
600
601    Ok(())
602}
603
604/// Sets up the review service with proper configuration
605fn setup_review_service(
606    common: &CommonParams,
607    repository_url: Option<String>,
608    config: &Config,
609) -> Result<Arc<CommitService>> {
610    // Combine repository URL from CLI and CommonParams
611    let repo_url = repository_url.or(common.repository_url.clone());
612
613    // Create the git repository
614    let git_repo = GitRepo::new_from_url(repo_url).context("Failed to create GitRepo")?;
615
616    let repo_path = git_repo.repo_path().clone();
617    let provider_name = &config.default_provider;
618
619    let service = Arc::new(
620        CommitService::new(
621            config.clone(),
622            &repo_path,
623            provider_name,
624            false, // emoji not needed for review
625            false, // verification not needed for review
626            git_repo,
627        )
628        .context("Failed to create CommitService")?,
629    );
630
631    // Check environment prerequisites
632    if let Err(e) = service.check_environment() {
633        ui::print_error(&format!("Error: {e}"));
634        ui::print_info("\nPlease ensure the following:");
635        ui::print_info("1. Git is installed and accessible from the command line.");
636        ui::print_info(
637            "2. You are running this command from within a Git repository or provide a repository URL with --repo.",
638        );
639        return Err(e);
640    }
641
642    Ok(service)
643}
644
645/// Generates a review based on the provided parameters
646async fn generate_review_based_on_parameters(
647    service: Arc<CommitService>,
648    common: CommonParams,
649    config: Config,
650    include_unstaged: bool,
651    commit_id: Option<String>,
652    from: Option<String>,
653    to: Option<String>,
654) -> Result<GeneratedReview> {
655    let effective_instructions = common
656        .instructions
657        .unwrap_or_else(|| config.instructions.clone());
658    let preset_str = common.preset.as_deref().unwrap_or("");
659
660    // Create and start the spinner
661    let spinner = ui::create_spinner("");
662    let random_message = messages::get_review_waiting_message();
663    spinner.set_message(random_message.text.to_string());
664
665    let review = if let (Some(from_branch), Some(to_branch)) = (from.as_ref(), to.as_ref()) {
666        // Branch comparison review
667        generate_branch_comparison_review(
668            &service,
669            &spinner,
670            random_message,
671            preset_str,
672            &effective_instructions,
673            from_branch,
674            to_branch,
675        )
676        .await?
677    } else if let Some(to_branch) = to.as_ref() {
678        // If only --to is specified, default --from to "main"
679        let from_branch = "main";
680        generate_branch_comparison_review(
681            &service,
682            &spinner,
683            random_message,
684            preset_str,
685            &effective_instructions,
686            from_branch,
687            to_branch,
688        )
689        .await?
690    } else if let Some(commit_id) = commit_id {
691        // Generate review for specific commit
692        generate_commit_review(
693            &service,
694            &spinner,
695            random_message,
696            preset_str,
697            &effective_instructions,
698            &commit_id,
699        )
700        .await?
701    } else {
702        // Generate review for staged/unstaged changes
703        generate_working_directory_review(
704            &service,
705            &spinner,
706            random_message,
707            preset_str,
708            &effective_instructions,
709            include_unstaged,
710        )
711        .await?
712    };
713
714    // Stop the spinner
715    spinner.finish_and_clear();
716
717    Ok(review)
718}
719
720/// Generates a review for branch comparison
721async fn generate_branch_comparison_review(
722    service: &Arc<CommitService>,
723    spinner: &indicatif::ProgressBar,
724    random_message: &messages::ColoredMessage,
725    preset_str: &str,
726    effective_instructions: &str,
727    from_branch: &str,
728    to_branch: &str,
729) -> Result<GeneratedReview> {
730    spinner.set_message(format!(
731        "{} - Comparing {} -> {}",
732        random_message.text, from_branch, to_branch
733    ));
734
735    service
736        .generate_review_for_branch_diff(preset_str, effective_instructions, from_branch, to_branch)
737        .await
738}
739
740/// Generates a review for a specific commit
741async fn generate_commit_review(
742    service: &Arc<CommitService>,
743    spinner: &indicatif::ProgressBar,
744    random_message: &messages::ColoredMessage,
745    preset_str: &str,
746    effective_instructions: &str,
747    commit_id: &str,
748) -> Result<GeneratedReview> {
749    spinner.set_message(format!(
750        "{} - Reviewing commit: {}",
751        random_message.text, commit_id
752    ));
753
754    service
755        .generate_review_for_commit(preset_str, effective_instructions, commit_id)
756        .await
757}
758
759/// Generates a review for working directory changes (staged/unstaged)
760async fn generate_working_directory_review(
761    service: &Arc<CommitService>,
762    spinner: &indicatif::ProgressBar,
763    random_message: &messages::ColoredMessage,
764    preset_str: &str,
765    effective_instructions: &str,
766    include_unstaged: bool,
767) -> Result<GeneratedReview> {
768    if include_unstaged {
769        spinner.set_message(format!(
770            "{} - Including unstaged changes",
771            random_message.text
772        ));
773
774        // Get the git info with unstaged changes to check if there are any changes
775        let git_info = service.get_git_info_with_unstaged(include_unstaged).await?;
776
777        if git_info.staged_files.is_empty() {
778            spinner.finish_and_clear();
779            ui::print_warning("No changes found (staged or unstaged). Nothing to review.");
780            return Err(anyhow::anyhow!("No changes to review"));
781        }
782
783        // Generate the review with unstaged changes
784        service
785            .generate_review_with_unstaged(preset_str, effective_instructions, include_unstaged)
786            .await
787    } else {
788        // Original behavior - only staged changes
789        let git_info = service.get_git_info().await?;
790
791        if git_info.staged_files.is_empty() {
792            spinner.finish_and_clear();
793            ui::print_warning(
794                "No staged changes. Please stage your changes before generating a review.",
795            );
796            ui::print_info("You can stage changes using 'git add <file>' or 'git add .'");
797            ui::print_info("To include unstaged changes, use --include-unstaged");
798            ui::print_info(
799                "To review differences between branches, use --from and --to (--from defaults to 'main')",
800            );
801            return Err(anyhow::anyhow!("No staged changes to review"));
802        }
803
804        // Generate the review with only staged changes
805        service
806            .generate_review(preset_str, effective_instructions)
807            .await
808    }
809}