Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
51b7653
feat(discussion/client): add discussion client package
babakks Jun 1, 2026
d3a1538
feat(discussion/shared): add shared utilities for discussion commands
babakks Jun 1, 2026
8b73951
feat(discussion list): add discussion list command
babakks Jun 1, 2026
57008e7
feat(discussion view): add discussion view command
babakks Jun 1, 2026
7bd67a8
feat(discussion create): add discussion create command
babakks Jun 1, 2026
797effe
feat(discussion edit): add discussion edit command
babakks Jun 1, 2026
dc5d7c1
fix(discussion): various polish and small fixes
babakks Jun 4, 2026
0d93148
refactor(discussion): extract command-level consts for enums
babakks Jun 4, 2026
97b2296
refactor(discussion list): make pager call/error consistent with view…
babakks Jun 4, 2026
dcd3507
docs(discussion/client): add godoc to exported consts
babakks Jun 4, 2026
d6a089d
chore(discussion/client): fix formatting
babakks Jun 4, 2026
b102900
fix(discussion): handle partial failure on create/update label mutations
babakks Jun 5, 2026
6dcb0b0
docs(discussion view): improve long help text for clarity
babakks Jun 5, 2026
fbd733e
refactor(discussion): add Cursor field and ExportData to DiscussionLi…
babakks Jun 5, 2026
e104885
refactor(discussion/client): precheck discussions enabled via getRepo…
babakks Jun 5, 2026
ca8e126
fix(discussion list): print "answered" instead of checkmark in non-tt…
babakks Jun 5, 2026
ada8583
test(discussion): add acceptance tests for discussion commands
babakks Jun 5, 2026
8ec0830
docs(acceptance): add new jq2env and jq-assert functions
babakks Jun 5, 2026
f147d02
test(discussion list): consolidate tests into table-driven format
babakks Jun 5, 2026
e61df07
Merge branch 'trunk' into feature/discussion
babakks Jun 5, 2026
c1f3c1a
fix(discussion): add missing repo flag override
babakks Jun 8, 2026
9d413e7
test(discussion list): rename TestNewCmdList2 to TestNewCmdList
babakks Jun 9, 2026
e2d150d
fix(discussion): remove redundant error wrapping on ListCategories
babakks Jun 9, 2026
95fc89c
chore(discussion): remove unused HttpClient field from create and edit
babakks Jun 9, 2026
4166ecf
chore: apply formatting
babakks Jun 9, 2026
2618999
feat(discussion/client): add comment manipulation methods
babakks Jun 6, 2026
6f5e114
feat(discussion): add discussion comment command
babakks Jun 8, 2026
82ac0d7
refactor(acceptance): use discussion comment command instead of raw A…
babakks Jun 8, 2026
2629753
test(acceptance): add discussion comment acceptance test
babakks Jun 8, 2026
61a4476
test(discussion comment): add non-tty delete flag validation test case
babakks Jun 8, 2026
d026f8f
feat(discussion): support comment URLs in --replies and comment command
babakks Jun 8, 2026
6394ca8
test(acceptance): cover discussion comment URLs in comment and view t…
babakks Jun 8, 2026
bc7ed48
chore: fix formatting
babakks Jun 8, 2026
55928c9
refactor(discussion view): replace --replies flag with positional com…
babakks Jun 10, 2026
8747c69
refactor(discussion/client): take host instead of repo in GetComment
babakks Jun 10, 2026
869c044
test(acceptance): use positional comment argument in discussion view …
babakks Jun 10, 2026
8d2b059
fix(discussion view): error when --comments is used with a comment ar…
babakks Jun 10, 2026
42db02d
Merge pull request #13620 from cli/babakks/add-discussion-comment
babakks Jun 10, 2026
27aabfa
fix(discussion view): use color scheme method for success icon
babakks Jun 10, 2026
951d76e
refactor(discussion list): simplify no-results message and use succes…
babakks Jun 10, 2026
9f2da11
fix(discussion view): show comments and replies in chronological order
babakks Jun 10, 2026
06d2e34
docs(discussion list): clarify answered examples refer to Q&A discuss…
babakks Jun 10, 2026
5d77247
fix(discussion view): print only requested items in non-tty output
babakks Jun 10, 2026
d63ab8d
fix(discussion/shared): error on out-of-range discussion number in URL
babakks Jun 10, 2026
5e6a58b
test(acceptance): fix discussion comment acceptance tests
babakks Jun 10, 2026
616d929
chore(discussion/client): rename client files
babakks Jun 10, 2026
69855b7
fix(discussion comment): fix bug in requiring body/body-file in add/e…
babakks Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(discussion): various polish and small fixes
- Fix double default in list --limit help text
- Wire --body-file flag in create command (symmetric with edit)
- Cap comment/reply pagination to maxPageSize (100)
- Use int32 for discussion number params (fixes CodeQL alerts)
- Use strconv.ParseInt instead of Atoi for int32 targets
- Fix IsStdoutTTY -> IsStderrTTY for web mode message
- Use CancelError instead of error string for no-op edit
- Trim whitespace from label names in resolution
- Fix category error formatting (comma-joined instead of %q slice)
- Fix typo in lookup.go comment
- Remove dead len==0 check in view replies path
- Inline exporterNeedsComments into needsComments
- Validate comment ownership in GetCommentReplies
- Remove unimplemented interface methods and CloseReason type

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
babakks and Copilot committed Jun 4, 2026
commit dc5d7c18a6fd325a634792401f54688e8f195fe9
13 changes: 3 additions & 10 deletions pkg/cmd/discussion/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,11 @@ import "github.com/cli/cli/v2/internal/ghrepo"
type DiscussionClient interface {
List(repo ghrepo.Interface, filters ListFilters, after string, limit int) (*DiscussionListResult, error)
Search(repo ghrepo.Interface, filters SearchFilters, after string, limit int) (*DiscussionListResult, error)
GetByNumber(repo ghrepo.Interface, number int) (*Discussion, error)
GetWithComments(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*Discussion, error)
GetCommentReplies(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*Discussion, error)
GetByNumber(repo ghrepo.Interface, number int32) (*Discussion, error)
GetWithComments(repo ghrepo.Interface, number int32, commentLimit int, after string, newest bool) (*Discussion, error)
GetCommentReplies(repo ghrepo.Interface, number int32, commentID string, limit int, after string, newest bool) (*Discussion, error)
ListCategories(repo ghrepo.Interface) ([]DiscussionCategory, error)
ListLabels(repo ghrepo.Interface) ([]DiscussionLabel, error)
Create(repo ghrepo.Interface, input CreateDiscussionInput) (*Discussion, error)
Update(repo ghrepo.Interface, input UpdateDiscussionInput) (*Discussion, error)
Close(repo ghrepo.Interface, id string, reason CloseReason) (*Discussion, error)
Reopen(repo ghrepo.Interface, id string) (*Discussion, error)
AddComment(repo ghrepo.Interface, discussionID string, body string, replyToID string) (*DiscussionComment, error)
Lock(repo ghrepo.Interface, id string, reason string) error
Unlock(repo ghrepo.Interface, id string) error
MarkAnswer(repo ghrepo.Interface, commentID string) error
UnmarkAnswer(repo ghrepo.Interface, commentID string) error
}
52 changes: 19 additions & 33 deletions pkg/cmd/discussion/client/client_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@
return &result, nil
}

