diff --git a/README.md b/README.md index 88d4072..02faacf 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,11 @@ gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 - # Add a comment gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "Fixed" +# List, edit, and delete comments +gitlink-cli issue +comments --owner Gitlink --repo forgeplus -n 123 --category comment +gitlink-cli issue +comment-edit --owner Gitlink --repo forgeplus -n 123 -c 484049 -b "Updated" +gitlink-cli issue +comment-delete --owner Gitlink --repo forgeplus -n 123 -c 484049 + # List issue assigners gitlink-cli issue +assigners --owner Gitlink --repo forgeplus diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 69a98a6..bbfe856 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -44,6 +44,9 @@ "cmd.issue.batch_list.short": "List issue batch maintenance candidates without changing remote data", "cmd.issue.close.short": "Close an issue", "cmd.issue.comment.short": "Add a comment to an issue", + "cmd.issue.comment_delete.short": "Delete an issue comment", + "cmd.issue.comment_edit.short": "Edit an issue comment", + "cmd.issue.comments.short": "List comments (journals) of an issue", "cmd.issue.create.short": "Create a new issue", "cmd.issue.list.short": "List issues", "cmd.issue.short": "Issue operations", @@ -166,6 +169,9 @@ "flag.issue.batch_list.limit": "Maximum issues to return, capped at 100", "flag.issue.batch_process.limit": "Maximum issues to process, capped at 100", "flag.issue.body": "Issue description", + "flag.issue.comment_id": "Comment (journal) ID", + "flag.issue.comments_category": "Journal category filter: comment or operate", + "flag.issue.comments_keyword": "Filter comments by keyword", "flag.issue.label": "Label ID", "flag.issue.label_filter": "Filter by existing label", "flag.issue.milestone": "Milestone ID", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 355a53d..0e14515 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -44,6 +44,9 @@ "cmd.issue.batch_list.short": "列出议题批量维护候选项,不修改远端数据", "cmd.issue.close.short": "关闭议题", "cmd.issue.comment.short": "给议题添加评论", + "cmd.issue.comment_delete.short": "删除疑修评论", + "cmd.issue.comment_edit.short": "编辑疑修评论", + "cmd.issue.comments.short": "列出疑修的评论(journals)", "cmd.issue.create.short": "创建新议题", "cmd.issue.list.short": "列出议题", "cmd.issue.short": "议题操作", @@ -166,6 +169,9 @@ "flag.issue.batch_list.limit": "最多返回的议题数,上限 100", "flag.issue.batch_process.limit": "最多处理的议题数,上限 100", "flag.issue.body": "议题描述", + "flag.issue.comment_id": "评论(journal)ID", + "flag.issue.comments_category": "journal 类别过滤:comment 或 operate", + "flag.issue.comments_keyword": "按关键字过滤评论", "flag.issue.label": "标签 ID", "flag.issue.label_filter": "按已有标签筛选", "flag.issue.milestone": "里程碑 ID", diff --git a/shortcuts/issue/issue.go b/shortcuts/issue/issue.go index 9f0c256..c158936 100644 --- a/shortcuts/issue/issue.go +++ b/shortcuts/issue/issue.go @@ -309,6 +309,94 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "comments", + Description: tr.T("cmd.issue.comments.short"), + Flags: appendIssueNumberFlags( + common.Flag{Name: "keyword", Short: "k", Usage: tr.T("flag.issue.comments_keyword")}, + common.Flag{Name: "category", Usage: tr.T("flag.issue.comments_category")}, + common.Flag{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + common.Flag{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, + ), + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + number, err := issueNumberArg(ctx) + if err != nil { + return err + } + q := url.Values{} + q.Set("page", ctx.Arg("page")) + q.Set("limit", ctx.Arg("limit")) + if keyword := ctx.Arg("keyword"); keyword != "" { + q.Set("keyword", keyword) + } + if category := ctx.Arg("category"); category != "" { + q.Set("category", category) + } + env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "comment-edit", + Description: tr.T("cmd.issue.comment_edit.short"), + Flags: appendIssueNumberFlags( + common.Flag{Name: "comment-id", Short: "c", Usage: tr.T("flag.issue.comment_id"), Required: true}, + common.Flag{Name: "body", Short: "b", Usage: tr.T("flag.comment.body"), Required: true}, + ), + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + number, err := issueNumberArg(ctx) + if err != nil { + return err + } + commentID, err := parseIssueID(ctx.Arg("comment-id"), "comment-id") + if err != nil { + return err + } + body, err := ctx.RequireArg("body") + if err != nil { + return err + } + env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s/journals/%d", v1RepoPath(ctx), number, commentID), map[string]interface{}{"notes": body}) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "comment-delete", + Description: tr.T("cmd.issue.comment_delete.short"), + Flags: appendIssueNumberFlags( + common.Flag{Name: "comment-id", Short: "c", Usage: tr.T("flag.issue.comment_id"), Required: true}, + ), + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + number, err := issueNumberArg(ctx) + if err != nil { + return err + } + commentID, err := parseIssueID(ctx.Arg("comment-id"), "comment-id") + if err != nil { + return err + } + env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s/journals/%d", v1RepoPath(ctx), number, commentID), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, { Name: "assigners", Description: "List issue assigners", diff --git a/shortcuts/issue/issue_test.go b/shortcuts/issue/issue_test.go index 0112c0d..d69fdd7 100644 --- a/shortcuts/issue/issue_test.go +++ b/shortcuts/issue/issue_test.go @@ -1159,3 +1159,64 @@ func TestNormalizeIssueStatus(t *testing.T) { } } } + +func TestIssueCommentsListsJournalsWithFilters(t *testing.T) { + var query string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues/7/journals.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + query = r.URL.RawQuery + writeJSON(t, w, map[string]interface{}{"journals": []interface{}{}}) + })) + defer server.Close() + + args := map[string]string{"number": "7", "keyword": "lgtm", "category": "comment", "page": "1", "limit": "20"} + if err := runShortcut(t, server, "comments", args); err != nil { + t.Fatalf("comments failed: %v", err) + } + for _, want := range []string{"keyword=lgtm", "category=comment"} { + if !strings.Contains(query, want) { + t.Fatalf("expected %q in query, got %q", want, query) + } + } +} + +func TestIssueCommentEditPatchesJournal(t *testing.T) { + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/7/journals/484049.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + payload = decodeJSON(t, r) + writeJSON(t, w, map[string]interface{}{"id": float64(484049)}) + })) + defer server.Close() + + args := map[string]string{"number": "7", "comment-id": "484049", "body": "edited"} + if err := runShortcut(t, server, "comment-edit", args); err != nil { + t.Fatalf("comment-edit failed: %v", err) + } + if payload["notes"] != "edited" { + t.Fatalf("expected notes=edited, got %v", payload["notes"]) + } + + args["comment-id"] = "abc" + if err := runShortcut(t, server, "comment-edit", args); err == nil { + t.Fatal("expected error for non-integer --comment-id") + } +} + +func TestIssueCommentDeleteUsesJournalEndpoint(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "DELETE" || r.URL.Path != "/v1/owner/repo/issues/7/journals/484049.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + writeJSON(t, w, map[string]interface{}{"status": float64(0)}) + })) + defer server.Close() + + if err := runShortcut(t, server, "comment-delete", map[string]string{"number": "7", "comment-id": "484049"}); err != nil { + t.Fatalf("comment-delete failed: %v", err) + } +}