This commit is contained in:
997
operation/pull/pull.go
Normal file
997
operation/pull/pull.go
Normal file
@@ -0,0 +1,997 @@
|
||||
package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/gitea-mcp/pkg/annotation"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/params"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/slim"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||
|
||||
gitea_sdk "gitea.dev/sdk"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
|
||||
const (
|
||||
ListRepoPullRequestsToolName = "list_pull_requests"
|
||||
PullRequestReadToolName = "pull_request_read"
|
||||
PullRequestWriteToolName = "pull_request_write"
|
||||
PullRequestReviewWriteToolName = "pull_request_review_write"
|
||||
)
|
||||
|
||||
var (
|
||||
ListRepoPullRequestsTool = mcp.NewTool(
|
||||
ListRepoPullRequestsToolName,
|
||||
mcp.WithToolAnnotation(annotation.ReadOnly("List pull requests")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)),
|
||||
mcp.WithString("state", mcp.Enum("open", "closed", "all"), mcp.DefaultString("all")),
|
||||
mcp.WithString("sort", mcp.Enum("oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"), mcp.DefaultString("recentupdate")),
|
||||
mcp.WithNumber("milestone"),
|
||||
mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)),
|
||||
mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)),
|
||||
)
|
||||
|
||||
PullRequestReadTool = mcp.NewTool(
|
||||
PullRequestReadToolName,
|
||||
mcp.WithDescription("Read pull request: details, diff, changed files, head commit status, reviews."),
|
||||
mcp.WithToolAnnotation(annotation.ReadOnly("Read pull request details")),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Enum("get", "get_diff", "get_files", "get_status", "get_reviews", "get_review", "get_review_comments")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)),
|
||||
mcp.WithNumber("pull_number", mcp.Required()),
|
||||
mcp.WithNumber("review_id", mcp.Description("for 'get_review'/'get_review_comments'")),
|
||||
mcp.WithBoolean("binary", mcp.Description("include binary diff")),
|
||||
mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)),
|
||||
mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)),
|
||||
)
|
||||
|
||||
PullRequestWriteTool = mcp.NewTool(
|
||||
PullRequestWriteToolName,
|
||||
mcp.WithDescription("Write pull requests: create, update, close, reopen, merge, update branch from base, manage reviewers."),
|
||||
mcp.WithToolAnnotation(annotation.Write("Create, update, close, reopen, or merge pull requests")),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Enum("create", "update", "close", "reopen", "merge", "update_branch", "add_reviewers", "remove_reviewers")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)),
|
||||
mcp.WithNumber("pull_number", mcp.Description("required except for 'create'")),
|
||||
mcp.WithString("title", mcp.Description("required for 'create'; optional for 'update'/'merge'")),
|
||||
mcp.WithString("body", mcp.Description("required for 'create'; optional for 'update'")),
|
||||
mcp.WithString("head", mcp.Description("head branch (required for 'create')")),
|
||||
mcp.WithString("base", mcp.Description("base branch (required for 'create')")),
|
||||
mcp.WithString("assignee", mcp.Description("for 'update'")),
|
||||
mcp.WithArray("assignees", mcp.Description("for 'update'"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithNumber("milestone", mcp.Description("for 'update'")),
|
||||
mcp.WithString("state", mcp.Description("for 'update'"), mcp.Enum("open", "closed")),
|
||||
mcp.WithBoolean("allow_maintainer_edit", mcp.Description("for 'update'")),
|
||||
mcp.WithArray("labels", mcp.Description("label IDs"), mcp.Items(map[string]any{"type": "number"})),
|
||||
mcp.WithString("deadline", mcp.Description("ISO 8601")),
|
||||
mcp.WithBoolean("remove_deadline", mcp.Description("for 'update'")),
|
||||
mcp.WithString("merge_style", mcp.Description("for 'merge'"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")),
|
||||
mcp.WithString("message", mcp.Description("merge commit message or dismissal reason")),
|
||||
mcp.WithBoolean("delete_branch", mcp.Description("for 'merge'")),
|
||||
mcp.WithBoolean("force_merge", mcp.Description("merge even if checks fail")),
|
||||
mcp.WithBoolean("merge_when_checks_succeed", mcp.Description("for 'merge'")),
|
||||
mcp.WithString("head_commit_id", mcp.Description("expected head SHA for conflict detection")),
|
||||
mcp.WithArray("reviewers", mcp.Description("for 'add_reviewers'/'remove_reviewers'"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithArray("team_reviewers", mcp.Description("for 'add_reviewers'/'remove_reviewers'"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithBoolean("draft", mcp.Description("uses 'WIP: ' title prefix")),
|
||||
)
|
||||
|
||||
PullRequestReviewWriteTool = mcp.NewTool(
|
||||
PullRequestReviewWriteToolName,
|
||||
mcp.WithDescription("Write PR reviews: create, submit, delete, dismiss."),
|
||||
mcp.WithToolAnnotation(annotation.Write("Submit a pull request review")),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Enum("create", "submit", "delete", "dismiss")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)),
|
||||
mcp.WithNumber("pull_number", mcp.Required()),
|
||||
mcp.WithNumber("review_id", mcp.Description("required except for 'create'")),
|
||||
mcp.WithString("state", mcp.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT", "PENDING")),
|
||||
mcp.WithString("body"),
|
||||
mcp.WithString("commit_id", mcp.Description("for 'create'")),
|
||||
mcp.WithString("message", mcp.Description("dismissal reason")),
|
||||
mcp.WithArray("comments", mcp.Description("inline comments (for 'create')"), mcp.Items(map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"path": map[string]any{"type": "string"},
|
||||
"body": map[string]any{"type": "string"},
|
||||
"old_line_num": map[string]any{"type": "number", "description": "old-file line (deletions)"},
|
||||
"new_line_num": map[string]any{"type": "number", "description": "new-file line (additions)"},
|
||||
},
|
||||
})),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: ListRepoPullRequestsTool,
|
||||
Handler: listRepoPullRequestsFn,
|
||||
})
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: PullRequestReadTool,
|
||||
Handler: pullRequestReadFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: PullRequestWriteTool,
|
||||
Handler: pullRequestWriteFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: PullRequestReviewWriteTool,
|
||||
Handler: pullRequestReviewWriteFn,
|
||||
})
|
||||
}
|
||||
|
||||
func pullRequestReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
method, err := params.GetString(req.GetArguments(), "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "get":
|
||||
return getPullRequestByIndexFn(ctx, req)
|
||||
case "get_diff":
|
||||
return getPullRequestDiffFn(ctx, req)
|
||||
case "get_files":
|
||||
return getPullRequestFilesFn(ctx, req)
|
||||
case "get_status":
|
||||
return getPullRequestStatusFn(ctx, req)
|
||||
case "get_reviews":
|
||||
return listPullRequestReviewsFn(ctx, req)
|
||||
case "get_review":
|
||||
return getPullRequestReviewFn(ctx, req)
|
||||
case "get_review_comments":
|
||||
return listPullRequestReviewCommentsFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func pullRequestWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
method, err := params.GetString(req.GetArguments(), "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "create":
|
||||
return createPullRequestFn(ctx, req)
|
||||
case "update":
|
||||
return editPullRequestFn(ctx, req)
|
||||
case "close":
|
||||
return closePullRequestFn(ctx, req)
|
||||
case "reopen":
|
||||
return reopenPullRequestFn(ctx, req)
|
||||
case "merge":
|
||||
return mergePullRequestFn(ctx, req)
|
||||
case "update_branch":
|
||||
return updatePullRequestBranchFn(ctx, req)
|
||||
case "add_reviewers":
|
||||
return createPullRequestReviewerFn(ctx, req)
|
||||
case "remove_reviewers":
|
||||
return deletePullRequestReviewerFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func closePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
owner, err := params.GetString(req.GetArguments(), "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(req.GetArguments(), "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(req.GetArguments(), "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
state := gitea_sdk.StateClosed
|
||||
pr, _, err := client.PullRequests.EditPullRequest(ctx, owner, repo, index, gitea_sdk.EditPullRequestOption{
|
||||
State: &state,
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("close %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||
}
|
||||
|
||||
return to.TextResult(slimPullRequest(pr))
|
||||
}
|
||||
|
||||
func reopenPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
owner, err := params.GetString(req.GetArguments(), "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(req.GetArguments(), "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(req.GetArguments(), "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
state := gitea_sdk.StateOpen
|
||||
pr, _, err := client.PullRequests.EditPullRequest(ctx, owner, repo, index, gitea_sdk.EditPullRequestOption{
|
||||
State: &state,
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("reopen %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||
}
|
||||
|
||||
return to.TextResult(slimPullRequest(pr))
|
||||
}
|
||||
|
||||
func pullRequestReviewWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
method, err := params.GetString(req.GetArguments(), "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "create":
|
||||
return createPullRequestReviewFn(ctx, req)
|
||||
case "submit":
|
||||
return submitPullRequestReviewFn(ctx, req)
|
||||
case "delete":
|
||||
return deletePullRequestReviewFn(ctx, req)
|
||||
case "dismiss":
|
||||
return dismissPullRequestReviewFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func getPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
pr, _, err := client.PullRequests.GetPullRequest(ctx, owner, repo, index)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||
}
|
||||
|
||||
// /pulls/{n} omits `assets`; PRs are issues internally, so the issue
|
||||
// assets endpoint surfaces description attachments.
|
||||
var assets []*gitea_sdk.Attachment
|
||||
assetsPath := fmt.Sprintf("repos/%s/%s/issues/%d/assets", url.PathEscape(owner), url.PathEscape(repo), index)
|
||||
if _, err := gitea.DoJSON(ctx, "GET", assetsPath, nil, nil, &assets); err != nil {
|
||||
log.Debugf("fetch %v/%v/issues/%v/assets err: %v", owner, repo, index, err)
|
||||
}
|
||||
|
||||
m := slimPullRequest(pr)
|
||||
m["body"] = slim.BodyWithAttachments(pr.Body, assets)
|
||||
return to.TextResult(m)
|
||||
}
|
||||
|
||||
func getPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
binary, _ := args["binary"].(bool)
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
diffBytes, _, err := client.PullRequests.GetPullRequestDiff(ctx, owner, repo, index, gitea_sdk.PullRequestDiffOptions{
|
||||
Binary: binary,
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v diff err: %v", owner, repo, index, err))
|
||||
}
|
||||
|
||||
return to.TextResult(string(diffBytes))
|
||||
}
|
||||
|
||||
func listRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
state, _ := args["state"].(string)
|
||||
sort := params.GetOptionalString(args, "sort", "recentupdate")
|
||||
milestone := params.GetOptionalInt(args, "milestone", 0)
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
opt := gitea_sdk.ListPullRequestsOptions{
|
||||
State: gitea_sdk.StateType(state),
|
||||
Sort: sort,
|
||||
Milestone: milestone,
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
pullRequests, _, err := client.PullRequests.ListRepoPullRequests(ctx, owner, repo, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list %v/%v/pull_requests err: %v", owner, repo, err))
|
||||
}
|
||||
|
||||
return to.TextResult(slimPullRequests(pullRequests))
|
||||
}
|
||||
|
||||
// defaultWIPPrefixes are the default Gitea title prefixes that mark a PR as
|
||||
// work-in-progress / draft. Gitea matches these case-insensitively.
|
||||
var defaultWIPPrefixes = []string{"WIP:", "[WIP]"}
|
||||
|
||||
// applyDraftPrefix adds or removes a WIP title prefix that Gitea uses to mark
|
||||
// pull requests as drafts. When the title already carries a recognized prefix
|
||||
// and isDraft is true, the title is returned unchanged to avoid normalization.
|
||||
func applyDraftPrefix(title string, isDraft bool) string {
|
||||
for _, prefix := range defaultWIPPrefixes {
|
||||
if len(title) >= len(prefix) && strings.EqualFold(title[:len(prefix)], prefix) {
|
||||
if isDraft {
|
||||
return title
|
||||
}
|
||||
return strings.TrimLeft(title[len(prefix):], " ")
|
||||
}
|
||||
}
|
||||
if isDraft {
|
||||
return "WIP: " + title
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func createPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
title, err := params.GetString(args, "title")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
body, err := params.GetString(args, "body")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
head, err := params.GetString(args, "head")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
base, err := params.GetString(args, "base")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
if draft, ok := args["draft"].(bool); ok {
|
||||
title = applyDraftPrefix(title, draft)
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
opt := gitea_sdk.CreatePullRequestOption{
|
||||
Title: title,
|
||||
Body: body,
|
||||
Head: head,
|
||||
Base: base,
|
||||
}
|
||||
if labelIDs, err := params.GetInt64Slice(args, "labels"); err == nil {
|
||||
opt.Labels = labelIDs
|
||||
}
|
||||
opt.Deadline = params.GetOptionalTime(args, "deadline")
|
||||
pr, _, err := client.PullRequests.CreatePullRequest(ctx, owner, repo, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("create %v/%v/pull_request err: %v", owner, repo, err))
|
||||
}
|
||||
|
||||
return to.TextResult(slimPullRequest(pr))
|
||||
}
|
||||
|
||||
type reviewerOp func(client *gitea_sdk.PullRequestsService, ctx context.Context, owner, repo string, index int64, opt gitea_sdk.PullReviewRequestOptions) (*gitea_sdk.Response, error)
|
||||
|
||||
func pullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest, verb string, op reviewerOp) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
reviewers := params.GetStringSlice(args, "reviewers")
|
||||
teamReviewers := params.GetStringSlice(args, "team_reviewers")
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
if _, err := op(client.PullRequests, ctx, owner, repo, index, gitea_sdk.PullReviewRequestOptions{
|
||||
Reviewers: reviewers,
|
||||
TeamReviewers: teamReviewers,
|
||||
}); err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("%s review requests for %v/%v/pr/%v err: %v", verb, owner, repo, index, err))
|
||||
}
|
||||
|
||||
return to.TextResult(map[string]any{
|
||||
"message": fmt.Sprintf("Successfully %sd review requests", verb),
|
||||
"reviewers": reviewers,
|
||||
"team_reviewers": teamReviewers,
|
||||
"pr_index": index,
|
||||
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||
})
|
||||
}
|
||||
|
||||
func createPullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return pullRequestReviewerFn(ctx, req, "create", (*gitea_sdk.PullRequestsService).CreateReviewRequests)
|
||||
}
|
||||
|
||||
func deletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return pullRequestReviewerFn(ctx, req, "delete", (*gitea_sdk.PullRequestsService).DeleteReviewRequests)
|
||||
}
|
||||
|
||||
func listPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
reviews, _, err := client.PullRequests.ListPullReviews(ctx, owner, repo, index, gitea_sdk.ListPullReviewsOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list reviews for %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||
}
|
||||
|
||||
return to.TextResult(slimReviews(reviews))
|
||||
}
|
||||
|
||||
func getPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
reviewID, err := params.GetIndex(args, "review_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
review, _, err := client.PullRequests.GetPullReview(ctx, owner, repo, index, reviewID)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
|
||||
}
|
||||
|
||||
return to.TextResult(slimReview(review))
|
||||
}
|
||||
|
||||
func listPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
reviewID, err := params.GetIndex(args, "review_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
comments, _, err := client.PullRequests.ListPullReviewComments(ctx, owner, repo, index, reviewID)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list review comments for review %v on %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
|
||||
}
|
||||
|
||||
return to.TextResult(slimReviewComments(comments))
|
||||
}
|
||||
|
||||
func createPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
opt := gitea_sdk.CreatePullReviewOptions{}
|
||||
|
||||
if state, ok := args["state"].(string); ok {
|
||||
opt.State = gitea_sdk.ReviewStateType(state)
|
||||
}
|
||||
if body, ok := args["body"].(string); ok {
|
||||
opt.Body = body
|
||||
}
|
||||
if commitID, ok := args["commit_id"].(string); ok {
|
||||
opt.CommitID = commitID
|
||||
}
|
||||
|
||||
// Parse inline comments
|
||||
if commentsArg, exists := args["comments"]; exists {
|
||||
if commentsSlice, ok := commentsArg.([]any); ok {
|
||||
for _, comment := range commentsSlice {
|
||||
if commentMap, ok := comment.(map[string]any); ok {
|
||||
reviewComment := gitea_sdk.CreatePullReviewComment{}
|
||||
if path, ok := commentMap["path"].(string); ok {
|
||||
reviewComment.Path = path
|
||||
}
|
||||
if body, ok := commentMap["body"].(string); ok {
|
||||
reviewComment.Body = body
|
||||
}
|
||||
if oldLineNum, ok := params.ToInt64(commentMap["old_line_num"]); ok {
|
||||
reviewComment.OldLineNum = oldLineNum
|
||||
}
|
||||
if newLineNum, ok := params.ToInt64(commentMap["new_line_num"]); ok {
|
||||
reviewComment.NewLineNum = newLineNum
|
||||
}
|
||||
opt.Comments = append(opt.Comments, reviewComment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
review, _, err := client.PullRequests.CreatePullReview(ctx, owner, repo, index, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("create review for %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||
}
|
||||
|
||||
return to.TextResult(slimReview(review))
|
||||
}
|
||||
|
||||
func submitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
reviewID, err := params.GetIndex(args, "review_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
state, err := params.GetString(args, "state")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
opt := gitea_sdk.SubmitPullReviewOptions{
|
||||
State: gitea_sdk.ReviewStateType(state),
|
||||
}
|
||||
if body, ok := args["body"].(string); ok {
|
||||
opt.Body = body
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
review, _, err := client.PullRequests.SubmitPullReview(ctx, owner, repo, index, reviewID, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("submit review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
|
||||
}
|
||||
|
||||
return to.TextResult(slimReview(review))
|
||||
}
|
||||
|
||||
func deletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
reviewID, err := params.GetIndex(args, "review_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
_, err = client.PullRequests.DeletePullReview(ctx, owner, repo, index, reviewID)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("delete review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
|
||||
}
|
||||
|
||||
successMsg := map[string]any{
|
||||
"message": "Successfully deleted review",
|
||||
"review_id": reviewID,
|
||||
"pr_index": index,
|
||||
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||
}
|
||||
|
||||
return to.TextResult(successMsg)
|
||||
}
|
||||
|
||||
func dismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
reviewID, err := params.GetIndex(args, "review_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
opt := gitea_sdk.DismissPullReviewOptions{}
|
||||
if message, ok := args["message"].(string); ok {
|
||||
opt.Message = message
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
_, err = client.PullRequests.DismissPullReview(ctx, owner, repo, index, reviewID, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("dismiss review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
|
||||
}
|
||||
|
||||
successMsg := map[string]any{
|
||||
"message": "Successfully dismissed review",
|
||||
"review_id": reviewID,
|
||||
"pr_index": index,
|
||||
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||
}
|
||||
|
||||
return to.TextResult(successMsg)
|
||||
}
|
||||
|
||||
func mergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
mergeStyle := params.GetOptionalString(args, "merge_style", "merge")
|
||||
title, _ := args["title"].(string)
|
||||
message, _ := args["message"].(string)
|
||||
deleteBranch, _ := args["delete_branch"].(bool)
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
forceMerge, _ := args["force_merge"].(bool)
|
||||
mergeWhenChecksSucceed, _ := args["merge_when_checks_succeed"].(bool)
|
||||
headCommitID, _ := args["head_commit_id"].(string)
|
||||
deleteBranchAfterMerge := &deleteBranch
|
||||
|
||||
opt := gitea_sdk.MergePullRequestOption{
|
||||
Style: gitea_sdk.MergeStyle(mergeStyle),
|
||||
Title: title,
|
||||
Message: message,
|
||||
DeleteBranchAfterMerge: deleteBranchAfterMerge,
|
||||
ForceMerge: forceMerge,
|
||||
MergeWhenChecksSucceed: mergeWhenChecksSucceed,
|
||||
HeadCommitId: headCommitID,
|
||||
}
|
||||
|
||||
merged, resp, err := client.PullRequests.MergePullRequest(ctx, owner, repo, index, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||
}
|
||||
|
||||
if !merged && resp != nil && resp.StatusCode >= 400 {
|
||||
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v failed: HTTP %d %s", owner, repo, index, resp.StatusCode, resp.Status))
|
||||
}
|
||||
|
||||
if !merged {
|
||||
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v returned merged=false", owner, repo, index))
|
||||
}
|
||||
|
||||
successMsg := map[string]any{
|
||||
"merged": merged,
|
||||
"pr_index": index,
|
||||
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||
"merge_style": mergeStyle,
|
||||
"branch_deleted": deleteBranch,
|
||||
}
|
||||
|
||||
return to.TextResult(successMsg)
|
||||
}
|
||||
|
||||
func editPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
opt := gitea_sdk.EditPullRequestOption{}
|
||||
|
||||
if title, ok := args["title"].(string); ok {
|
||||
opt.Title = title
|
||||
}
|
||||
if draft, ok := args["draft"].(bool); ok {
|
||||
if opt.Title == "" {
|
||||
// Fetch current title so the caller doesn't have to provide it
|
||||
// just to toggle draft status.
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
pr, _, err := client.PullRequests.GetPullRequest(ctx, owner, repo, index)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||
}
|
||||
opt.Title = pr.Title
|
||||
}
|
||||
opt.Title = applyDraftPrefix(opt.Title, draft)
|
||||
}
|
||||
opt.Body = params.GetPresentStringPtr(args, "body")
|
||||
opt.AllowMaintainerEdit = params.GetOptionalBoolPtr(args, "allow_maintainer_edit")
|
||||
opt.RemoveDeadline = params.GetOptionalBoolPtr(args, "remove_deadline")
|
||||
opt.Deadline = params.GetOptionalTime(args, "deadline")
|
||||
if base, ok := args["base"].(string); ok {
|
||||
opt.Base = base
|
||||
}
|
||||
if assignee, ok := args["assignee"].(string); ok {
|
||||
opt.Assignee = assignee
|
||||
}
|
||||
if assignees := params.GetStringSlice(args, "assignees"); assignees != nil {
|
||||
opt.Assignees = assignees
|
||||
}
|
||||
if val, exists := args["milestone"]; exists {
|
||||
if milestone, ok := params.ToInt64(val); ok {
|
||||
opt.Milestone = milestone
|
||||
}
|
||||
}
|
||||
if state, ok := args["state"].(string); ok {
|
||||
s := gitea_sdk.StateType(state)
|
||||
opt.State = &s
|
||||
}
|
||||
if labelIDs, err := params.GetInt64Slice(args, "labels"); err == nil {
|
||||
opt.Labels = labelIDs
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
pr, _, err := client.PullRequests.EditPullRequest(ctx, owner, repo, index, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("edit %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||
}
|
||||
|
||||
return to.TextResult(slimPullRequest(pr))
|
||||
}
|
||||
|
||||
func updatePullRequestBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("repos/%s/%s/pulls/%d/update", url.PathEscape(owner), url.PathEscape(repo), index)
|
||||
if _, err := gitea.DoJSON(ctx, "POST", path, nil, nil, nil); err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("update %v/%v/pr/%v branch err: %v", owner, repo, index, err))
|
||||
}
|
||||
return to.TextResult(map[string]any{"message": "branch updated from base"})
|
||||
}
|
||||
|
||||
func getPullRequestFilesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
files, _, err := client.PullRequests.ListPullRequestFiles(ctx, owner, repo, index, gitea_sdk.ListPullRequestFilesOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize},
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v files err: %v", owner, repo, index, err))
|
||||
}
|
||||
return to.TextResult(files)
|
||||
}
|
||||
|
||||
func getPullRequestStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "pull_number")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
pr, _, err := client.PullRequests.GetPullRequest(ctx, owner, repo, index)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||
}
|
||||
if pr.Head == nil || pr.Head.Sha == "" {
|
||||
return to.ErrorResult(fmt.Errorf("pr %v/%v/%v has no head SHA", owner, repo, index))
|
||||
}
|
||||
|
||||
status, _, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, pr.Head.Sha)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v status err: %v", owner, repo, index, err))
|
||||
}
|
||||
return to.TextResult(status)
|
||||
}
|
||||
Reference in New Issue
Block a user