func (c *discussionClient) GetByNumber(repo ghrepo.Interface, number int) (*Discussion, error) {
func (c *discussionClient) GetByNumber(repo ghrepo.Interface, number int32) (*Discussion, error) {
var query struct {
Repository struct {
HasDiscussionsEnabled bool
Expand Down Expand Up @@ -483,7 +483,7 @@
return dc
}

func (c *discussionClient) GetWithComments(repo ghrepo.Interface, number int, limit int, after string, newest bool) (*Discussion, error) {
func (c *discussionClient) GetWithComments(repo ghrepo.Interface, number int32, limit int, after string, newest bool) (*Discussion, error) {
var query struct {
Repository struct {
HasDiscussionsEnabled bool
Expand Down Expand Up @@ -514,12 +514,12 @@
}

if newest {
variables["last"] = githubv4.Int(limit)
variables["last"] = githubv4.Int(min(limit, maxPageSize))
if after != "" {
variables["before"] = githubv4.String(after)
}
} else {
variables["first"] = githubv4.Int(limit)
variables["first"] = githubv4.Int(min(limit, maxPageSize))
if after != "" {
variables["after"] = githubv4.String(after)
}
Expand Down Expand Up @@ -585,7 +585,7 @@
// GetCommentReplies fetches a discussion and a single comment with its
// paginated replies. It uses the top-level node(id:) query for the comment
// because the Discussion type does not expose a comment(id:) field.
func (c *discussionClient) GetCommentReplies(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*Discussion, error) {
func (c *discussionClient) GetCommentReplies(repo ghrepo.Interface, number int32, commentID string, limit int, after string, newest bool) (*Discussion, error) {
var query struct {
Repository struct {
HasDiscussionsEnabled bool
Expand All @@ -608,6 +608,13 @@
TotalCount int
}
}
Discussion struct {
Number int
Repository struct {
Owner struct{ Login string }
Name string
}
}
Replies struct {
TotalCount int
PageInfo struct {
Expand All @@ -634,12 +641,12 @@
}

if newest {
variables["last"] = githubv4.Int(limit)
variables["last"] = githubv4.Int(min(limit, maxPageSize))
if after != "" {
variables["before"] = githubv4.String(after)
}
} else {
variables["first"] = githubv4.Int(limit)
variables["first"] = githubv4.Int(min(limit, maxPageSize))
if after != "" {
variables["after"] = githubv4.String(after)
}
Expand All @@ -663,6 +670,11 @@
return nil, fmt.Errorf("node %s is not a discussion comment", commentID)
}

if !strings.EqualFold(src.Discussion.Repository.Owner.Login, repo.RepoOwner()) ||
!strings.EqualFold(src.Discussion.Repository.Name, repo.RepoName()) {
return nil, fmt.Errorf("comment %s does not belong to %s/%s", commentID, repo.RepoOwner(), repo.RepoName())
}
Comment thread
BagToad marked this conversation as resolved.
Outdated

d := mapDiscussionFromListNode(query.Repository.Discussion.discussionListNode)

for _, rg := range query.Repository.Discussion.ReactionGroups {
Expand Down Expand Up @@ -1028,31 +1040,5 @@

return &d, nil
}

Check failure on line 1043 in pkg/cmd/discussion/client/client_impl.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gofmt)
func (c *discussionClient) Close(_ ghrepo.Interface, _ string, _ CloseReason) (*Discussion, error) {
return nil, fmt.Errorf("not implemented")
}

func (c *discussionClient) Reopen(_ ghrepo.Interface, _ string) (*Discussion, error) {
return nil, fmt.Errorf("not implemented")
}

func (c *discussionClient) AddComment(_ ghrepo.Interface, _ string, _ string, _ string) (*DiscussionComment, error) {
return nil, fmt.Errorf("not implemented")
}

func (c *discussionClient) Lock(_ ghrepo.Interface, _ string, _ string) error {
return fmt.Errorf("not implemented")
}

func (c *discussionClient) Unlock(_ ghrepo.Interface, _ string) error {
return fmt.Errorf("not implemented")
}

func (c *discussionClient) MarkAnswer(_ ghrepo.Interface, _ string) error {
return fmt.Errorf("not implemented")
}

func (c *discussionClient) UnmarkAnswer(_ ghrepo.Interface, _ string) error {
return fmt.Errorf("not implemented")
}
63 changes: 63 additions & 0 deletions pkg/cmd/discussion/client/client_impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1870,6 +1870,7 @@ func TestGetCommentReplies(t *testing.T) {
"isAnswer": true,
"upvoteCount": 5,
"reactionGroups": [{"content": "HEART", "users": {"totalCount": 2}}],
"discussion": {"number": 42, "repository": {"owner": {"login": "OWNER"}, "name": "REPO"}},
"replies": {
"totalCount": 1,
"pageInfo": {"endCursor": "REP_CUR", "hasNextPage": true, "startCursor": "REP_START", "hasPreviousPage": false},
Expand Down Expand Up @@ -1996,6 +1997,7 @@ func TestGetCommentReplies(t *testing.T) {
"isAnswer": false,
"upvoteCount": 0,
"reactionGroups": [],
"discussion": {"number": 42, "repository": {"owner": {"login": "OWNER"}, "name": "REPO"}},
"replies": {
"totalCount": 3,
"pageInfo": {"endCursor": "CUR_B", "hasNextPage": true, "startCursor": "CUR_A", "hasPreviousPage": false},
Expand Down Expand Up @@ -2083,6 +2085,7 @@ func TestGetCommentReplies(t *testing.T) {
"isAnswer": false,
"upvoteCount": 0,
"reactionGroups": [],
"discussion": {"number": 42, "repository": {"owner": {"login": "OWNER"}, "name": "REPO"}},
"replies": {
"totalCount": 5,
"pageInfo": {"endCursor": "CUR_END", "hasNextPage": false, "startCursor": "CUR_Y", "hasPreviousPage": true},
Expand Down Expand Up @@ -2169,6 +2172,7 @@ func TestGetCommentReplies(t *testing.T) {
"isAnswer": false,
"upvoteCount": 0,
"reactionGroups": [],
"discussion": {"number": 42, "repository": {"owner": {"login": "OWNER"}, "name": "REPO"}},
"replies": {
"totalCount": 3,
"pageInfo": {"endCursor": "", "hasNextPage": false, "startCursor": "CUR_START", "hasPreviousPage": true},
Expand Down Expand Up @@ -2255,6 +2259,7 @@ func TestGetCommentReplies(t *testing.T) {
"isAnswer": false,
"upvoteCount": 0,
"reactionGroups": [],
"discussion": {"number": 42, "repository": {"owner": {"login": "OWNER"}, "name": "REPO"}},
"replies": {
"totalCount": 1,
"pageInfo": {"endCursor": "CUR_ONLY", "hasNextPage": false, "startCursor": "CUR_ONLY", "hasPreviousPage": false},
Expand Down Expand Up @@ -2309,6 +2314,7 @@ func TestGetCommentReplies(t *testing.T) {
"isAnswer": false,
"upvoteCount": 0,
"reactionGroups": [],
"discussion": {"number": 42, "repository": {"owner": {"login": "OWNER"}, "name": "REPO"}},
"replies": {
"totalCount": 0,
"pageInfo": {"endCursor": null, "hasNextPage": false, "startCursor": null, "hasPreviousPage": false},
Expand Down Expand Up @@ -2447,6 +2453,63 @@ func TestGetCommentReplies(t *testing.T) {
},
wantErr: "node I_notacomment is not a discussion comment",
},
{
name: "comment belongs to different repo",
commentID: "DC_other",
limit: 10,
newest: false,
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query DiscussionCommentReplies\b`),
httpmock.StringResponse(heredoc.Doc(`
{
"data": {
"repository": {
"hasDiscussionsEnabled": true,
"discussion": {
"id": "D_1",
"number": 1,
"title": "Test",
"body": "",
"url": "",
"closed": false,
"stateReason": "",
"isAnswered": false,
"answerChosenAt": "0001-01-01T00:00:00Z",
"author": {"__typename": "User", "login": "alice"},
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
"answerChosenBy": null,
"labels": {"nodes": []},
"reactionGroups": [],
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"closedAt": "0001-01-01T00:00:00Z",
"locked": false
}
},
"node": {
"id": "DC_other",
"url": "",
"author": {"__typename": "User", "login": "alice"},
"body": "Comment",
"createdAt": "2025-01-01T00:00:00Z",
"isAnswer": false,
"upvoteCount": 0,
"reactionGroups": [],
"discussion": {"number": 99, "repository": {"owner": {"login": "OTHER"}, "name": "DIFFERENT"}},
"replies": {
"totalCount": 0,
"pageInfo": {"endCursor": "", "hasNextPage": false, "startCursor": "", "hasPreviousPage": false},
"nodes": []
}
}
}
}
`)),
)
},
wantErr: "comment DC_other does not belong to OWNER/REPO",
},
}

for _, tt := range tests {
Expand Down
Loading
Loading