1use crate::proxy::{LlmMessage, LlmProxy, LlmRequest, LlmRole};
12use anyhow::{Context, Result};
13use std::process::Command;
14
15#[derive(Debug, Clone, Default)]
17pub enum ReviewProvider {
18 #[default]
20 Local,
21 Grok,
23 OpenRouter(Option<String>),
25 Custom(String, Option<String>),
27}
28
29#[derive(Debug, Clone)]
31pub struct CodeReviewConfig {
32 pub provider: ReviewProvider,
34 pub staged: bool,
36 pub files: Vec<String>,
38 pub compare_branch: Option<String>,
40 pub context_lines: usize,
42 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#[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
68pub async fn run_code_review(config: CodeReviewConfig) -> Result<CodeReviewResult> {
70 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 let files_reviewed = extract_files_from_diff(&diff);
84
85 let (review, provider_used) = match &config.provider {
87 ReviewProvider::Local => {
88 (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
115fn get_diff(config: &CodeReviewConfig) -> Result<String> {
117 let mut cmd = Command::new("git");
118 cmd.arg("diff");
119
120 cmd.arg(format!("-U{}", config.context_lines));
122
123 if config.staged {
125 cmd.arg("--staged");
126 }
127
128 if let Some(branch) = &config.compare_branch {
130 cmd.arg(branch);
131 }
132
133 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
151fn extract_files_from_diff(diff: &str) -> Vec<String> {
153 diff.lines()
154 .filter(|line| line.starts_with("diff --git"))
155 .filter_map(|line| {
156 line.split(" b/").nth(1).map(|s| s.to_string())
158 })
159 .collect()
160}
161
162async 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 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 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 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), max_tokens: Some(2000),
207 stream: false,
208 };
209
210 let response = proxy.complete(provider_name, request).await?;
211 Ok(response.content)
212}
213
214pub fn display_review(result: &CodeReviewResult) {
216 println!("\nš Code Review Results");
217 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
218
219 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 println!("š¤ Provider: {}\n", result.provider_used);
230
231 if result.review.is_none() && !result.diff.is_empty() {
233 println!("š Diff:");
234 println!("{}", "-".repeat(60));
235 for line in result.diff.lines() {
237 if line.starts_with('+') && !line.starts_with("+++") {
238 println!("\x1b[32m{}\x1b[0m", line); } else if line.starts_with('-') && !line.starts_with("---") {
240 println!("\x1b[31m{}\x1b[0m", line); } else if line.starts_with("@@") {
242 println!("\x1b[36m{}\x1b[0m", line); } else if line.starts_with("diff --git") {
244 println!("\x1b[1;34m{}\x1b[0m", line); } else {
246 println!("{}", line);
247 }
248 }
249 println!();
250 }
251
252 if let Some(review) = &result.review {
254 println!("š Review:");
255 println!("{}", "-".repeat(60));
256 println!("{}", review);
257 println!();
258 }
259
260 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
261}
262
263pub 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
271pub 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
282pub 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}