gitai/server/tools/
codereview.rs

1//! MCP code review tool implementation
2//!
3//! This module provides the MCP tool for generating code reviews with options for
4//! staged changes, unstaged changes, and specific commits.
5
6use 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/// Code review tool for generating comprehensive code reviews
24#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
25pub struct CodeReviewTool {
26    /// Include unstaged changes in the review
27    #[serde(default)]
28    pub include_unstaged: bool,
29
30    /// Specific commit to review (hash, branch name, or reference)
31    #[serde(default)]
32    pub commit_id: String,
33
34    /// Starting branch for comparison (defaults to 'main'). Used with to for branch comparison reviews
35    #[serde(default)]
36    pub from: String,
37
38    /// Target branch for comparison (e.g., 'feature-branch', 'pr-branch'). Used with from for branch comparison reviews
39    #[serde(default)]
40    pub to: String,
41
42    /// Preset instruction set to use for the review
43    #[serde(default)]
44    pub preset: String,
45
46    /// Custom instructions for the AI
47    #[serde(default)]
48    pub custom_instructions: String,
49
50    /// Repository path (local) or URL (remote). Required.
51    pub repository: String,
52}
53
54impl CodeReviewTool {
55    /// Returns the tool definition for the code review tool
56    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    /// Execute the code review tool with the provided repository and configuration
74    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
82        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        // Validate parameter combinations
87        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        // Check if local operations are required
109        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        // Create a commit service for processing
120        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, // emoji not needed for review
128            false, // verification not needed for review
129            GitRepo::new(&repo_path)?,
130        )?;
131
132        // Set up config with custom instructions if provided
133        let mut config_clone = config.clone();
134        apply_custom_instructions(&mut config_clone, &self.custom_instructions);
135
136        // Process the preset
137        let preset = if self.preset.trim().is_empty() {
138            "default"
139        } else {
140            &self.preset
141        };
142
143        // Generate the code review based on parameters
144        let review = if has_branches {
145            // Branch comparison review
146            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            // Review a specific commit
163            service
164                .generate_review_for_commit(preset, &self.custom_instructions, &self.commit_id)
165                .await?
166        } else if self.include_unstaged {
167            // Review including unstaged changes
168            service
169                .generate_review_with_unstaged(preset, &self.custom_instructions, true)
170                .await?
171        } else {
172            // Review only staged changes (default behavior)
173            service
174                .generate_review(preset, &self.custom_instructions)
175                .await?
176        };
177
178        // Format and return the review
179        let formatted_review = review.format();
180        Ok(create_text_result(formatted_review))
181    }
182}