use git::context::facade::GitContext; use pulls::services::comments::CreatePullCommentCmd; use pulls::services::inbox::{InboxQuery, InboxScope}; use pulls::services::labels::AttachLabelCmd; use pulls::services::merge::MergePullCmd; use pulls::services::pulls::{CreatePullCmd, ListPullQuery, UpdatePullCmd}; use pulls::services::reviewers::RequestReviewersCmd; use pulls::services::reviews::{InlineDraft, ReviewIntent, SubmitReviewCmd}; use pulls::share::lookup::PullHandle; use rmcp::model::CallToolResult; use serde_json::json; use crate::auth::Principal; use crate::error::{tool_done, tool_error, tool_message, tool_ok, write_denied}; use crate::services::McpServices; use crate::tools::args; use crate::tools::helpers::{page, parse_enum, repo_read, repo_write}; async fn handle_for( services: &McpServices, principal: &Principal, space: &str, repo: &str, pull_ref: &str, write: bool, ) -> Result { let ctx: GitContext = if write { repo_write(services, principal.uid, space, repo).await? } else { repo_read(services, principal.uid, space, repo).await? }; let pull = pulls::share::lookup::find_pull(&services.pulls, ctx.model.id, pull_ref.trim()) .await .map_err(tool_error)?; let admin = pulls::share::lookup::is_admin(&services.pulls, principal.uid, &ctx).await; Ok(PullHandle { ctx, pull, caller: Some(principal.uid), admin, }) } async fn detail( services: &McpServices, space: &str, repo: &str, handle: &PullHandle, ) -> CallToolResult { match services .pulls .pull_detail( space.trim().to_string(), repo.trim().to_string(), &handle.ctx, &handle.pull, ) .await { Ok(dto) => tool_ok(&dto), Err(error) => tool_error(error), } } pub(crate) async fn list( services: &McpServices, principal: &Principal, args: args::PullListArgs, ) -> CallToolResult { let ctx = match repo_read(services, principal.uid, &args.space, &args.repo).await { Ok(ctx) => ctx, Err(error) => return error, }; let query = ListPullQuery { state: args.state, author: args.author, base: args.base, head: args.head, draft: args.draft, search: args.search, sort: args.sort, order: args.order, limit: args.limit, offset: args.offset, }; match services.pulls.list_pulls(ctx.model.id, query).await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn create( services: &McpServices, principal: &Principal, args: args::PullCreateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let ctx = match repo_write(services, principal.uid, &args.space, &args.repo).await { Ok(ctx) => ctx, Err(error) => return error, }; let cmd = CreatePullCmd { title: args.title, body: args.body, head_branch: args.head_branch, base_branch: args.base_branch, draft: args.draft, issue_id: args.issue_id, }; let space = args.space.trim().to_string(); let repo = args.repo.trim().to_string(); match services .pulls .create_pull(space.clone(), repo.clone(), &ctx, principal.uid, cmd) .await { Ok(model) => { let handle = PullHandle { ctx, pull: model, caller: Some(principal.uid), admin: false, }; detail(services, &space, &repo, &handle).await } Err(error) => tool_error(error), } } pub(crate) async fn get( services: &McpServices, principal: &Principal, args: args::PullRefArgs, ) -> CallToolResult { let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, false, ) .await { Ok(handle) => handle, Err(error) => return error, }; detail(services, &args.space, &args.repo, &handle).await } pub(crate) async fn update( services: &McpServices, principal: &Principal, args: args::PullUpdateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; let cmd = UpdatePullCmd { title: args.title, body: args.body.map(Some), base_branch: args.base_branch, draft: args.draft, auto_merge: args.auto_merge.map(Some), issue_id: None, }; let space = args.space.trim().to_string(); let repo = args.repo.trim().to_string(); match services .pulls .update_pull(space.clone(), repo.clone(), &handle, cmd) .await { Ok(model) => { let handle = PullHandle { ctx: handle.ctx, pull: model, caller: Some(principal.uid), admin: handle.admin, }; detail(services, &space, &repo, &handle).await } Err(error) => tool_error(error), } } pub(crate) async fn set_state( services: &McpServices, principal: &Principal, args: args::PullRefArgs, closed: bool, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .set_state(&handle.pull, principal.uid, closed) .await { Ok(model) => { let handle = PullHandle { ctx: handle.ctx, pull: model, caller: Some(principal.uid), admin: handle.admin, }; detail(services, &args.space, &args.repo, &handle).await } Err(error) => tool_error(error), } } pub(crate) async fn delete( services: &McpServices, principal: &Principal, args: args::PullRefArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; if !handle.admin { return tool_message("仅仓库管理员可以删除合并请求"); } match services .pulls .delete_pull(&handle.pull, principal.uid) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn merge( services: &McpServices, principal: &Principal, args: args::PullMergeArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; let method = match args.method.as_deref() { Some(raw) => match parse_enum::(raw) { Ok(method) => Some(method), Err(error) => return error, }, None => None, }; let cmd = MergePullCmd { method, title: args.title, message: args.message, fast_forward_only: args.fast_forward_only, expect_head_sha: args.expect_head_sha, delete_branch_after_merge: args.delete_branch_after_merge, }; let space = args.space.trim().to_string(); let repo = args.repo.trim().to_string(); match services .pulls .merge_pull(space.clone(), repo.clone(), &handle, cmd) .await { Ok((model, result, summary)) => { let handle = PullHandle { ctx: handle.ctx, pull: model, caller: Some(principal.uid), admin: handle.admin, }; match services .pulls .pull_detail(space, repo, &handle.ctx, &handle.pull) .await { Ok(dto) => tool_ok(&json!({ "pull": dto, "result": result, "summary": summary })), Err(error) => tool_error(error), } } Err(error) => tool_error(error), } } pub(crate) async fn comment_list( services: &McpServices, principal: &Principal, args: args::PullCommentListArgs, ) -> CallToolResult { let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, false, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .list_comments(handle.pull.id, page(args.limit, args.offset, None)) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn comment_create( services: &McpServices, principal: &Principal, args: args::PullCommentCreateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; let cmd = CreatePullCommentCmd { body: args.body, path: args.path, line: args.line, side: args.side, commit_sha: args.commit_sha, review_id: None, parent_comment_id: args.parent_comment_id, }; match services .pulls .create_comment(&handle.ctx, &handle.pull, principal.uid, cmd) .await { Ok(model) => tool_ok(&json!({ "id": model.id })), Err(error) => tool_error(error), } } pub(crate) async fn comment_update( services: &McpServices, principal: &Principal, args: args::PullCommentUpdateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .update_comment( &handle.pull, args.comment_id, principal.uid, handle.admin, args.body, ) .await { Ok(model) => tool_ok(&json!({ "id": model.id })), Err(error) => tool_error(error), } } pub(crate) async fn comment_delete( services: &McpServices, principal: &Principal, args: args::PullCommentRefArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .delete_comment(&handle.pull, args.comment_id, principal.uid, handle.admin) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn comment_resolve( services: &McpServices, principal: &Principal, args: args::PullCommentResolveArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .set_comment_resolved(&handle.pull, args.comment_id, principal.uid, args.resolved) .await { Ok(model) => tool_ok(&json!({ "id": model.id, "resolved": args.resolved })), Err(error) => tool_error(error), } } pub(crate) async fn review_list( services: &McpServices, principal: &Principal, args: args::PullReviewsArgs, ) -> CallToolResult { let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, false, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .list_reviews(handle.pull.id, Some(principal.uid)) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn review_submit( services: &McpServices, principal: &Principal, args: args::PullReviewSubmitArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let state = match parse_enum::(&args.state) { Ok(state) => state, Err(error) => return error, }; let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; let cmd = SubmitReviewCmd { state, body: args.body, comments: Vec::::new(), }; match services .pulls .submit_review(&handle.ctx, &handle.pull, principal.uid, handle.admin, cmd) .await { Ok(model) => tool_ok(&json!({ "id": model.id })), Err(error) => tool_error(error), } } pub(crate) async fn review_dismiss( services: &McpServices, principal: &Principal, args: args::PullReviewDismissArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .dismiss_review(&handle.pull, args.review_id, principal.uid) .await { Ok(model) => tool_ok(&json!({ "id": model.id })), Err(error) => tool_error(error), } } pub(crate) async fn reviewer_list( services: &McpServices, principal: &Principal, args: args::PullReviewsArgs, ) -> CallToolResult { let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, false, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services.pulls.list_reviewers(handle.pull.id).await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn reviewer_request( services: &McpServices, principal: &Principal, args: args::PullReviewerRequestArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; let cmd = RequestReviewersCmd { reviewers: args.reviewers, required: args.required.unwrap_or(true), }; match services .pulls .request_reviewers(&handle.pull, principal.uid, cmd) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn reviewer_cancel( services: &McpServices, principal: &Principal, args: args::PullReviewerCancelArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .cancel_reviewer_request(&handle.pull, principal.uid, args.reviewer_id) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn assignee_list( services: &McpServices, principal: &Principal, args: args::PullReviewsArgs, ) -> CallToolResult { let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, false, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services.pulls.list_assignees(handle.pull.id).await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn assignee_add( services: &McpServices, principal: &Principal, args: args::PullAssigneeArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .add_assignee(&handle.pull, principal.uid, &args.user) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn assignee_remove( services: &McpServices, principal: &Principal, args: args::PullAssigneeArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .remove_assignee(&handle.pull, principal.uid, &args.user) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn label_list( services: &McpServices, principal: &Principal, args: args::PullReviewsArgs, ) -> CallToolResult { let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, false, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services.pulls.list_labels(handle.pull.id).await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn label_attach( services: &McpServices, principal: &Principal, args: args::PullLabelArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; let cmd = AttachLabelCmd { label: args.label }; match services .pulls .attach_label(&handle.pull, handle.ctx.model.dir_id, principal.uid, cmd) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn label_detach( services: &McpServices, principal: &Principal, args: args::PullLabelArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, true, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .detach_label( &handle.pull, handle.ctx.model.dir_id, principal.uid, &args.label, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn checks( services: &McpServices, principal: &Principal, args: args::PullReviewsArgs, ) -> CallToolResult { let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, false, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .pull_checks( args.space.trim().to_string(), args.repo.trim().to_string(), &handle.pull, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn gate( services: &McpServices, principal: &Principal, args: args::PullReviewsArgs, ) -> CallToolResult { let handle = match handle_for( services, principal, &args.space, &args.repo, &args.pull, false, ) .await { Ok(handle) => handle, Err(error) => return error, }; match services .pulls .pull_gate( args.space.trim().to_string(), args.repo.trim().to_string(), &handle.ctx, &handle.pull, Some(principal.uid), handle.admin, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn inbox( services: &McpServices, principal: &Principal, args: args::PullInboxArgs, ) -> CallToolResult { let scope = match parse_enum::(&args.scope) { Ok(scope) => scope, Err(error) => return error, }; let query = InboxQuery { state: args.state, limit: args.limit, offset: args.offset, }; match services.pulls.list_inbox(principal.uid, scope, query).await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } }