diff --git a/cmd/cli/prs_comments.go b/cmd/cli/prs_comments.go index 6525baf..f573a6e 100644 --- a/cmd/cli/prs_comments.go +++ b/cmd/cli/prs_comments.go @@ -41,14 +41,21 @@ var prCommentsListCmd = &cobra.Command{ } page, pagelen := paginationArgs(cmd) + fetchAll, _ := cmd.Flags().GetBool("all") client := getClient() - result, err := client.ListPRComments(bitbucket.ListPRCommentsArgs{ + listArgs := bitbucket.ListPRCommentsArgs{ Workspace: workspace, RepoSlug: repoSlug, PRID: prID, Page: page, Pagelen: pagelen, - }) + } + var result *bitbucket.Paginated[bitbucket.PRComment] + if fetchAll { + result, err = client.ListAllPRComments(listArgs) + } else { + result, err = client.ListPRComments(listArgs) + } if err != nil { return err } @@ -176,6 +183,7 @@ func init() { prCommentsCmd.AddCommand(prCommentsResolveCmd) addPaginationFlags(prCommentsListCmd) + prCommentsListCmd.Flags().Bool("all", false, "Fetch every comment page (follows pagination)") prCommentsAddCmd.Flags().StringP("content", "m", "", "Comment body (markdown supported)") prCommentsAddCmd.Flags().Int("parent", 0, "Reply to this comment ID (creates a threaded reply)") diff --git a/internal/bitbucket/comments.go b/internal/bitbucket/comments.go index 660010d..928f88f 100644 --- a/internal/bitbucket/comments.go +++ b/internal/bitbucket/comments.go @@ -34,6 +34,37 @@ func (c *Client) ListPRComments(args ListPRCommentsArgs) (*Paginated[PRComment], return GetPaginated[PRComment](c, path) } +// ListAllPRComments fetches every page of comments, following the paginated +// `next` cursor, and returns them merged into a single result. Use when a PR +// may carry more comments than one page (default page size 50). +func (c *Client) ListAllPRComments(args ListPRCommentsArgs) (*Paginated[PRComment], error) { + page := args.Page + if page == 0 { + page = 1 + } + + var all []PRComment + var last *Paginated[PRComment] + for { + args.Page = page + res, err := c.ListPRComments(args) + if err != nil { + return nil, err + } + all = append(all, res.Values...) + last = res + if res.Next == "" { + break + } + page++ + } + + last.Values = all + last.Size = len(all) + last.Next = "" + return last, nil +} + type CreatePRCommentArgs struct { Workspace string `json:"workspace" jsonschema:"Workspace slug"` RepoSlug string `json:"repo_slug" jsonschema:"Repository slug"` diff --git a/internal/bitbucket/comments_test.go b/internal/bitbucket/comments_test.go index cd1b7b8..ab04676 100644 --- a/internal/bitbucket/comments_test.go +++ b/internal/bitbucket/comments_test.go @@ -135,3 +135,32 @@ func TestCreatePRComment_ValidatesRequiredFields(t *testing.T) { }) } } + +// TestListAllPRComments_WalksPages verifies --all follows the paginated next +// cursor across pages and merges every comment into one result. +func TestListAllPRComments_WalksPages(t *testing.T) { + c := newVCRClient(t, "list_comments_paged") + + res, err := c.ListAllPRComments(ListPRCommentsArgs{ + Workspace: "demo-ws", + RepoSlug: "demo-repo", + PRID: 1, + }) + if err != nil { + t.Fatalf("ListAllPRComments: %v", err) + } + + if len(res.Values) != 2 { + t.Fatalf("merged comments = %d, want 2 (one per page)", len(res.Values)) + } + if res.Next != "" { + t.Errorf("merged result Next = %q, want empty", res.Next) + } + got := map[int]bool{} + for _, cm := range res.Values { + got[cm.ID] = true + } + if !got[2001] || !got[2002] { + t.Errorf("want comments 2001 (page 1) and 2002 (page 2), got %v", got) + } +} diff --git a/internal/bitbucket/testdata/fixtures/list_comments_paged.yaml b/internal/bitbucket/testdata/fixtures/list_comments_paged.yaml new file mode 100644 index 0000000..ff10eb3 --- /dev/null +++ b/internal/bitbucket/testdata/fixtures/list_comments_paged.yaml @@ -0,0 +1,95 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: api.bitbucket.org + url: https://api.bitbucket.org/2.0/repositories/demo-ws/demo-repo/pullrequests/1/comments?pagelen=50&page=1 + method: GET + headers: + Accept: + - application/json + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + uncompressed: true + status: 200 OK + code: 200 + duration: 10ms + headers: + Content-Type: + - application/json; charset=utf-8 + body: | + { + "pagelen": 1, + "size": 2, + "page": 1, + "next": "https://api.bitbucket.org/2.0/repositories/demo-ws/demo-repo/pullrequests/1/comments?pagelen=50&page=2", + "values": [ + { + "id": 2001, + "content": {"raw": "page one comment"}, + "user": {"display_name": "Zach Snell", "type": "user", "links": {}}, + "created_on": "2026-03-05T09:00:00.000000+00:00", + "updated_on": "2026-03-05T09:00:00.000000+00:00", + "inline": null, + "parent": null, + "deleted": false, + "pending": false, + "type": "pullrequest_comment", + "links": {} + } + ] + } + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: api.bitbucket.org + url: https://api.bitbucket.org/2.0/repositories/demo-ws/demo-repo/pullrequests/1/comments?pagelen=50&page=2 + method: GET + headers: + Accept: + - application/json + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + uncompressed: true + status: 200 OK + code: 200 + duration: 10ms + headers: + Content-Type: + - application/json; charset=utf-8 + body: | + { + "pagelen": 1, + "size": 2, + "page": 2, + "next": "", + "values": [ + { + "id": 2002, + "content": {"raw": "page two comment"}, + "user": {"display_name": "Aulia Lionar", "type": "user", "links": {}}, + "created_on": "2026-03-05T09:05:00.000000+00:00", + "updated_on": "2026-03-05T09:05:00.000000+00:00", + "inline": null, + "parent": null, + "deleted": false, + "pending": false, + "type": "pullrequest_comment", + "links": {} + } + ] + }