194c44c
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<PullHandle, CallToolResult> {
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::<models::pull_request::pull_requests::MergeMethod>(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::<ReviewIntent>(&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::<InlineDraft>::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::<InboxScope>(&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),
}
}