gitai/server/tools/
commit.rs

1//! MCP commit tool implementation
2//!
3//! This module provides the MCP tool for generating and performing commits.
4
5use crate::config::Config as PilotConfig;
6use crate::debug;
7use crate::features::commit::{CommitService, format_commit_message};
8use crate::git::GitRepo;
9use crate::server::tools::utils::{
10    PilotTool, create_text_result, resolve_git_repo, validate_repository_parameter,
11};
12
13use rmcp::handler::server::tool::cached_schema_for_type;
14use rmcp::model::{CallToolResult, Tool};
15use rmcp::schemars;
16
17use serde::{Deserialize, Serialize};
18use std::borrow::Cow;
19use std::sync::Arc;
20
21/// Commit tool for generating commit messages and performing commits
22#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
23pub struct CommitTool {
24    /// Whether to generate and perform the commit (true) or just generate a message (false)
25    #[serde(default)]
26    pub auto_commit: bool,
27
28    /// Whether to use emoji in commit messages
29    #[serde(default)]
30    pub use_emoji: bool,
31
32    /// Whether to skip commit verification
33    #[serde(default)]
34    pub no_verify: bool,
35
36    /// Instruction preset to use
37    #[serde(default)]
38    pub preset: String,
39
40    /// Custom instructions for the AI
41    #[serde(default)]
42    pub custom_instructions: String,
43
44    /// Repository path (local) or URL (remote). Required.
45    pub repository: String,
46}
47
48impl CommitTool {
49    /// Returns the tool definition for the commit tool
50    pub fn get_tool_definition() -> Tool {
51        Tool {
52            name: Cow::Borrowed("gitai_commit"),
53            description: Some(Cow::Borrowed(
54                "Generate commit messages and perform Git commits",
55            )),
56            input_schema: cached_schema_for_type::<Self>(),
57            annotations: None,
58            icons: None,
59            output_schema: None,
60            title: None,
61        }
62    }
63}
64
65#[async_trait::async_trait]
66impl PilotTool for CommitTool {
67    /// Execute the commit tool with the provided repository and configuration
68    async fn execute(
69        &self,
70        git_repo: Arc<GitRepo>,
71        config: PilotConfig,
72    ) -> Result<CallToolResult, anyhow::Error> {
73        debug!("Processing commit request with: {:?}", self);
74
75        // Validate repository parameter
76        validate_repository_parameter(&self.repository)?;
77        let git_repo = resolve_git_repo(Some(self.repository.as_str()), git_repo)?;
78        debug!("Using repository: {}", git_repo.repo_path().display());
79
80        // Check if we can perform the operation on this repository
81        if self.auto_commit && git_repo.is_remote() {
82            return Err(anyhow::anyhow!("Cannot auto-commit to a remote repository"));
83        }
84
85        // Create the commit service
86        let provider_name = &config.default_provider;
87        let repo_path = git_repo.repo_path().clone();
88        let verify = !self.no_verify;
89
90        // Create a new GitRepo instance rather than trying to clone it
91        let service = CommitService::new(
92            config.clone(),
93            &repo_path,
94            provider_name,
95            self.use_emoji,
96            verify,
97            GitRepo::new(&repo_path)?,
98        )?;
99
100        // First check if we have staged changes
101        let git_info = service.get_git_info().await?;
102        if git_info.staged_files.is_empty() {
103            return Err(anyhow::anyhow!(
104                "No staged changes. Please stage your changes before generating a commit message."
105            ));
106        }
107
108        // Run pre-commit hook
109        if let Err(e) = service.pre_commit() {
110            return Err(anyhow::anyhow!("Pre-commit failed: {e}"));
111        }
112
113        // Generate a commit message
114        let preset = if self.preset.is_empty() {
115            "default"
116        } else {
117            &self.preset
118        };
119
120        let message = service
121            .generate_message(preset, &self.custom_instructions)
122            .await?;
123        let formatted_message = format_commit_message(&message);
124
125        // If auto_commit is true, perform the commit
126        if self.auto_commit {
127            match service.perform_commit(&formatted_message) {
128                Ok(result) => {
129                    // Create result with commit info
130                    let result_text = format!(
131                        "Commit successful! [{}]\n\n{}\n\n{} file{} changed, {} insertion{}(+), {} deletion{}(-)",
132                        result.commit_hash,
133                        formatted_message,
134                        result.files_changed,
135                        if result.files_changed == 1 { "" } else { "s" },
136                        result.insertions,
137                        if result.insertions == 1 { "" } else { "s" },
138                        result.deletions,
139                        if result.deletions == 1 { "" } else { "s" }
140                    );
141
142                    return Ok(create_text_result(result_text));
143                }
144                Err(e) => {
145                    return Err(anyhow::anyhow!("Failed to commit: {e}"));
146                }
147            }
148        }
149
150        // If we're just generating a message, return it
151        Ok(create_text_result(formatted_message))
152    }
153}