git_iris/mcp/tools/
commit.rs

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