use ai::error::{AiError, AiResult}; use ai::tool::tools::FunctionCall; use async_trait::async_trait; use git::rpc::proto as p; use git::rpc::proto::merge_service_client::MergeServiceClient; use serde_json::{json, Value}; use super::helpers::{arg_str, git_ctx, require_repo_member, rpc_err}; use crate::agent::run::AppAgentContext; pub struct GitMergeBaseTool; impl GitMergeBaseTool { pub fn new() -> Self { Self } } impl Default for GitMergeBaseTool { fn default() -> Self { Self::new() } } #[async_trait] impl FunctionCall for GitMergeBaseTool { type Context = AppAgentContext; fn name(&self) -> &'static str { "git_merge_base" } fn description(&self) -> &'static str { "Find the common ancestor (merge base) of two commits." } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "workspace": { "type": "string", "description": "Workspace name" }, "repo": { "type": "string", "description": "Repository name" }, "oid_a": { "type": "string", "description": "First commit OID" }, "oid_b": { "type": "string", "description": "Second commit OID" } }, "required": ["workspace", "repo", "oid_a", "oid_b"] }) } async fn call(&self, ctx: &mut AppAgentContext, args: Value) -> AiResult { let git = git_ctx(ctx)?; let workspace = arg_str(&args, "workspace")?; let repo_name = arg_str(&args, "repo")?; let oid_a = arg_str(&args, "oid_a")?; let oid_b = arg_str(&args, "oid_b")?; let repo = require_repo_member(git, ctx.user_id, workspace, repo_name).await?; let mut client = MergeServiceClient::new(git.channel.clone()); let resp = client .merge_base(p::MergeBaseRequest { repo_id: repo.id.to_string(), oid_a: Some(p::ObjectId { value: oid_a.to_string() }), oid_b: Some(p::ObjectId { value: oid_b.to_string() }), }) .await .map_err(rpc_err)? .into_inner(); Ok(json!({ "base_oid": resp.base_oid.map(|o| o.value) })) } } pub struct GitMergeAnalysisTool; impl GitMergeAnalysisTool { pub fn new() -> Self { Self } } impl Default for GitMergeAnalysisTool { fn default() -> Self { Self::new() } } #[async_trait] impl FunctionCall for GitMergeAnalysisTool { type Context = AppAgentContext; fn name(&self) -> &'static str { "git_merge_analysis" } fn description(&self) -> &'static str { "Analyze whether two commits can be merged (fast-forward, normal, up-to-date, etc)." } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "workspace": { "type": "string", "description": "Workspace name" }, "repo": { "type": "string", "description": "Repository name" }, "oid_a": { "type": "string", "description": "First commit OID" }, "oid_b": { "type": "string", "description": "Second commit OID" } }, "required": ["workspace", "repo", "oid_a", "oid_b"] }) } async fn call(&self, ctx: &mut AppAgentContext, args: Value) -> AiResult { let git = git_ctx(ctx)?; let workspace = arg_str(&args, "workspace")?; let repo_name = arg_str(&args, "repo")?; let oid_a = arg_str(&args, "oid_a")?; let oid_b = arg_str(&args, "oid_b")?; let repo = require_repo_member(git, ctx.user_id, workspace, repo_name).await?; let mut client = MergeServiceClient::new(git.channel.clone()); let resp = client .merge_analysis(p::MergeAnalysisRequest { repo_id: repo.id.to_string(), oid_a: Some(p::ObjectId { value: oid_a.to_string() }), oid_b: Some(p::ObjectId { value: oid_b.to_string() }), }) .await .map_err(rpc_err)? .into_inner(); let analysis = resp.analysis.ok_or_else(|| AiError::Response("no analysis".to_string()))?; let pref = resp.preference.ok_or_else(|| AiError::Response("no preference".to_string()))?; // Determine overall status let status = if analysis.is_up_to_date { "up_to_date" } else if analysis.is_fast_forward { "fast_forward" } else if analysis.is_normal { "normal_merge" } else if analysis.is_unborn { "unborn" } else { "none" }; Ok(json!({ "status": status, "analysis": { "is_none": analysis.is_none, "is_normal": analysis.is_normal, "is_up_to_date": analysis.is_up_to_date, "is_fast_forward": analysis.is_fast_forward, "is_unborn": analysis.is_unborn, }, "preference": { "is_none": pref.is_none, "is_no_fast_forward": pref.is_no_fast_forward, "is_fastforward_only": pref.is_fastforward_only, }, })) } } pub struct GitMergeIsConflictedTool; impl GitMergeIsConflictedTool { pub fn new() -> Self { Self } } impl Default for GitMergeIsConflictedTool { fn default() -> Self { Self::new() } } #[async_trait] impl FunctionCall for GitMergeIsConflictedTool { type Context = AppAgentContext; fn name(&self) -> &'static str { "git_merge_is_conflicted" } fn description(&self) -> &'static str { "Check if the repository is currently in a conflicted merge state." } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "workspace": { "type": "string", "description": "Workspace name" }, "repo": { "type": "string", "description": "Repository name" } }, "required": ["workspace", "repo"] }) } async fn call(&self, ctx: &mut AppAgentContext, args: Value) -> AiResult { let git = git_ctx(ctx)?; let workspace = arg_str(&args, "workspace")?; let repo_name = arg_str(&args, "repo")?; let repo = require_repo_member(git, ctx.user_id, workspace, repo_name).await?; let mut client = MergeServiceClient::new(git.channel.clone()); let resp = client .merge_is_conflicted(p::MergeIsConflictedRequest { repo_id: repo.id.to_string(), }) .await .map_err(rpc_err)? .into_inner(); Ok(json!({ "is_conflicted": resp.is_conflicted })) } }