From 026f40bc15fa52e5ce198c302bfaac9d37730e20 Mon Sep 17 00:00:00 2001 From: Gronod Date: Tue, 15 Sep 2026 14:48:10 +0100 Subject: [PATCH] feat(issue): add dependency methods to issue tools Expose Gitea /issues/{i}/dependencies endpoints: list_dependencies on issue_read and add_dependency/remove_dependency on issue_write via the blocking_issue param. Assisted-by: Devin:SWE-2 Max --- operation/issue/blocking.go | 54 ++++++++++++++++++++++++++++++++ operation/issue/blocking_test.go | 42 ++++++++++++++++++++++--- operation/issue/issue.go | 11 +++++-- 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/operation/issue/blocking.go b/operation/issue/blocking.go index d9f9318..62a1e14 100644 --- a/operation/issue/blocking.go +++ b/operation/issue/blocking.go @@ -66,6 +66,60 @@ func setIssueBlockingFn(ctx context.Context, args map[string]any, block bool) (* return to.TextResult(slimIssue(issue)) } +func listIssueDependenciesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, repo, index, err := issueRef(args) + 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)) + } + listOpt := gitea_sdk.ListOptions{Page: page, PageSize: pageSize} + issues, _, err := client.Issues.ListIssueDependencies(ctx, owner, repo, index, gitea_sdk.ListIssueDependenciesOptions{ + ListOptions: listOpt, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err)) + } + return to.TextResult(slimIssues(issues)) +} + +func addIssueDependencyFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + return setIssueDependencyFn(ctx, args, true) +} + +func removeIssueDependencyFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + return setIssueDependencyFn(ctx, args, false) +} + +func setIssueDependencyFn(ctx context.Context, args map[string]any, add bool) (*mcp.CallToolResult, error) { + owner, repo, index, err := issueRef(args) + if err != nil { + return to.ErrorResult(err) + } + blocking, err := params.GetIndex(args, "blocking_issue") + 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)) + } + meta := gitea_sdk.IssueMeta{Index: blocking} + var issue *gitea_sdk.Issue + if add { + issue, _, err = client.Issues.CreateIssueDependency(ctx, owner, repo, index, meta) + } else { + issue, _, err = client.Issues.RemoveIssueDependency(ctx, owner, repo, index, meta) + } + if err != nil { + return to.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err)) + } + return to.TextResult(slimIssue(issue)) +} + func issueRef(args map[string]any) (owner, repo string, index int64, err error) { if owner, err = params.GetString(args, "owner"); err != nil { return "", "", 0, err diff --git a/operation/issue/blocking_test.go b/operation/issue/blocking_test.go index a2b9c40..cd1c4d2 100644 --- a/operation/issue/blocking_test.go +++ b/operation/issue/blocking_test.go @@ -21,11 +21,13 @@ func Test_issueBlocking(t *testing.T) { repo = "demo" ) blocksPath := fmt.Sprintf("/api/v1/repos/%s/%s/issues/42/blocks", owner, repo) + depsPath := fmt.Sprintf("/api/v1/repos/%s/%s/issues/42/dependencies", owner, repo) tests := []struct { name string fn func(context.Context, map[string]any) (*mcp.CallToolResult, error) args map[string]any + wantPath string wantMethod string wantQuery string wantIndex float64 @@ -34,29 +36,57 @@ func Test_issueBlocking(t *testing.T) { name: "list blocks paginates", fn: listIssueBlocksFn, args: map[string]any{"page": float64(2), "per_page": float64(10)}, - wantMethod: "GET", + wantPath: blocksPath, + wantMethod: http.MethodGet, wantQuery: "limit=10&page=2", }, { name: "block issue posts index", fn: blockIssueFn, args: map[string]any{"blocked_issue": float64(7)}, - wantMethod: "POST", + wantPath: blocksPath, + wantMethod: http.MethodPost, wantIndex: 7, }, { name: "unblock issue deletes with index", fn: unblockIssueFn, args: map[string]any{"blocked_issue": float64(7)}, - wantMethod: "DELETE", + wantPath: blocksPath, + wantMethod: http.MethodDelete, wantIndex: 7, }, + { + name: "list dependencies paginates", + fn: listIssueDependenciesFn, + args: map[string]any{"page": float64(3), "per_page": float64(5)}, + wantPath: depsPath, + wantMethod: http.MethodGet, + wantQuery: "limit=5&page=3", + }, + { + name: "add dependency posts index", + fn: addIssueDependencyFn, + args: map[string]any{"blocking_issue": float64(9)}, + wantPath: depsPath, + wantMethod: http.MethodPost, + wantIndex: 9, + }, + { + name: "remove dependency deletes with index", + fn: removeIssueDependencyFn, + args: map[string]any{"blocking_issue": float64(9)}, + wantPath: depsPath, + wantMethod: http.MethodDelete, + wantIndex: 9, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var ( gotMethod string + gotPath string gotQuery string gotBody map[string]any ) @@ -65,8 +95,9 @@ func Test_issueBlocking(t *testing.T) { case "/api/v1/version": w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"version":"1.27.2"}`)) - case blocksPath: + case blocksPath, depsPath: gotMethod = r.Method + gotPath = r.URL.Path gotQuery = r.URL.RawQuery _ = json.NewDecoder(r.Body).Decode(&gotBody) w.Header().Set("Content-Type", "application/json") @@ -98,6 +129,9 @@ func Test_issueBlocking(t *testing.T) { if gotMethod != tt.wantMethod { t.Fatalf("method = %s, want %s", gotMethod, tt.wantMethod) } + if gotPath != tt.wantPath { + t.Fatalf("path = %s, want %s", gotPath, tt.wantPath) + } if tt.wantQuery != "" && gotQuery != tt.wantQuery { t.Fatalf("query = %s, want %s", gotQuery, tt.wantQuery) } diff --git a/operation/issue/issue.go b/operation/issue/issue.go index 18c7ee3..dc2a151 100644 --- a/operation/issue/issue.go +++ b/operation/issue/issue.go @@ -57,7 +57,7 @@ var ( IssueReadToolName, "Read issue: details, comments, labels, or blocking relationships.", annotation.ReadOnly("Read issue details"), - tool.String("method", tool.Required(), tool.Enum("get", "get_comments", "get_labels", "list_blocks")), + tool.String("method", tool.Required(), tool.Enum("get", "get_comments", "get_labels", "list_blocks", "list_dependencies")), tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), tool.Number("issue_number", tool.Required()), @@ -69,7 +69,7 @@ var ( IssueWriteToolName, "Write issues: create, update, manage comments, labels, and blocking relationships.", annotation.Write("Create or update issues, comments, and labels"), - tool.String("method", tool.Required(), tool.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels", "block_issue", "unblock_issue")), + tool.String("method", tool.Required(), tool.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels", "block_issue", "unblock_issue", "add_dependency", "remove_dependency")), tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), tool.Number("issue_number", tool.Description("required except for 'create'")), @@ -85,6 +85,7 @@ var ( tool.String("deadline", tool.Description("ISO 8601")), tool.Boolean("remove_deadline"), tool.Number("blocked_issue", tool.Description("issue number that issue_number blocks (required for 'block_issue'/'unblock_issue')")), + tool.Number("blocking_issue", tool.Description("issue number that blocks issue_number (required for 'add_dependency'/'remove_dependency')")), ) ) @@ -117,6 +118,8 @@ func issueReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, return getIssueLabelsFn(ctx, args) case "list_blocks": return listIssueBlocksFn(ctx, args) + case "list_dependencies": + return listIssueDependenciesFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } @@ -148,6 +151,10 @@ func issueWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult return blockIssueFn(ctx, args) case "unblock_issue": return unblockIssueFn(ctx, args) + case "add_dependency": + return addIssueDependencyFn(ctx, args) + case "remove_dependency": + return removeIssueDependencyFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } -- 2.39.5