From cf42b86f65987d394f0f1cc8666f6bfd68a23f9c Mon Sep 17 00:00:00 2001 From: 1os21ka23r9navae6mrro <1os21ka23r9navae6mrro@gmail.com> Date: Wed, 8 Jul 2026 14:43:26 +0000 Subject: [PATCH] feat(pr): add +comments, +comment-edit, +comment-delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the pull-journal comment lifecycle. The CLI could only create PR comments (via the linked issue's journals); the dedicated v1 pull journals endpoints also support list/update/destroy: - pr +comments -i : list a PR's journals - pr +comment-edit -i -c -b [-s state] - pr +comment-delete -i -c The server's UpdateService hard-requires state in opened|resolved|disabled (omitting it fails with 'State不包含于列表中'), so +comment-edit always sends state, defaulting to opened, with client-side validation of the enum. --comment-id is integer-validated. Production-verified full lifecycle on gitlink.org.cn: create -> list -> edit (note updated) -> delete. 3 unit tests, README examples, bilingual i18n keys. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- README.md | 5 ++ internal/i18n/locales/en-US.json | 5 ++ internal/i18n/locales/zh-CN.json | 5 ++ shortcuts/pr/pr.go | 85 ++++++++++++++++++++++++++++++++ shortcuts/pr/pr_test.go | 57 +++++++++++++++++++++ 5 files changed, 157 insertions(+) diff --git a/README.md b/README.md index e5e4318..45fcbb4 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,11 @@ gitlink-cli pr +reopen --owner Gitlink --repo forgeplus -i 42 # View changed files gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42 +# List, edit, and delete PR comments (pull journals) +gitlink-cli pr +comments --owner Gitlink --repo forgeplus -i 42 +gitlink-cli pr +comment-edit --owner Gitlink --repo forgeplus -i 42 -c 484052 -b "Updated" -s resolved +gitlink-cli pr +comment-delete --owner Gitlink --repo forgeplus -i 42 -c 484052 + # List PR patchset versions gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42 diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..6027728 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -54,6 +54,9 @@ "cmd.org.short": "Organization operations", "cmd.pr.close.short": "Close a pull request", "cmd.pr.comment.short": "Add a comment to a pull request", + "cmd.pr.comment_delete.short": "Delete a pull request comment", + "cmd.pr.comment_edit.short": "Edit a pull request comment", + "cmd.pr.comments.short": "List comments (journals) of a pull request", "cmd.pr.create.short": "Create a pull request", "cmd.pr.diff.short": "Show diff for a pull request", "cmd.pr.files.short": "List changed files in a pull request", @@ -181,6 +184,8 @@ "flag.pr.assignee_id": "Assignee user ID", "flag.pr.base": "Target branch", "flag.pr.body": "PR description", + "flag.pr.comment_id": "Comment (journal) ID", + "flag.pr.comment_state": "Comment state: opened, resolved, or disabled (server requires one; default opened)", "flag.pr.file": "Filter diff by file path", "flag.pr.head": "Source branch", "flag.pr.id": "PR number", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..7b1ee32 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -54,6 +54,9 @@ "cmd.org.short": "组织操作", "cmd.pr.close.short": "关闭拉取请求", "cmd.pr.comment.short": "给拉取请求添加评论", + "cmd.pr.comment_delete.short": "删除合并请求评论", + "cmd.pr.comment_edit.short": "编辑合并请求评论", + "cmd.pr.comments.short": "列出合并请求的评论(journals)", "cmd.pr.create.short": "创建拉取请求", "cmd.pr.diff.short": "显示拉取请求 diff", "cmd.pr.files.short": "列出拉取请求中的变更文件", @@ -181,6 +184,8 @@ "flag.pr.assignee_id": "指派人用户 ID", "flag.pr.base": "目标分支", "flag.pr.body": "PR 描述", + "flag.pr.comment_id": "评论(journal)ID", + "flag.pr.comment_state": "评论状态:opened、resolved 或 disabled(服务端必填,默认 opened)", "flag.pr.file": "按文件路径筛选 diff", "flag.pr.head": "源分支", "flag.pr.id": "PR 编号", diff --git a/shortcuts/pr/pr.go b/shortcuts/pr/pr.go index 03f537f..f660f69 100644 --- a/shortcuts/pr/pr.go +++ b/shortcuts/pr/pr.go @@ -3,6 +3,7 @@ package pr import ( "fmt" "net/url" + "strconv" "strings" "github.com/gitlink-org/gitlink-cli/internal/i18n" @@ -431,9 +432,93 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "comments", + Description: tr.T("cmd.pr.comments.short"), + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + id, _ := ctx.RequireArg("id") + env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/journals", v1RepoPath(ctx), id), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "comment-edit", + Description: tr.T("cmd.pr.comment_edit.short"), + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true}, + {Name: "comment-id", Short: "c", Usage: tr.T("flag.pr.comment_id"), Required: true}, + {Name: "body", Short: "b", Usage: tr.T("flag.comment.body"), Required: true}, + {Name: "state", Short: "s", Usage: tr.T("flag.pr.comment_state"), Default: "opened"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + id, _ := ctx.RequireArg("id") + commentID, err := requireIntFlag(ctx, "comment-id") + if err != nil { + return err + } + body, _ := ctx.RequireArg("body") + state := ctx.Arg("state") + switch state { + case "opened", "resolved", "disabled": + default: + return fmt.Errorf("--state must be one of opened, resolved, disabled; got %q", state) + } + env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/pulls/%s/journals/%s", v1RepoPath(ctx), id, commentID), map[string]interface{}{"note": body, "state": state}) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "comment-delete", + Description: tr.T("cmd.pr.comment_delete.short"), + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true}, + {Name: "comment-id", Short: "c", Usage: tr.T("flag.pr.comment_id"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + id, _ := ctx.RequireArg("id") + commentID, err := requireIntFlag(ctx, "comment-id") + if err != nil { + return err + } + env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/pulls/%s/journals/%s", v1RepoPath(ctx), id, commentID), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, } } +func requireIntFlag(ctx *common.RuntimeContext, name string) (string, error) { + value, err := ctx.RequireArg(name) + if err != nil { + return "", err + } + if _, err := strconv.Atoi(value); err != nil { + return "", fmt.Errorf("--%s must be an integer, got %q", name, value) + } + return value, nil +} + func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { if len(translators) > 0 && translators[0] != nil { return translators[0] diff --git a/shortcuts/pr/pr_test.go b/shortcuts/pr/pr_test.go index eece6d9..3bcbd5e 100644 --- a/shortcuts/pr/pr_test.go +++ b/shortcuts/pr/pr_test.go @@ -531,3 +531,60 @@ func assertEqual(t *testing.T, got interface{}, want interface{}) { t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want) } } + +func TestPRCommentsUsesPullJournalsEndpoint(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/pulls/382/journals.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + writeJSON(t, w, map[string]interface{}{"journals": []interface{}{}, "total_count": float64(0)}) + })) + defer server.Close() + + if err := runPRShortcut(t, server, "comments", map[string]string{"id": "382"}); err != nil { + t.Fatalf("comments failed: %v", err) + } +} + +func TestPRCommentEditSendsNoteAndState(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/pulls/382/journals/484052.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + payload = decodeJSON(t, r) + writeJSON(t, w, map[string]interface{}{"id": float64(484052)}) + })) + defer server.Close() + + args := map[string]string{"id": "382", "comment-id": "484052", "body": "edited", "state": "resolved"} + if err := runPRShortcut(t, server, "comment-edit", args); err != nil { + t.Fatalf("comment-edit failed: %v", err) + } + assertEqual(t, payload["note"], "edited") + assertEqual(t, payload["state"], "resolved") + + args["state"] = "bogus" + if err := runPRShortcut(t, server, "comment-edit", args); err == nil { + t.Fatal("expected error for invalid --state") + } + args["state"] = "opened" + args["comment-id"] = "abc" + if err := runPRShortcut(t, server, "comment-edit", args); err == nil { + t.Fatal("expected error for non-integer --comment-id") + } +} + +func TestPRCommentDeleteUsesPullJournalsEndpoint(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/pulls/382/journals/484052.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 := runPRShortcut(t, server, "comment-delete", map[string]string{"id": "382", "comment-id": "484052"}); err != nil { + t.Fatalf("comment-delete failed: %v", err) + } +}