Skip to content

Commit 17636e4

Browse files
haasonsaasclaude
andcommitted
Refactor monolithic main.rs into modular architecture with 93 tests
Split 5,393-line main.rs into 4 module trees: - commands/ (review, git, pr, eval, smart-review, misc) - parsing/ (LLM response parsing, smart review response parsing) - output/ (formatting: JSON, Patch, Markdown, Smart Review output) - review/ (pipeline, filters, feedback, context helpers, rule helpers) main.rs is now a 364-line thin shell with CLI struct and dispatch only. Added 70+ new unit tests across all modules (93 total, up from ~23). Zero warnings, clean clippy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8e23e1e commit 17636e4

File tree

19 files changed

+6102
-5047
lines changed

19 files changed

+6102
-5047
lines changed

src/commands/eval.rs

Lines changed: 899 additions & 0 deletions
Large diffs are not rendered by default.

src/commands/git.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use anyhow::Result;
2+
use clap::Subcommand;
3+
use std::path::PathBuf;
4+
use tracing::info;
5+
6+
use crate::adapters;
7+
use crate::config;
8+
use crate::core;
9+
use crate::output::OutputFormat;
10+
use crate::review::review_diff_content_with_repo;
11+
12+
#[derive(Subcommand)]
13+
pub enum GitCommands {
14+
Uncommitted,
15+
Staged,
16+
Branch {
17+
#[arg(help = "Base branch/ref (defaults to repo default)")]
18+
base: Option<String>,
19+
},
20+
Suggest,
21+
PrTitle,
22+
}
23+
24+
pub async fn git_command(
25+
command: GitCommands,
26+
config: config::Config,
27+
format: OutputFormat,
28+
) -> Result<()> {
29+
let git = core::GitIntegration::new(".")?;
30+
31+
let diff_content = match command {
32+
GitCommands::Uncommitted => {
33+
info!("Analyzing uncommitted changes");
34+
git.get_uncommitted_diff()?
35+
}
36+
GitCommands::Staged => {
37+
info!("Analyzing staged changes");
38+
git.get_staged_diff()?
39+
}
40+
GitCommands::Branch { base } => {
41+
let base_branch = base.unwrap_or_else(|| {
42+
git.get_default_branch()
43+
.unwrap_or_else(|_| "main".to_string())
44+
});
45+
info!("Analyzing changes from branch: {}", base_branch);
46+
git.get_branch_diff(&base_branch)?
47+
}
48+
GitCommands::Suggest => {
49+
return suggest_commit_message(config).await;
50+
}
51+
GitCommands::PrTitle => {
52+
return suggest_pr_title(config).await;
53+
}
54+
};
55+
56+
if diff_content.is_empty() {
57+
println!("No changes found");
58+
return Ok(());
59+
}
60+
61+
let repo_root = git.workdir().unwrap_or_else(|| PathBuf::from("."));
62+
review_diff_content_with_repo(&diff_content, config, format, &repo_root).await
63+
}
64+
65+
async fn suggest_commit_message(config: config::Config) -> Result<()> {
66+
let git = core::GitIntegration::new(".")?;
67+
let diff_content = git.get_staged_diff()?;
68+
69+
if diff_content.is_empty() {
70+
println!("No staged changes found. Stage your changes with 'git add' first.");
71+
return Ok(());
72+
}
73+
74+
let model_config = adapters::llm::ModelConfig {
75+
model_name: config.model.clone(),
76+
api_key: config.api_key.clone(),
77+
base_url: config.base_url.clone(),
78+
temperature: config.temperature,
79+
max_tokens: config.max_tokens,
80+
openai_use_responses: config.openai_use_responses,
81+
};
82+
83+
let adapter = adapters::llm::create_adapter(&model_config)?;
84+
85+
let (system_prompt, user_prompt) =
86+
core::CommitPromptBuilder::build_commit_prompt(&diff_content);
87+
88+
let request = adapters::llm::LLMRequest {
89+
system_prompt,
90+
user_prompt,
91+
temperature: Some(0.3),
92+
max_tokens: Some(500),
93+
};
94+
95+
let response = adapter.complete(request).await?;
96+
let commit_message = core::CommitPromptBuilder::extract_commit_message(&response.content);
97+
98+
println!("\nSuggested commit message:");
99+
println!("{}", commit_message);
100+
101+
if commit_message.len() > 72 {
102+
println!(
103+
"\n⚠️ Warning: Commit message exceeds 72 characters ({})",
104+
commit_message.len()
105+
);
106+
}
107+
108+
Ok(())
109+
}
110+
111+
async fn suggest_pr_title(config: config::Config) -> Result<()> {
112+
let git = core::GitIntegration::new(".")?;
113+
let base_branch = git
114+
.get_default_branch()
115+
.unwrap_or_else(|_| "main".to_string());
116+
let diff_content = git.get_branch_diff(&base_branch)?;
117+
118+
if diff_content.is_empty() {
119+
println!("No changes found compared to {} branch.", base_branch);
120+
return Ok(());
121+
}
122+
123+
let model_config = adapters::llm::ModelConfig {
124+
model_name: config.model.clone(),
125+
api_key: config.api_key.clone(),
126+
base_url: config.base_url.clone(),
127+
temperature: config.temperature,
128+
max_tokens: config.max_tokens,
129+
openai_use_responses: config.openai_use_responses,
130+
};
131+
132+
let adapter = adapters::llm::create_adapter(&model_config)?;
133+
134+
let (system_prompt, user_prompt) =
135+
core::CommitPromptBuilder::build_pr_title_prompt(&diff_content);
136+
137+
let request = adapters::llm::LLMRequest {
138+
system_prompt,
139+
user_prompt,
140+
temperature: Some(0.3),
141+
max_tokens: Some(200),
142+
};
143+
144+
let response = adapter.complete(request).await?;
145+
146+
// Extract title from response
147+
let title = if let Some(start) = response.content.find("<title>") {
148+
if let Some(end) = response.content.find("</title>") {
149+
response.content[start + 7..end].trim().to_string()
150+
} else {
151+
response.content.trim().to_string()
152+
}
153+
} else {
154+
// Fallback: take the first non-empty line
155+
response
156+
.content
157+
.lines()
158+
.find(|line| !line.trim().is_empty())
159+
.unwrap_or("")
160+
.trim()
161+
.to_string()
162+
};
163+
164+
println!("\nSuggested PR title:");
165+
println!("{}", title);
166+
167+
if title.len() > 65 {
168+
println!(
169+
"\n⚠️ Warning: PR title exceeds 65 characters ({})",
170+
title.len()
171+
);
172+
}
173+
174+
Ok(())
175+
}

0 commit comments

Comments
 (0)