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)
|
||||
}
|
||||
1044
operation/pull/pull_test.go
Normal file
1044
operation/pull/pull_test.go
Normal file
File diff suppressed because it is too large
Load Diff
160
operation/pull/slim.go
Normal file
160
operation/pull/slim.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package pull
|
||||
|
||||
import (
|
||||
"gitea.com/gitea/gitea-mcp/pkg/slim"
|
||||
|
||||
gitea_sdk "gitea.dev/sdk"
|
||||
)
|
||||
|
||||
func repoRef(r *gitea_sdk.Repository) map[string]any {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"full_name": r.FullName,
|
||||
"description": r.Description,
|
||||
}
|
||||
}
|
||||
|
||||
func slimPullRequest(pr *gitea_sdk.PullRequest) map[string]any {
|
||||
if pr == nil {
|
||||
return nil
|
||||
}
|
||||
m := map[string]any{
|
||||
"number": pr.Index,
|
||||
"title": pr.Title,
|
||||
"body": pr.Body,
|
||||
"state": pr.State,
|
||||
"draft": pr.Draft,
|
||||
"merged": pr.HasMerged,
|
||||
"mergeable": pr.Mergeable,
|
||||
"html_url": pr.HTMLURL,
|
||||
"user": slim.UserLogin(pr.Poster),
|
||||
"labels": slim.LabelNames(pr.Labels),
|
||||
"comments": pr.Comments,
|
||||
"created_at": pr.Created,
|
||||
"updated_at": pr.Updated,
|
||||
"closed_at": pr.Closed,
|
||||
}
|
||||
if pr.HasMerged {
|
||||
m["merged_at"] = pr.Merged
|
||||
m["merge_commit_sha"] = pr.MergedCommitID
|
||||
m["merged_by"] = slim.UserLogin(pr.MergedBy)
|
||||
}
|
||||
if pr.Head != nil {
|
||||
head := map[string]any{"ref": pr.Head.Ref, "sha": pr.Head.Sha}
|
||||
if pr.Head.Repository != nil {
|
||||
head["repo"] = repoRef(pr.Head.Repository)
|
||||
}
|
||||
m["head"] = head
|
||||
}
|
||||
if pr.Base != nil {
|
||||
base := map[string]any{"ref": pr.Base.Ref, "sha": pr.Base.Sha}
|
||||
if pr.Base.Repository != nil {
|
||||
base["repo"] = repoRef(pr.Base.Repository)
|
||||
}
|
||||
m["base"] = base
|
||||
}
|
||||
if pr.Additions != nil {
|
||||
m["additions"] = *pr.Additions
|
||||
}
|
||||
if pr.Deletions != nil {
|
||||
m["deletions"] = *pr.Deletions
|
||||
}
|
||||
if pr.ChangedFiles != nil {
|
||||
m["changed_files"] = *pr.ChangedFiles
|
||||
}
|
||||
if len(pr.Assignees) > 0 {
|
||||
m["assignees"] = slim.UserLogins(pr.Assignees)
|
||||
}
|
||||
if pr.Milestone != nil {
|
||||
m["milestone"] = pr.Milestone.Title
|
||||
}
|
||||
if pr.ReviewComments > 0 {
|
||||
m["review_comments"] = pr.ReviewComments
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func slimPullRequests(prs []*gitea_sdk.PullRequest) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(prs))
|
||||
for _, pr := range prs {
|
||||
if pr == nil {
|
||||
continue
|
||||
}
|
||||
m := map[string]any{
|
||||
"number": pr.Index,
|
||||
"title": pr.Title,
|
||||
"state": pr.State,
|
||||
"draft": pr.Draft,
|
||||
"merged": pr.HasMerged,
|
||||
"html_url": pr.HTMLURL,
|
||||
"user": slim.UserLogin(pr.Poster),
|
||||
"created_at": pr.Created,
|
||||
"updated_at": pr.Updated,
|
||||
}
|
||||
if pr.Head != nil {
|
||||
m["head"] = pr.Head.Ref
|
||||
}
|
||||
if pr.Base != nil {
|
||||
m["base"] = pr.Base.Ref
|
||||
}
|
||||
if len(pr.Labels) > 0 {
|
||||
m["labels"] = slim.LabelNames(pr.Labels)
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func slimReview(r *gitea_sdk.PullReview) map[string]any {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": r.ID,
|
||||
"state": r.State,
|
||||
"body": r.Body,
|
||||
"user": slim.UserLogin(r.Reviewer),
|
||||
"comments_count": r.CodeCommentsCount,
|
||||
"submitted_at": r.Submitted,
|
||||
"html_url": r.HTMLURL,
|
||||
"stale": r.Stale,
|
||||
"official": r.Official,
|
||||
"dismissed": r.Dismissed,
|
||||
}
|
||||
}
|
||||
|
||||
func slimReviews(reviews []*gitea_sdk.PullReview) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(reviews))
|
||||
for _, r := range reviews {
|
||||
out = append(out, slimReview(r))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func slimReviewComment(c *gitea_sdk.PullReviewComment) map[string]any {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": c.ID,
|
||||
"body": c.Body,
|
||||
"path": c.Path,
|
||||
"position": c.LineNum,
|
||||
"old_position": c.OldLineNum,
|
||||
"diff_hunk": c.DiffHunk,
|
||||
"user": slim.UserLogin(c.Reviewer),
|
||||
"html_url": c.HTMLURL,
|
||||
"created_at": c.Created,
|
||||
"updated_at": c.Updated,
|
||||
}
|
||||
}
|
||||
|
||||
func slimReviewComments(comments []*gitea_sdk.PullReviewComment) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(comments))
|
||||
for _, c := range comments {
|
||||
out = append(out, slimReviewComment(c))
|
||||
}
|
||||
return out
|
||||
}
|
||||
124
operation/pull/slim_test.go
Normal file
124
operation/pull/slim_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package pull
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gitea_sdk "gitea.dev/sdk"
|
||||
)
|
||||
|
||||
func TestSlimPullRequest(t *testing.T) {
|
||||
now := time.Now()
|
||||
additions := 10
|
||||
deletions := 5
|
||||
changedFiles := 3
|
||||
pr := &gitea_sdk.PullRequest{
|
||||
Index: 1,
|
||||
Title: "Fix bug",
|
||||
Body: "Fixes #123",
|
||||
State: "open",
|
||||
Draft: false,
|
||||
HasMerged: false,
|
||||
Mergeable: true,
|
||||
HTMLURL: "https://gitea.com/org/repo/pulls/1",
|
||||
Poster: &gitea_sdk.User{UserName: "bob"},
|
||||
Labels: []*gitea_sdk.Label{
|
||||
{Name: "bug"},
|
||||
{Name: "priority"},
|
||||
},
|
||||
Comments: 2,
|
||||
Created: &now,
|
||||
Updated: &now,
|
||||
Additions: &additions,
|
||||
Deletions: &deletions,
|
||||
ChangedFiles: &changedFiles,
|
||||
Head: &gitea_sdk.PRBranchInfo{
|
||||
Ref: "fix-branch",
|
||||
Sha: "abc123",
|
||||
},
|
||||
Base: &gitea_sdk.PRBranchInfo{
|
||||
Ref: "main",
|
||||
Sha: "def456",
|
||||
},
|
||||
Assignees: []*gitea_sdk.User{
|
||||
{UserName: "alice"},
|
||||
},
|
||||
Milestone: &gitea_sdk.Milestone{Title: "v1.0"},
|
||||
}
|
||||
|
||||
m := slimPullRequest(pr)
|
||||
|
||||
if m["number"] != int64(1) {
|
||||
t.Errorf("expected number 1, got %v", m["number"])
|
||||
}
|
||||
if m["title"] != "Fix bug" {
|
||||
t.Errorf("expected title Fix bug, got %v", m["title"])
|
||||
}
|
||||
if m["user"] != "bob" {
|
||||
t.Errorf("expected user bob, got %v", m["user"])
|
||||
}
|
||||
if m["additions"] != 10 {
|
||||
t.Errorf("expected additions 10, got %v", m["additions"])
|
||||
}
|
||||
if m["milestone"] != "v1.0" {
|
||||
t.Errorf("expected milestone v1.0, got %v", m["milestone"])
|
||||
}
|
||||
|
||||
labels := m["labels"].([]string)
|
||||
if len(labels) != 2 || labels[0] != "bug" {
|
||||
t.Errorf("expected labels [bug priority], got %v", labels)
|
||||
}
|
||||
|
||||
head := m["head"].(map[string]any)
|
||||
if head["ref"] != "fix-branch" {
|
||||
t.Errorf("expected head ref fix-branch, got %v", head["ref"])
|
||||
}
|
||||
|
||||
assignees := m["assignees"].([]string)
|
||||
if len(assignees) != 1 || assignees[0] != "alice" {
|
||||
t.Errorf("expected assignees [alice], got %v", assignees)
|
||||
}
|
||||
|
||||
// merged fields should not be present for unmerged PR
|
||||
if _, ok := m["merged_at"]; ok {
|
||||
t.Error("merged_at should not be present for unmerged PR")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlimPullRequests_ListIsSlimmer(t *testing.T) {
|
||||
pr := &gitea_sdk.PullRequest{
|
||||
Index: 1,
|
||||
Title: "PR title",
|
||||
State: "open",
|
||||
HTMLURL: "https://gitea.com/org/repo/pulls/1",
|
||||
Poster: &gitea_sdk.User{UserName: "bob"},
|
||||
Body: "Full body text here",
|
||||
Head: &gitea_sdk.PRBranchInfo{Ref: "feature"},
|
||||
Base: &gitea_sdk.PRBranchInfo{Ref: "main"},
|
||||
}
|
||||
|
||||
single := slimPullRequest(pr)
|
||||
list := slimPullRequests([]*gitea_sdk.PullRequest{pr})
|
||||
|
||||
// Single has body, list does not
|
||||
if _, ok := single["body"]; !ok {
|
||||
t.Error("single PR should have body")
|
||||
}
|
||||
if _, ok := list[0]["body"]; ok {
|
||||
t.Error("list PR should not have body")
|
||||
}
|
||||
|
||||
// List has head as string ref, single has head as map
|
||||
if _, ok := single["head"].(map[string]any); !ok {
|
||||
t.Error("single PR head should be a map")
|
||||
}
|
||||
if list[0]["head"] != "feature" {
|
||||
t.Errorf("list PR head should be string ref, got %v", list[0]["head"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlimPullRequests_Nil(t *testing.T) {
|
||||
if r := slimPullRequests(nil); len(r) != 0 {
|
||||
t.Errorf("expected empty slice, got %v", r)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user