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