mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-02-27 09:05:12 +00:00
feat: add title parameter to merge_pull_request tool (#134)
## Summary - Add missing `title` parameter to `merge_pull_request` tool for custom merge commit titles - Use `params.GetIndex()` for consistent index parameter handling (supports both string and number inputs) - Add test for `MergePullRequestFn` Closes #120 ## Test plan - [x] `go test ./operation/pull/ -run TestMergePullRequestFn` passes - [x] All existing tests pass (`go test ./...`) - [x] Build succeeds (`make build`) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/134 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: silverwind <silverwind@noreply.gitea.com> Co-committed-by: silverwind <silverwind@noreply.gitea.com>
This commit is contained in:
@@ -182,7 +182,8 @@ var (
|
|||||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||||
mcp.WithString("merge_style", mcp.Description("merge style: merge, rebase, rebase-merge, squash, fast-forward-only"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")),
|
mcp.WithString("merge_style", mcp.Description("merge style: merge, rebase, rebase-merge, squash, fast-forward-only"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")),
|
||||||
mcp.WithString("message", mcp.Description("custom merge commit message (optional)")),
|
mcp.WithString("title", mcp.Description("custom merge commit title")),
|
||||||
|
mcp.WithString("message", mcp.Description("custom merge commit message")),
|
||||||
mcp.WithBoolean("delete_branch", mcp.Description("delete the branch after merge"), mcp.DefaultBool(false)),
|
mcp.WithBoolean("delete_branch", mcp.Description("delete the branch after merge"), mcp.DefaultBool(false)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -845,9 +846,9 @@ func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(errors.New("repo is required"))
|
return to.ErrorResult(errors.New("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(errors.New("index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeStyle := "merge"
|
mergeStyle := "merge"
|
||||||
@@ -855,6 +856,11 @@ func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
|
|||||||
mergeStyle = style
|
mergeStyle = style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
title := ""
|
||||||
|
if t, exists := req.GetArguments()["title"].(string); exists {
|
||||||
|
title = t
|
||||||
|
}
|
||||||
|
|
||||||
message := ""
|
message := ""
|
||||||
if msg, exists := req.GetArguments()["message"].(string); exists {
|
if msg, exists := req.GetArguments()["message"].(string); exists {
|
||||||
message = msg
|
message = msg
|
||||||
@@ -872,26 +878,27 @@ func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
|
|||||||
|
|
||||||
opt := gitea_sdk.MergePullRequestOption{
|
opt := gitea_sdk.MergePullRequestOption{
|
||||||
Style: gitea_sdk.MergeStyle(mergeStyle),
|
Style: gitea_sdk.MergeStyle(mergeStyle),
|
||||||
|
Title: title,
|
||||||
Message: message,
|
Message: message,
|
||||||
DeleteBranchAfterMerge: deleteBranch,
|
DeleteBranchAfterMerge: deleteBranch,
|
||||||
}
|
}
|
||||||
|
|
||||||
merged, resp, err := client.MergePullRequest(owner, repo, int64(index), opt)
|
merged, resp, err := client.MergePullRequest(owner, repo, index, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v err: %v", owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !merged && resp != nil && resp.StatusCode >= 400 {
|
if !merged && resp != nil && resp.StatusCode >= 400 {
|
||||||
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v failed: HTTP %d %s", owner, repo, int64(index), resp.StatusCode, resp.Status))
|
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v failed: HTTP %d %s", owner, repo, index, resp.StatusCode, resp.Status))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !merged {
|
if !merged {
|
||||||
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v returned merged=false", owner, repo, int64(index)))
|
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v returned merged=false", owner, repo, index))
|
||||||
}
|
}
|
||||||
|
|
||||||
successMsg := map[string]any{
|
successMsg := map[string]any{
|
||||||
"merged": merged,
|
"merged": merged,
|
||||||
"pr_index": int64(index),
|
"pr_index": index,
|
||||||
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||||
"merge_style": mergeStyle,
|
"merge_style": mergeStyle,
|
||||||
"branch_deleted": deleteBranch,
|
"branch_deleted": deleteBranch,
|
||||||
|
|||||||
@@ -117,6 +117,123 @@ func TestEditPullRequestFn(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMergePullRequestFn(t *testing.T) {
|
||||||
|
const (
|
||||||
|
owner = "octo"
|
||||||
|
repo = "demo"
|
||||||
|
index = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
gotMethod string
|
||||||
|
gotPath string
|
||||||
|
gotBody map[string]any
|
||||||
|
)
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/version":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"version":"1.12.0"}`))
|
||||||
|
case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo):
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"private":false}`))
|
||||||
|
case fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index):
|
||||||
|
mu.Lock()
|
||||||
|
gotMethod = r.Method
|
||||||
|
gotPath = r.URL.Path
|
||||||
|
var body map[string]any
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
gotBody = body
|
||||||
|
mu.Unlock()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
origHost := flag.Host
|
||||||
|
origToken := flag.Token
|
||||||
|
origVersion := flag.Version
|
||||||
|
flag.Host = server.URL
|
||||||
|
flag.Token = ""
|
||||||
|
flag.Version = "test"
|
||||||
|
defer func() {
|
||||||
|
flag.Host = origHost
|
||||||
|
flag.Token = origToken
|
||||||
|
flag.Version = origVersion
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := mcp.CallToolRequest{
|
||||||
|
Params: mcp.CallToolParams{
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"owner": owner,
|
||||||
|
"repo": repo,
|
||||||
|
"index": float64(index),
|
||||||
|
"merge_style": "squash",
|
||||||
|
"title": "feat: my squashed commit",
|
||||||
|
"message": "Squash merge of PR #5",
|
||||||
|
"delete_branch": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := MergePullRequestFn(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MergePullRequestFn() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if gotMethod != http.MethodPost {
|
||||||
|
t.Fatalf("expected POST request, got %s", gotMethod)
|
||||||
|
}
|
||||||
|
if gotPath != fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index) {
|
||||||
|
t.Fatalf("unexpected path: %s", gotPath)
|
||||||
|
}
|
||||||
|
if gotBody["Do"] != "squash" {
|
||||||
|
t.Fatalf("expected Do 'squash', got %v", gotBody["Do"])
|
||||||
|
}
|
||||||
|
if gotBody["MergeTitleField"] != "feat: my squashed commit" {
|
||||||
|
t.Fatalf("expected MergeTitleField 'feat: my squashed commit', got %v", gotBody["MergeTitleField"])
|
||||||
|
}
|
||||||
|
if gotBody["MergeMessageField"] != "Squash merge of PR #5" {
|
||||||
|
t.Fatalf("expected MergeMessageField 'Squash merge of PR #5', got %v", gotBody["MergeMessageField"])
|
||||||
|
}
|
||||||
|
if gotBody["delete_branch_after_merge"] != true {
|
||||||
|
t.Fatalf("expected delete_branch_after_merge true, got %v", gotBody["delete_branch_after_merge"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Content) == 0 {
|
||||||
|
t.Fatalf("expected content in result")
|
||||||
|
}
|
||||||
|
textContent, ok := mcp.AsTextContent(result.Content[0])
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected text content, got %T", result.Content[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed struct {
|
||||||
|
Result map[string]any `json:"Result"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil {
|
||||||
|
t.Fatalf("unmarshal result text: %v", err)
|
||||||
|
}
|
||||||
|
if parsed.Result["merged"] != true {
|
||||||
|
t.Fatalf("expected merged=true, got %v", parsed.Result["merged"])
|
||||||
|
}
|
||||||
|
if parsed.Result["merge_style"] != "squash" {
|
||||||
|
t.Fatalf("expected merge_style 'squash', got %v", parsed.Result["merge_style"])
|
||||||
|
}
|
||||||
|
if parsed.Result["branch_deleted"] != true {
|
||||||
|
t.Fatalf("expected branch_deleted=true, got %v", parsed.Result["branch_deleted"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetPullRequestDiffFn(t *testing.T) {
|
func TestGetPullRequestDiffFn(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
owner = "octo"
|
owner = "octo"
|
||||||
|
|||||||
Reference in New Issue
Block a user