forgejo-mcp/operation/pull/review.go
Christoph Görn dd9116fbca
fix: 🚨 clear golangci-lint v2 backlog (closes bead forgejo-mcp-hc9)
- errorlint: fmt.Errorf %v→%w across all handlers (errors now wrap the
  underlying SDK/HTTP error for errors.Is/As); errors.Is for
  context.Canceled in cmd
- staticcheck quickfixes: Fprintf over WriteString(Sprintf), tagged
  switches, omit inferred 'any'; QF1001 resolved by hoisting hex checks
  into isHex predicates (normalizeColor, validateSHA)
- govet: reflect.Ptr → reflect.Pointer in pkg/ptr
- errcheck: explicit '_ =' for fs.Parse (ExitOnError) and deferred
  zap Sync
- repair four test assertions the autofix broke (it dropped the
  're.Code != X' half of combined type-assert checks, inverting the
  test); all errors.As sites use 'var re *resource.ResourceError'

golangci-lint: 0 issues. staticcheck, vet, tests: clean.
2026-06-11 00:03:36 +02:00

263 lines
9.6 KiB
Go

package pull
import (
"context"
"encoding/json"
"fmt"
"strings"
"codeberg.org/goern/forgejo-mcp/v2/operation/params"
"codeberg.org/goern/forgejo-mcp/v2/pkg/forgejo"
"codeberg.org/goern/forgejo-mcp/v2/pkg/log"
"codeberg.org/goern/forgejo-mcp/v2/pkg/to"
forgejo_sdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
CreatePullReviewToolName = "create_pull_review"
SubmitPullReviewToolName = "submit_pull_review"
DismissPullReviewToolName = "dismiss_pull_review"
DeletePullReviewToolName = "delete_pull_review"
CreateReviewRequestsToolName = "create_review_requests"
DeleteReviewRequestsToolName = "delete_review_requests"
)
var (
CreatePullReviewTool = mcp.NewTool(
CreatePullReviewToolName,
mcp.WithDescription("Create a pull request review with optional inline comments"),
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
mcp.WithString("body", mcp.Description(params.ReviewBody)),
mcp.WithString("state", mcp.Required(), mcp.Description(params.ReviewState)),
mcp.WithString("comments", mcp.Description(params.ReviewComments)),
)
SubmitPullReviewTool = mcp.NewTool(
SubmitPullReviewToolName,
mcp.WithDescription("Submit a pending pull request review"),
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
mcp.WithNumber("id", mcp.Required(), mcp.Description(params.ReviewID)),
mcp.WithString("body", mcp.Description(params.ReviewBody)),
mcp.WithString("state", mcp.Required(), mcp.Description(params.ReviewState)),
)
DismissPullReviewTool = mcp.NewTool(
DismissPullReviewToolName,
mcp.WithDescription("Dismiss a pull request review"),
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
mcp.WithNumber("id", mcp.Required(), mcp.Description(params.ReviewID)),
mcp.WithString("message", mcp.Required(), mcp.Description(params.DismissMessage)),
)
DeletePullReviewTool = mcp.NewTool(
DeletePullReviewToolName,
mcp.WithDescription("Delete a pending pull request review"),
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
mcp.WithNumber("id", mcp.Required(), mcp.Description(params.ReviewID)),
)
CreateReviewRequestsTool = mcp.NewTool(
CreateReviewRequestsToolName,
mcp.WithDescription("Request reviews from specific users or teams"),
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
mcp.WithString("reviewers", mcp.Description(params.Reviewers)),
mcp.WithString("team_reviewers", mcp.Description(params.TeamReviewers)),
)
DeleteReviewRequestsTool = mcp.NewTool(
DeleteReviewRequestsToolName,
mcp.WithDescription("Cancel pending review requests"),
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
mcp.WithString("reviewers", mcp.Description(params.Reviewers)),
mcp.WithString("team_reviewers", mcp.Description(params.TeamReviewers)),
)
)
func RegisterReviewTools(s *server.MCPServer) {
s.AddTool(CreatePullReviewTool, CreatePullReviewFn)
s.AddTool(SubmitPullReviewTool, SubmitPullReviewFn)
s.AddTool(DismissPullReviewTool, DismissPullReviewFn)
s.AddTool(DeletePullReviewTool, DeletePullReviewFn)
s.AddTool(CreateReviewRequestsTool, CreateReviewRequestsFn)
s.AddTool(DeleteReviewRequestsTool, DeleteReviewRequestsFn)
}
func CreatePullReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePullReviewFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
index, _ := to.Float64(req.GetArguments()["index"])
body, _ := req.GetArguments()["body"].(string)
state, _ := req.GetArguments()["state"].(string)
commentsJSON, _ := req.GetArguments()["comments"].(string)
opt := forgejo_sdk.CreatePullReviewOptions{
State: forgejo_sdk.ReviewStateType(state),
Body: body,
}
if commentsJSON != "" {
var comments []forgejo_sdk.CreatePullReviewComment
if err := json.Unmarshal([]byte(commentsJSON), &comments); err != nil {
return to.ErrorResult(fmt.Errorf("invalid comments JSON: %w", err))
}
opt.Comments = comments
}
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
review, _, err := client.CreatePullReview(owner, repo, int64(index), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create pull review err: %w", err))
}
return to.TextResult(review)
}
func SubmitPullReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called SubmitPullReviewFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
index, _ := to.Float64(req.GetArguments()["index"])
id, _ := to.Float64(req.GetArguments()["id"])
body, _ := req.GetArguments()["body"].(string)
state, _ := req.GetArguments()["state"].(string)
opt := forgejo_sdk.SubmitPullReviewOptions{
State: forgejo_sdk.ReviewStateType(state),
Body: body,
}
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
review, _, err := client.SubmitPullReview(owner, repo, int64(index), int64(id), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("submit pull review err: %w", err))
}
return to.TextResult(review)
}
func DismissPullReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DismissPullReviewFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
index, _ := to.Float64(req.GetArguments()["index"])
id, _ := to.Float64(req.GetArguments()["id"])
message, _ := req.GetArguments()["message"].(string)
opt := forgejo_sdk.DismissPullReviewOptions{
Message: message,
}
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
_, err = client.DismissPullReview(owner, repo, int64(index), int64(id), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("dismiss pull review err: %w", err))
}
return mcp.NewToolResultText(`{"Result":"review dismissed successfully"}`), nil
}
func DeletePullReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeletePullReviewFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
index, _ := to.Float64(req.GetArguments()["index"])
id, _ := to.Float64(req.GetArguments()["id"])
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
_, err = client.DeletePullReview(owner, repo, int64(index), int64(id))
if err != nil {
return to.ErrorResult(fmt.Errorf("delete pull review err: %w", err))
}
return mcp.NewToolResultText(`{"Result":"review deleted successfully"}`), nil
}
func CreateReviewRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateReviewRequestsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
index, _ := to.Float64(req.GetArguments()["index"])
reviewers, _ := req.GetArguments()["reviewers"].(string)
teamReviewers, _ := req.GetArguments()["team_reviewers"].(string)
opt := forgejo_sdk.PullReviewRequestOptions{}
if reviewers != "" {
opt.Reviewers = splitCSV(reviewers)
}
if teamReviewers != "" {
opt.TeamReviewers = splitCSV(teamReviewers)
}
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
_, err = client.CreateReviewRequests(owner, repo, int64(index), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create review requests err: %w", err))
}
return mcp.NewToolResultText(`{"Result":"review requests created successfully"}`), nil
}
func DeleteReviewRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteReviewRequestsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
index, _ := to.Float64(req.GetArguments()["index"])
reviewers, _ := req.GetArguments()["reviewers"].(string)
teamReviewers, _ := req.GetArguments()["team_reviewers"].(string)
opt := forgejo_sdk.PullReviewRequestOptions{}
if reviewers != "" {
opt.Reviewers = splitCSV(reviewers)
}
if teamReviewers != "" {
opt.TeamReviewers = splitCSV(teamReviewers)
}
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
_, err = client.DeleteReviewRequests(owner, repo, int64(index), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete review requests err: %w", err))
}
return mcp.NewToolResultText(`{"Result":"review requests deleted successfully"}`), nil
}
func splitCSV(s string) []string {
parts := strings.Split(s, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
trimmed := strings.TrimSpace(p)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}