Merge pull request #13295 from cli/babakks/discussion-view-replies
feat(discussion view): add `--replies COMMENT-ID` experience
This commit is contained in:
commit
99a099928e
5 changed files with 399 additions and 59 deletions
|
|
@ -13,6 +13,7 @@ type DiscussionClient interface {
|
|||
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)
|
||||
ListCategories(repo ghrepo.Interface) ([]DiscussionCategory, error)
|
||||
Create(repo ghrepo.Interface, input CreateDiscussionInput) (*Discussion, error)
|
||||
Update(repo ghrepo.Interface, input UpdateDiscussionInput) (*Discussion, error)
|
||||
|
|
|
|||
|
|
@ -392,6 +392,43 @@ func (c *discussionClient) GetByNumber(repo ghrepo.Interface, number int) (*Disc
|
|||
return &d, nil
|
||||
}
|
||||
|
||||
// discussionReplyNode is the GraphQL response shape for a reply to a discussion comment.
|
||||
type discussionReplyNode struct {
|
||||
ID string
|
||||
URL string `graphql:"url"`
|
||||
Author actorNode
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
IsAnswer bool
|
||||
UpvoteCount int
|
||||
ReactionGroups []struct {
|
||||
Content string
|
||||
Users struct {
|
||||
TotalCount int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mapReplyFromNode converts a discussionReplyNode into the domain DiscussionComment type.
|
||||
func mapReplyFromNode(n discussionReplyNode) DiscussionComment {
|
||||
rc := DiscussionComment{
|
||||
ID: n.ID,
|
||||
URL: n.URL,
|
||||
Author: mapActorFromListNode(n.Author),
|
||||
Body: n.Body,
|
||||
CreatedAt: n.CreatedAt,
|
||||
IsAnswer: n.IsAnswer,
|
||||
UpvoteCount: n.UpvoteCount,
|
||||
}
|
||||
for _, rg := range n.ReactionGroups {
|
||||
rc.ReactionGroups = append(rc.ReactionGroups, ReactionGroup{
|
||||
Content: rg.Content,
|
||||
TotalCount: rg.Users.TotalCount,
|
||||
})
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
// discussionCommentNode is the GraphQL response shape for a discussion comment
|
||||
// including nested replies.
|
||||
type discussionCommentNode struct {
|
||||
|
|
@ -410,21 +447,7 @@ type discussionCommentNode struct {
|
|||
}
|
||||
Replies struct {
|
||||
TotalCount int
|
||||
Nodes []struct {
|
||||
ID string
|
||||
URL string `graphql:"url"`
|
||||
Author actorNode
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
IsAnswer bool
|
||||
UpvoteCount int
|
||||
ReactionGroups []struct {
|
||||
Content string
|
||||
Users struct {
|
||||
TotalCount int
|
||||
}
|
||||
}
|
||||
}
|
||||
Nodes []discussionReplyNode
|
||||
} `graphql:"replies(last: 4)"`
|
||||
}
|
||||
|
||||
|
|
@ -449,22 +472,7 @@ func mapCommentFromNode(n discussionCommentNode) DiscussionComment {
|
|||
|
||||
replyComments := make([]DiscussionComment, len(n.Replies.Nodes))
|
||||
for i, r := range n.Replies.Nodes {
|
||||
rc := DiscussionComment{
|
||||
ID: r.ID,
|
||||
URL: r.URL,
|
||||
Author: mapActorFromListNode(r.Author),
|
||||
Body: r.Body,
|
||||
CreatedAt: r.CreatedAt,
|
||||
IsAnswer: r.IsAnswer,
|
||||
UpvoteCount: r.UpvoteCount,
|
||||
}
|
||||
for _, rg := range r.ReactionGroups {
|
||||
rc.ReactionGroups = append(rc.ReactionGroups, ReactionGroup{
|
||||
Content: rg.Content,
|
||||
TotalCount: rg.Users.TotalCount,
|
||||
})
|
||||
}
|
||||
replyComments[i] = rc
|
||||
replyComments[i] = mapReplyFromNode(r)
|
||||
}
|
||||
dc.Replies = DiscussionCommentList{
|
||||
Comments: replyComments,
|
||||
|
|
@ -574,6 +582,156 @@ func (c *discussionClient) GetWithComments(repo ghrepo.Interface, number int, li
|
|||
return &d, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
HasDiscussionsEnabled bool
|
||||
Discussion struct {
|
||||
discussionListNode
|
||||
} `graphql:"discussion(number: $number)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
Node *struct {
|
||||
DiscussionComment struct {
|
||||
ID string
|
||||
URL string `graphql:"url"`
|
||||
Author actorNode
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
IsAnswer bool
|
||||
UpvoteCount int
|
||||
ReactionGroups []struct {
|
||||
Content string
|
||||
Users struct {
|
||||
TotalCount int
|
||||
}
|
||||
}
|
||||
Replies struct {
|
||||
TotalCount int
|
||||
PageInfo struct {
|
||||
EndCursor string
|
||||
HasNextPage bool
|
||||
StartCursor string
|
||||
HasPreviousPage bool
|
||||
}
|
||||
Nodes []discussionReplyNode
|
||||
} `graphql:"replies(first: $first, last: $last, after: $after, before: $before)"`
|
||||
} `graphql:"... on DiscussionComment"`
|
||||
} `graphql:"node(id: $commentID)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
"number": githubv4.Int(number),
|
||||
"commentID": githubv4.ID(commentID),
|
||||
"first": (*githubv4.Int)(nil),
|
||||
"last": (*githubv4.Int)(nil),
|
||||
"after": (*githubv4.String)(nil),
|
||||
"before": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
if newest {
|
||||
variables["last"] = githubv4.Int(limit)
|
||||
if after != "" {
|
||||
variables["before"] = githubv4.String(after)
|
||||
}
|
||||
} else {
|
||||
variables["first"] = githubv4.Int(limit)
|
||||
if after != "" {
|
||||
variables["after"] = githubv4.String(after)
|
||||
}
|
||||
}
|
||||
|
||||
err := c.gql.Query(repo.RepoHost(), "DiscussionCommentReplies", &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !query.Repository.HasDiscussionsEnabled {
|
||||
return nil, fmt.Errorf("the '%s/%s' repository has discussions disabled", repo.RepoOwner(), repo.RepoName())
|
||||
}
|
||||
|
||||
// The query above should already error for an invalid node ID, but guard against nil.
|
||||
if query.Node == nil {
|
||||
return nil, fmt.Errorf("comment %s not found", commentID)
|
||||
}
|
||||
|
||||
src := query.Node.DiscussionComment
|
||||
if src.ID == "" {
|
||||
return nil, fmt.Errorf("node %s is not a discussion comment", commentID)
|
||||
}
|
||||
|
||||
d := mapDiscussionFromListNode(query.Repository.Discussion.discussionListNode)
|
||||
|
||||
for _, rg := range query.Repository.Discussion.ReactionGroups {
|
||||
d.ReactionGroups = append(d.ReactionGroups, ReactionGroup{
|
||||
Content: rg.Content,
|
||||
TotalCount: rg.Users.TotalCount,
|
||||
})
|
||||
}
|
||||
|
||||
dc := DiscussionComment{
|
||||
ID: src.ID,
|
||||
URL: src.URL,
|
||||
Author: mapActorFromListNode(src.Author),
|
||||
Body: src.Body,
|
||||
CreatedAt: src.CreatedAt,
|
||||
IsAnswer: src.IsAnswer,
|
||||
UpvoteCount: src.UpvoteCount,
|
||||
}
|
||||
|
||||
for _, rg := range src.ReactionGroups {
|
||||
dc.ReactionGroups = append(dc.ReactionGroups, ReactionGroup{
|
||||
Content: rg.Content,
|
||||
TotalCount: rg.Users.TotalCount,
|
||||
})
|
||||
}
|
||||
|
||||
replies := make([]DiscussionComment, len(src.Replies.Nodes))
|
||||
for i, r := range src.Replies.Nodes {
|
||||
replies[i] = mapReplyFromNode(r)
|
||||
}
|
||||
|
||||
// When using "last" (newest order), the API returns items in chronological
|
||||
// order. Reverse them so the newest reply appears first.
|
||||
if newest {
|
||||
slices.Reverse(replies)
|
||||
}
|
||||
|
||||
nextCursor := ""
|
||||
if newest {
|
||||
if src.Replies.PageInfo.HasPreviousPage {
|
||||
nextCursor = src.Replies.PageInfo.StartCursor
|
||||
}
|
||||
} else {
|
||||
if src.Replies.PageInfo.HasNextPage {
|
||||
nextCursor = src.Replies.PageInfo.EndCursor
|
||||
}
|
||||
}
|
||||
|
||||
direction := DiscussionCommentListDirectionForward
|
||||
if newest {
|
||||
direction = DiscussionCommentListDirectionBackward
|
||||
}
|
||||
|
||||
dc.Replies = DiscussionCommentList{
|
||||
Comments: replies,
|
||||
TotalCount: src.Replies.TotalCount,
|
||||
Cursor: after,
|
||||
NextCursor: nextCursor,
|
||||
Direction: direction,
|
||||
}
|
||||
|
||||
d.Comments = DiscussionCommentList{
|
||||
Comments: []DiscussionComment{dc},
|
||||
TotalCount: 1,
|
||||
}
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func (c *discussionClient) ListCategories(repo ghrepo.Interface) ([]DiscussionCategory, error) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ var _ DiscussionClient = &DiscussionClientMock{}
|
|||
// GetByNumberFunc: func(repo ghrepo.Interface, number int) (*Discussion, error) {
|
||||
// panic("mock out the GetByNumber method")
|
||||
// },
|
||||
// GetCommentRepliesFunc: func(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*Discussion, error) {
|
||||
// panic("mock out the GetCommentReplies method")
|
||||
// },
|
||||
// GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*Discussion, error) {
|
||||
// panic("mock out the GetWithComments method")
|
||||
// },
|
||||
|
|
@ -79,6 +82,9 @@ type DiscussionClientMock struct {
|
|||
// GetByNumberFunc mocks the GetByNumber method.
|
||||
GetByNumberFunc func(repo ghrepo.Interface, number int) (*Discussion, error)
|
||||
|
||||
// GetCommentRepliesFunc mocks the GetCommentReplies method.
|
||||
GetCommentRepliesFunc func(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*Discussion, error)
|
||||
|
||||
// GetWithCommentsFunc mocks the GetWithComments method.
|
||||
GetWithCommentsFunc func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*Discussion, error)
|
||||
|
||||
|
|
@ -145,6 +151,21 @@ type DiscussionClientMock struct {
|
|||
// Number is the number argument value.
|
||||
Number int
|
||||
}
|
||||
// GetCommentReplies holds details about calls to the GetCommentReplies method.
|
||||
GetCommentReplies []struct {
|
||||
// Repo is the repo argument value.
|
||||
Repo ghrepo.Interface
|
||||
// Number is the number argument value.
|
||||
Number int
|
||||
// CommentID is the commentID argument value.
|
||||
CommentID string
|
||||
// Limit is the limit argument value.
|
||||
Limit int
|
||||
// After is the after argument value.
|
||||
After string
|
||||
// Newest is the newest argument value.
|
||||
Newest bool
|
||||
}
|
||||
// GetWithComments holds details about calls to the GetWithComments method.
|
||||
GetWithComments []struct {
|
||||
// Repo is the repo argument value.
|
||||
|
|
@ -230,20 +251,21 @@ type DiscussionClientMock struct {
|
|||
Input UpdateDiscussionInput
|
||||
}
|
||||
}
|
||||
lockAddComment sync.RWMutex
|
||||
lockClose sync.RWMutex
|
||||
lockCreate sync.RWMutex
|
||||
lockGetByNumber sync.RWMutex
|
||||
lockGetWithComments sync.RWMutex
|
||||
lockList sync.RWMutex
|
||||
lockListCategories sync.RWMutex
|
||||
lockLock sync.RWMutex
|
||||
lockMarkAnswer sync.RWMutex
|
||||
lockReopen sync.RWMutex
|
||||
lockSearch sync.RWMutex
|
||||
lockUnlock sync.RWMutex
|
||||
lockUnmarkAnswer sync.RWMutex
|
||||
lockUpdate sync.RWMutex
|
||||
lockAddComment sync.RWMutex
|
||||
lockClose sync.RWMutex
|
||||
lockCreate sync.RWMutex
|
||||
lockGetByNumber sync.RWMutex
|
||||
lockGetCommentReplies sync.RWMutex
|
||||
lockGetWithComments sync.RWMutex
|
||||
lockList sync.RWMutex
|
||||
lockListCategories sync.RWMutex
|
||||
lockLock sync.RWMutex
|
||||
lockMarkAnswer sync.RWMutex
|
||||
lockReopen sync.RWMutex
|
||||
lockSearch sync.RWMutex
|
||||
lockUnlock sync.RWMutex
|
||||
lockUnmarkAnswer sync.RWMutex
|
||||
lockUpdate sync.RWMutex
|
||||
}
|
||||
|
||||
// AddComment calls AddCommentFunc.
|
||||
|
|
@ -402,6 +424,58 @@ func (mock *DiscussionClientMock) GetByNumberCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// GetCommentReplies calls GetCommentRepliesFunc.
|
||||
func (mock *DiscussionClientMock) GetCommentReplies(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*Discussion, error) {
|
||||
if mock.GetCommentRepliesFunc == nil {
|
||||
panic("DiscussionClientMock.GetCommentRepliesFunc: method is nil but DiscussionClient.GetCommentReplies was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Repo ghrepo.Interface
|
||||
Number int
|
||||
CommentID string
|
||||
Limit int
|
||||
After string
|
||||
Newest bool
|
||||
}{
|
||||
Repo: repo,
|
||||
Number: number,
|
||||
CommentID: commentID,
|
||||
Limit: limit,
|
||||
After: after,
|
||||
Newest: newest,
|
||||
}
|
||||
mock.lockGetCommentReplies.Lock()
|
||||
mock.calls.GetCommentReplies = append(mock.calls.GetCommentReplies, callInfo)
|
||||
mock.lockGetCommentReplies.Unlock()
|
||||
return mock.GetCommentRepliesFunc(repo, number, commentID, limit, after, newest)
|
||||
}
|
||||
|
||||
// GetCommentRepliesCalls gets all the calls that were made to GetCommentReplies.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedDiscussionClient.GetCommentRepliesCalls())
|
||||
func (mock *DiscussionClientMock) GetCommentRepliesCalls() []struct {
|
||||
Repo ghrepo.Interface
|
||||
Number int
|
||||
CommentID string
|
||||
Limit int
|
||||
After string
|
||||
Newest bool
|
||||
} {
|
||||
var calls []struct {
|
||||
Repo ghrepo.Interface
|
||||
Number int
|
||||
CommentID string
|
||||
Limit int
|
||||
After string
|
||||
Newest bool
|
||||
}
|
||||
mock.lockGetCommentReplies.RLock()
|
||||
calls = mock.calls.GetCommentReplies
|
||||
mock.lockGetCommentReplies.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetWithComments calls GetWithCommentsFunc.
|
||||
func (mock *DiscussionClientMock) GetWithComments(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*Discussion, error) {
|
||||
if mock.GetWithCommentsFunc == nil {
|
||||
|
|
|
|||
|
|
@ -185,8 +185,37 @@ type DiscussionComment struct {
|
|||
func (c DiscussionComment) Export() map[string]interface{} {
|
||||
replies := make([]interface{}, len(c.Replies.Comments))
|
||||
for i, r := range c.Replies.Comments {
|
||||
replies[i] = r.Export()
|
||||
replies[i] = r.ExportReply()
|
||||
}
|
||||
reactions := make([]interface{}, len(c.ReactionGroups))
|
||||
for i, rg := range c.ReactionGroups {
|
||||
reactions[i] = rg.Export()
|
||||
}
|
||||
repliesMap := map[string]interface{}{
|
||||
"totalCount": c.Replies.TotalCount,
|
||||
"nodes": replies,
|
||||
}
|
||||
if c.Replies.Cursor != "" {
|
||||
repliesMap["cursor"] = c.Replies.Cursor
|
||||
}
|
||||
if c.Replies.NextCursor != "" {
|
||||
repliesMap["next"] = c.Replies.NextCursor
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": c.ID,
|
||||
"url": c.URL,
|
||||
"author": c.Author.Export(),
|
||||
"body": c.Body,
|
||||
"createdAt": c.CreatedAt,
|
||||
"isAnswer": c.IsAnswer,
|
||||
"upvoteCount": c.UpvoteCount,
|
||||
"reactionGroups": reactions,
|
||||
"replies": repliesMap,
|
||||
}
|
||||
}
|
||||
|
||||
// ExportReply returns a reply as a map for JSON output, without nested replies.
|
||||
func (c DiscussionComment) ExportReply() map[string]interface{} {
|
||||
reactions := make([]interface{}, len(c.ReactionGroups))
|
||||
for i, rg := range c.ReactionGroups {
|
||||
reactions[i] = rg.Export()
|
||||
|
|
@ -200,10 +229,6 @@ func (c DiscussionComment) Export() map[string]interface{} {
|
|||
"isAnswer": c.IsAnswer,
|
||||
"upvoteCount": c.UpvoteCount,
|
||||
"reactionGroups": reactions,
|
||||
"replies": map[string]interface{}{
|
||||
"totalCount": c.Replies.TotalCount,
|
||||
"nodes": replies,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ type ViewOptions struct {
|
|||
DiscussionNumber int
|
||||
WebMode bool
|
||||
Comments bool
|
||||
Replies string
|
||||
Limit int
|
||||
After string
|
||||
Order string
|
||||
|
|
@ -103,6 +104,10 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
Use %[1]s--order%[1]s to control comment ordering (oldest or newest first).
|
||||
Use %[1]s--limit%[1]s and %[1]s--after%[1]s for paginating through comments.
|
||||
|
||||
With %[1]s--replies%[1]s flag, show paginated replies on a specific comment.
|
||||
Pass the comment node ID (e.g. %[1]sDC_abc123%[1]s) to fetch its replies.
|
||||
Use %[1]s--limit%[1]s, %[1]s--after%[1]s, and %[1]s--order%[1]s to control reply pagination.
|
||||
|
||||
With %[1]s--web%[1]s flag, open the discussion in a web browser instead.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
|
|
@ -124,20 +129,34 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
# Fetch the next page of comments
|
||||
$ gh discussion view 123 --comments --after CURSOR
|
||||
|
||||
# View replies on a specific comment
|
||||
$ gh discussion view 123 --replies COMMENT-ID
|
||||
|
||||
# Paginate through replies
|
||||
$ gh discussion view 123 --replies COMMENT-ID --limit 10 --after CURSOR
|
||||
|
||||
# Open in browser
|
||||
$ gh discussion view 123 --web
|
||||
`),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of --comments, --replies, or --web",
|
||||
opts.Comments, opts.Replies != "", opts.WebMode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repliesMode := opts.Replies != ""
|
||||
commentsMode := needsComments(opts)
|
||||
if cmd.Flags().Changed("order") && !commentsMode {
|
||||
return cmdutil.FlagErrorf("--order requires --comments")
|
||||
|
||||
paginatedMode := commentsMode || repliesMode
|
||||
if cmd.Flags().Changed("order") && !paginatedMode {
|
||||
return cmdutil.FlagErrorf("--order requires --comments or --replies")
|
||||
}
|
||||
if cmd.Flags().Changed("limit") && !commentsMode {
|
||||
return cmdutil.FlagErrorf("--limit requires --comments")
|
||||
if cmd.Flags().Changed("limit") && !paginatedMode {
|
||||
return cmdutil.FlagErrorf("--limit requires --comments or --replies")
|
||||
}
|
||||
if cmd.Flags().Changed("after") && !commentsMode {
|
||||
return cmdutil.FlagErrorf("--after requires --comments")
|
||||
if cmd.Flags().Changed("after") && !paginatedMode {
|
||||
return cmdutil.FlagErrorf("--after requires --comments or --replies")
|
||||
}
|
||||
if opts.Limit < 1 {
|
||||
return cmdutil.FlagErrorf("invalid limit: %d", opts.Limit)
|
||||
|
|
@ -168,9 +187,10 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
|
||||
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open a discussion in the browser")
|
||||
cmd.Flags().BoolVarP(&opts.Comments, "comments", "c", false, "View discussion comments")
|
||||
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum number of comments to fetch")
|
||||
cmd.Flags().StringVar(&opts.After, "after", "", "Cursor for the next page of comments")
|
||||
cmdutil.StringEnumFlag(cmd, &opts.Order, "order", "", "newest", []string{"oldest", "newest"}, "Order of comments")
|
||||
cmd.Flags().StringVar(&opts.Replies, "replies", "", "View replies on a specific comment by its node ID")
|
||||
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum number of comments or replies to fetch")
|
||||
cmd.Flags().StringVar(&opts.After, "after", "", "Cursor for the next page")
|
||||
cmdutil.StringEnumFlag(cmd, &opts.Order, "order", "", "newest", []string{"oldest", "newest"}, "Order of comments or replies")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, discussionFields)
|
||||
|
||||
return cmd
|
||||
|
|
@ -209,6 +229,32 @@ func viewRun(opts *ViewOptions) error {
|
|||
opts.IO.DetectTerminalTheme()
|
||||
opts.IO.StartProgressIndicator()
|
||||
|
||||
if opts.Replies != "" {
|
||||
discussion, err := c.GetCommentReplies(repo, opts.DiscussionNumber, opts.Replies, opts.Limit, opts.After, opts.Order == "newest")
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Exporter != nil {
|
||||
return opts.Exporter.Write(opts.IO, discussion)
|
||||
}
|
||||
|
||||
if err := opts.IO.StartPager(); err != nil {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err)
|
||||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if len(discussion.Comments.Comments) == 0 {
|
||||
return fmt.Errorf("no comment found for reply ID %s", opts.Replies)
|
||||
}
|
||||
comment := discussion.Comments.Comments[0]
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
return printHumanReplies(opts, &comment)
|
||||
}
|
||||
return printRawReplies(opts.IO.Out, &comment)
|
||||
}
|
||||
|
||||
var discussion *client.Discussion
|
||||
if needsComments(opts) {
|
||||
discussion, err = c.GetWithComments(repo, opts.DiscussionNumber, opts.Limit, opts.After, opts.Order == "newest")
|
||||
|
|
@ -448,3 +494,39 @@ func labelList(labels []client.DiscussionLabel, cs *iostreams.ColorScheme) strin
|
|||
}
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
func printHumanReplies(opts *ViewOptions, c *client.DiscussionComment) error {
|
||||
out := opts.IO.Out
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
if err := printHumanComment(opts, out, *c, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Replies.NextCursor != "" {
|
||||
fmt.Fprintf(out, cs.Muted("To see more replies, pass: --after %s\n"), c.Replies.NextCursor)
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printRawReplies(out io.Writer, c *client.DiscussionComment) error {
|
||||
answer := ""
|
||||
if c.IsAnswer {
|
||||
answer = "\tanswer"
|
||||
}
|
||||
fmt.Fprintf(out, "comment:\t%s\t%s\t%s%s\n", c.Author.Login, c.CreatedAt.Format(time.RFC3339), c.URL, answer)
|
||||
fmt.Fprintf(out, "replies:\t%d\n", c.Replies.TotalCount)
|
||||
if c.Replies.NextCursor != "" {
|
||||
fmt.Fprintf(out, "next:\t%s\n", c.Replies.NextCursor)
|
||||
}
|
||||
fmt.Fprintln(out, "--")
|
||||
fmt.Fprintln(out, c.Body)
|
||||
|
||||
for _, reply := range c.Replies.Comments {
|
||||
printRawComment(out, reply, " ")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue