gitai/server/tools/
codereview.rs1use crate::config::Config as PilotConfig;
7use crate::debug;
8use crate::features::commit::service::CommitService;
9use crate::git::GitRepo;
10use crate::server::tools::utils::{
11 PilotTool, apply_custom_instructions, create_text_result, resolve_git_repo,
12 validate_repository_parameter,
13};
14
15use rmcp::handler::server::tool::cached_schema_for_type;
16use rmcp::model::{CallToolResult, Tool};
17use rmcp::schemars;
18
19use serde::{Deserialize, Serialize};
20use std::borrow::Cow;
21use std::sync::Arc;
22
23#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
25pub struct CodeReviewTool {
26 #[serde(default)]
28 pub include_unstaged: bool,
29
30 #[serde(default)]
32 pub commit_id: String,
33
34 #[serde(default)]
36 pub from: String,
37
38 #[serde(default)]
40 pub to: String,
41
42 #[serde(default)]
44 pub preset: String,
45
46 #[serde(default)]
48 pub custom_instructions: String,
49
50 pub repository: String,
52}
53
54impl CodeReviewTool {
55 pub fn get_tool_definition() -> Tool {
57 Tool {
58 name: Cow::Borrowed("gitai_review"),
59 description: Some(Cow::Borrowed(
60 "Generate a comprehensive code review with options for staged changes, unstaged changes, specific commits, or branch comparisons (e.g., PR reviews)",
61 )),
62 input_schema: cached_schema_for_type::<Self>(),
63 annotations: None,
64 icons: None,
65 output_schema: None,
66 title: None,
67 }
68 }
69}
70
71#[async_trait::async_trait]
72impl PilotTool for CodeReviewTool {
73 async fn execute(
75 &self,
76 git_repo: Arc<GitRepo>,
77 config: PilotConfig,
78 ) -> Result<CallToolResult, anyhow::Error> {
79 debug!("Generating code review with: {:?}", self);
80
81 validate_repository_parameter(&self.repository)?;
83 let git_repo = resolve_git_repo(Some(self.repository.as_str()), git_repo)?;
84 debug!("Using repository: {}", git_repo.repo_path().display());
85
86 let has_commit = !self.commit_id.trim().is_empty();
88 let has_branches = !self.from.trim().is_empty() || !self.to.trim().is_empty();
89
90 if has_commit && has_branches {
91 return Err(anyhow::anyhow!(
92 "Cannot use both commit_id and branch parameters. These options are mutually exclusive."
93 ));
94 }
95
96 if !self.from.trim().is_empty() && self.to.trim().is_empty() {
97 return Err(anyhow::anyhow!(
98 "When using 'from', you must also specify 'to' for branch comparison reviews"
99 ));
100 }
101
102 if self.include_unstaged && has_branches {
103 return Err(anyhow::anyhow!(
104 "Cannot use include_unstaged with branch comparison. Branch reviews compare committed changes only."
105 ));
106 }
107
108 if !has_commit
110 && !has_branches
111 && git_repo.is_remote()
112 && (self.include_unstaged || self.commit_id.trim().is_empty())
113 {
114 return Err(anyhow::anyhow!(
115 "Cannot review staged/unstaged changes on a remote repository"
116 ));
117 }
118
119 let repo_path = git_repo.repo_path().clone();
121 let provider_name = &config.default_provider;
122
123 let service = CommitService::new(
124 config.clone(),
125 &repo_path,
126 provider_name,
127 false, false, GitRepo::new(&repo_path)?,
130 )?;
131
132 let mut config_clone = config.clone();
134 apply_custom_instructions(&mut config_clone, &self.custom_instructions);
135
136 let preset = if self.preset.trim().is_empty() {
138 "default"
139 } else {
140 &self.preset
141 };
142
143 let review = if has_branches {
145 let from_branch = if self.from.trim().is_empty() {
147 "main"
148 } else {
149 self.from.trim()
150 };
151 let to_branch = self.to.trim();
152
153 service
154 .generate_review_for_branch_diff(
155 preset,
156 &self.custom_instructions,
157 from_branch,
158 to_branch,
159 )
160 .await?
161 } else if has_commit {
162 service
164 .generate_review_for_commit(preset, &self.custom_instructions, &self.commit_id)
165 .await?
166 } else if self.include_unstaged {
167 service
169 .generate_review_with_unstaged(preset, &self.custom_instructions, true)
170 .await?
171 } else {
172 service
174 .generate_review(preset, &self.custom_instructions)
175 .await?
176 };
177
178 let formatted_review = review.format();
180 Ok(create_text_result(formatted_review))
181 }
182}