Skip to main content

st/
code_review.rs

1//! šŸ” Code Review Module - AI-powered code review
2//!
3//! "Let the machines judge your code!" - The Cheet 😺
4//!
5//! Supports multiple review modes:
6//! - Local: Just show the diff with syntax highlighting
7//! - Grok: Use X.AI's Grok for witty, thorough reviews
8//! - OpenRouter: Access 100+ models for reviews
9//! - Any configured LLM provider
10
11use crate::proxy::{LlmMessage, LlmProxy, LlmRequest, LlmRole};
12use anyhow::{Context, Result};
13use std::process::Command;
14
15/// Code review provider selection
16#[derive(Debug, Clone, Default)]
17pub enum ReviewProvider {
18    /// Local analysis only (no LLM)
19    #[default]
20    Local,
21    /// Use Grok (X.AI)
22    Grok,
23    /// Use OpenRouter with optional model
24    OpenRouter(Option<String>),
25    /// Use any configured provider by name
26    Custom(String, Option<String>),
27}
28
29/// Code review configuration
30#[derive(Debug, Clone)]
31pub struct CodeReviewConfig {
32    /// Provider to use for review
33    pub provider: ReviewProvider,
34    /// Review staged changes only
35    pub staged: bool,
36    /// Review specific files
37    pub files: Vec<String>,
38    /// Compare against branch
39    pub compare_branch: Option<String>,
40    /// Include context lines
41    pub context_lines: usize,
42    /// Focus areas for review
43    pub focus: Vec<String>,
44}
45
46impl Default for CodeReviewConfig {
47    fn default() -> Self {
48        Self {
49            provider: ReviewProvider::Local,
50            staged: false,
51            files: Vec::new(),
52            compare_branch: None,
53            context_lines: 3,
54            focus: Vec::new(),
55        }
56    }
57}
58
59/// Code review result
60#[derive(Debug)]
61pub struct CodeReviewResult {
62    pub diff: String,
63    pub review: Option<String>,
64    pub provider_used: String,
65    pub files_reviewed: Vec<String>,
66}
67
68/// Run code review with the given configuration
69pub async fn run_code_review(config: CodeReviewConfig) -> Result<CodeReviewResult> {
70    // Get the diff
71    let diff = get_diff(&config)?;
72
73    if diff.trim().is_empty() {
74        return Ok(CodeReviewResult {
75            diff: String::new(),
76            review: Some("No changes to review.".to_string()),
77            provider_used: "none".to_string(),
78            files_reviewed: Vec::new(),
79        });
80    }
81
82    // Extract file list from diff
83    let files_reviewed = extract_files_from_diff(&diff);
84
85    // Run review based on provider
86    let (review, provider_used) = match &config.provider {
87        ReviewProvider::Local => {
88            // Just format and display the diff
89            (None, "local".to_string())
90        }
91        ReviewProvider::Grok => {
92            let review = review_with_llm("grok", "grok-beta", &diff, &config).await?;
93            (Some(review), "grok".to_string())
94        }
95        ReviewProvider::OpenRouter(model) => {
96            let model = model.as_deref().unwrap_or("anthropic/claude-3-haiku");
97            let review = review_with_llm("openrouter", model, &diff, &config).await?;
98            (Some(review), format!("openrouter/{}", model))
99        }
100        ReviewProvider::Custom(provider, model) => {
101            let model = model.as_deref().unwrap_or("default");
102            let review = review_with_llm(provider, model, &diff, &config).await?;
103            (Some(review), format!("{}/{}", provider, model))
104        }
105    };
106
107    Ok(CodeReviewResult {
108        diff,
109        review,
110        provider_used,
111        files_reviewed,
112    })
113}
114
115/// Get the diff based on configuration
116fn get_diff(config: &CodeReviewConfig) -> Result<String> {
117    let mut cmd = Command::new("git");
118    cmd.arg("diff");
119
120    // Add context lines
121    cmd.arg(format!("-U{}", config.context_lines));
122
123    // Staged only?
124    if config.staged {
125        cmd.arg("--staged");
126    }
127
128    // Compare against branch?
129    if let Some(branch) = &config.compare_branch {
130        cmd.arg(branch);
131    }
132
133    // Specific files?
134    if !config.files.is_empty() {
135        cmd.arg("--");
136        for file in &config.files {
137            cmd.arg(file);
138        }
139    }
140
141    let output = cmd.output().context("Failed to run git diff")?;
142
143    if !output.status.success() {
144        let stderr = String::from_utf8_lossy(&output.stderr);
145        return Err(anyhow::anyhow!("git diff failed: {}", stderr));
146    }
147
148    Ok(String::from_utf8_lossy(&output.stdout).to_string())
149}
150
151/// Extract file names from diff output
152fn extract_files_from_diff(diff: &str) -> Vec<String> {
153    diff.lines()
154        .filter(|line| line.starts_with("diff --git"))
155        .filter_map(|line| {
156            // Format: diff --git a/file b/file
157            line.split(" b/").nth(1).map(|s| s.to_string())
158        })
159        .collect()
160}
161
162/// Review code using an LLM provider
163async fn review_with_llm(
164    provider_name: &str,
165    model: &str,
166    diff: &str,
167    config: &CodeReviewConfig,
168) -> Result<String> {
169    let proxy = LlmProxy::default();
170
171    // Build the system prompt
172    let mut system_prompt = String::from(
173        "You are an expert code reviewer. Review the following git diff and provide:
174
1751. **Summary**: Brief overview of changes
1762. **Positives**: What's good about the changes
1773. **Issues**: Potential bugs, security issues, or code smells
1784. **Suggestions**: Specific improvements with code examples
1795. **Rating**: Overall quality (1-10)
180
181Be concise but thorough. Use markdown formatting.
182",
183    );
184
185    // Add focus areas if specified
186    if !config.focus.is_empty() {
187        system_prompt.push_str("\nFocus especially on: ");
188        system_prompt.push_str(&config.focus.join(", "));
189        system_prompt.push('\n');
190    }
191
192    // Build the request
193    let request = LlmRequest {
194        model: model.to_string(),
195        messages: vec![
196            LlmMessage {
197                role: LlmRole::System,
198                content: system_prompt,
199            },
200            LlmMessage {
201                role: LlmRole::User,
202                content: format!("Please review this diff:\n\n```diff\n{}\n```", diff),
203            },
204        ],
205        temperature: Some(0.3), // Lower temperature for more consistent reviews
206        max_tokens: Some(2000),
207        stream: false,
208    };
209
210    let response = proxy.complete(provider_name, request).await?;
211    Ok(response.content)
212}
213
214/// Display code review result in a nice format
215pub fn display_review(result: &CodeReviewResult) {
216    println!("\nšŸ” Code Review Results");
217    println!("═══════════════════════════════════════════════════════════════\n");
218
219    // Show files reviewed
220    if !result.files_reviewed.is_empty() {
221        println!("šŸ“ Files reviewed ({}):", result.files_reviewed.len());
222        for file in &result.files_reviewed {
223            println!("   • {}", file);
224        }
225        println!();
226    }
227
228    // Show provider used
229    println!("šŸ¤– Provider: {}\n", result.provider_used);
230
231    // Show the diff if local mode
232    if result.review.is_none() && !result.diff.is_empty() {
233        println!("šŸ“ Diff:");
234        println!("{}", "-".repeat(60));
235        // Colorize the diff output
236        for line in result.diff.lines() {
237            if line.starts_with('+') && !line.starts_with("+++") {
238                println!("\x1b[32m{}\x1b[0m", line); // Green for additions
239            } else if line.starts_with('-') && !line.starts_with("---") {
240                println!("\x1b[31m{}\x1b[0m", line); // Red for deletions
241            } else if line.starts_with("@@") {
242                println!("\x1b[36m{}\x1b[0m", line); // Cyan for hunks
243            } else if line.starts_with("diff --git") {
244                println!("\x1b[1;34m{}\x1b[0m", line); // Bold blue for file headers
245            } else {
246                println!("{}", line);
247            }
248        }
249        println!();
250    }
251
252    // Show the AI review if available
253    if let Some(review) = &result.review {
254        println!("šŸ“‹ Review:");
255        println!("{}", "-".repeat(60));
256        println!("{}", review);
257        println!();
258    }
259
260    println!("═══════════════════════════════════════════════════════════════");
261}
262
263/// Quick helper to run a local review
264pub async fn review_local() -> Result<()> {
265    let config = CodeReviewConfig::default();
266    let result = run_code_review(config).await?;
267    display_review(&result);
268    Ok(())
269}
270
271/// Quick helper to run a Grok review
272pub async fn review_with_grok() -> Result<()> {
273    let config = CodeReviewConfig {
274        provider: ReviewProvider::Grok,
275        ..Default::default()
276    };
277    let result = run_code_review(config).await?;
278    display_review(&result);
279    Ok(())
280}
281
282/// Quick helper to run an OpenRouter review
283pub async fn review_with_openrouter(model: Option<String>) -> Result<()> {
284    let config = CodeReviewConfig {
285        provider: ReviewProvider::OpenRouter(model),
286        ..Default::default()
287    };
288    let result = run_code_review(config).await?;
289    display_review(&result);
290    Ok(())
291}