Address PR comments
This commit is contained in:
parent
bad5a59427
commit
bec5e0cd77
9 changed files with 253 additions and 501 deletions
65
api/queries_comments.go
Normal file
65
api/queries_comments.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
type Comments struct {
|
||||
Nodes []Comment
|
||||
TotalCount int
|
||||
PageInfo PageInfo
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
Author Author
|
||||
AuthorAssociation string
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
IncludesCreatedEdit bool
|
||||
ReactionGroups ReactionGroups
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
|
||||
func CommentsForIssue(client *Client, repo ghrepo.Interface, issue *Issue) (*Comments, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
Issue struct {
|
||||
Comments Comments `graphql:"comments(first: 100, after: $endCursor)"`
|
||||
} `graphql:"issue(number: $number)"`
|
||||
} `graphql:"repository(owner: $owner, name: $repo)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"repo": githubv4.String(repo.RepoName()),
|
||||
"number": githubv4.Int(issue.Number),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
gql := graphQLClient(client.http, repo.RepoHost())
|
||||
|
||||
var comments []Comment
|
||||
for {
|
||||
var query response
|
||||
err := gql.QueryNamed(context.Background(), "CommentsForIssue", &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comments = append(comments, query.Repository.Issue.Comments.Nodes...)
|
||||
if !query.Repository.Issue.Comments.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Repository.Issue.Comments.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return &Comments{Nodes: comments, TotalCount: len(comments)}, nil
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ type Issue struct {
|
|||
Body string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Comments IssueComments
|
||||
Comments Comments
|
||||
Author Author
|
||||
Assignees struct {
|
||||
Nodes []struct {
|
||||
|
|
@ -68,20 +68,6 @@ type IssuesDisabledError struct {
|
|||
error
|
||||
}
|
||||
|
||||
type IssueComments struct {
|
||||
Nodes []IssueComment
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
type IssueComment struct {
|
||||
Author Author
|
||||
AuthorAssociation string
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
IncludesCreatedEdit bool
|
||||
ReactionGroups ReactionGroups
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
Login string
|
||||
}
|
||||
|
|
@ -335,7 +321,7 @@ loop:
|
|||
return &res, nil
|
||||
}
|
||||
|
||||
func IssueByNumber(client *Client, repo ghrepo.Interface, number, comments int) (*Issue, error) {
|
||||
func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
Issue Issue
|
||||
|
|
@ -344,7 +330,7 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number, comments int)
|
|||
}
|
||||
|
||||
query := `
|
||||
query IssueByNumber($owner: String!, $repo: String!, $issue_number: Int!, $comments: Int!) {
|
||||
query IssueByNumber($owner: String!, $repo: String!, $issue_number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
hasIssuesEnabled
|
||||
issue(number: $issue_number) {
|
||||
|
|
@ -356,7 +342,7 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number, comments int)
|
|||
author {
|
||||
login
|
||||
}
|
||||
comments(last: $comments) {
|
||||
comments(last: 1) {
|
||||
nodes {
|
||||
author {
|
||||
login
|
||||
|
|
@ -417,7 +403,6 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number, comments int)
|
|||
"owner": repo.RepoOwner(),
|
||||
"repo": repo.RepoName(),
|
||||
"issue_number": number,
|
||||
"comments": comments,
|
||||
}
|
||||
|
||||
var resp response
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ReactionGroups []ReactionGroup
|
||||
|
||||
type ReactionGroup struct {
|
||||
|
|
@ -16,28 +11,12 @@ type ReactionGroupUsers struct {
|
|||
TotalCount int
|
||||
}
|
||||
|
||||
func (rg ReactionGroup) String() string {
|
||||
c := rg.Users.TotalCount
|
||||
if c == 0 {
|
||||
return ""
|
||||
}
|
||||
e := reactionEmoji[rg.Content]
|
||||
if e == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%v %s", c, e)
|
||||
func (rg ReactionGroup) Count() int {
|
||||
return rg.Users.TotalCount
|
||||
}
|
||||
|
||||
func (rgs ReactionGroups) String() string {
|
||||
var rs []string
|
||||
|
||||
for _, rg := range rgs {
|
||||
if r := rg.String(); r != "" {
|
||||
rs = append(rs, r)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(rs, " • ")
|
||||
func (rg ReactionGroup) Emoji() string {
|
||||
return reactionEmoji[rg.Content]
|
||||
}
|
||||
|
||||
var reactionEmoji = map[string]string{
|
||||
|
|
|
|||
|
|
@ -8,48 +8,93 @@ import (
|
|||
|
||||
func Test_String(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
rgs ReactionGroups
|
||||
output string
|
||||
rg ReactionGroup
|
||||
emoji string
|
||||
count int
|
||||
}{
|
||||
"empty reaction groups": {
|
||||
rgs: []ReactionGroup{},
|
||||
output: `^$`,
|
||||
"empty reaction group": {
|
||||
rg: ReactionGroup{},
|
||||
emoji: "",
|
||||
count: 0,
|
||||
},
|
||||
"non-empty reaction groups": {
|
||||
rgs: []ReactionGroup{
|
||||
{
|
||||
Content: "LAUGH",
|
||||
Users: ReactionGroupUsers{TotalCount: 0},
|
||||
},
|
||||
{
|
||||
Content: "HOORAY",
|
||||
Users: ReactionGroupUsers{TotalCount: 1},
|
||||
},
|
||||
{
|
||||
Content: "CONFUSED",
|
||||
Users: ReactionGroupUsers{TotalCount: 0},
|
||||
},
|
||||
{
|
||||
Content: "HEART",
|
||||
Users: ReactionGroupUsers{TotalCount: 2},
|
||||
},
|
||||
"unknown reaction group": {
|
||||
rg: ReactionGroup{
|
||||
Content: "UNKNOWN",
|
||||
Users: ReactionGroupUsers{TotalCount: 1},
|
||||
},
|
||||
output: `^1 \x{1f389} • 2 \x{2764}\x{fe0f}$`,
|
||||
emoji: "",
|
||||
count: 1,
|
||||
},
|
||||
"reaction groups with unmapped emoji": {
|
||||
rgs: []ReactionGroup{
|
||||
{
|
||||
Content: "UNKNOWN",
|
||||
Users: ReactionGroupUsers{TotalCount: 1},
|
||||
},
|
||||
"thumbs up reaction group": {
|
||||
rg: ReactionGroup{
|
||||
Content: "THUMBS_UP",
|
||||
Users: ReactionGroupUsers{TotalCount: 2},
|
||||
},
|
||||
output: `^$`,
|
||||
emoji: "\U0001f44d",
|
||||
count: 2,
|
||||
},
|
||||
"thumbs down reaction group": {
|
||||
rg: ReactionGroup{
|
||||
Content: "THUMBS_DOWN",
|
||||
Users: ReactionGroupUsers{TotalCount: 3},
|
||||
},
|
||||
emoji: "\U0001f44e",
|
||||
count: 3,
|
||||
},
|
||||
"laugh reaction group": {
|
||||
rg: ReactionGroup{
|
||||
Content: "LAUGH",
|
||||
Users: ReactionGroupUsers{TotalCount: 4},
|
||||
},
|
||||
emoji: "\U0001f604",
|
||||
count: 4,
|
||||
},
|
||||
"hooray reaction group": {
|
||||
rg: ReactionGroup{
|
||||
Content: "HOORAY",
|
||||
Users: ReactionGroupUsers{TotalCount: 5},
|
||||
},
|
||||
emoji: "\U0001f389",
|
||||
count: 5,
|
||||
},
|
||||
"confused reaction group": {
|
||||
rg: ReactionGroup{
|
||||
Content: "CONFUSED",
|
||||
Users: ReactionGroupUsers{TotalCount: 6},
|
||||
},
|
||||
emoji: "\U0001f615",
|
||||
count: 6,
|
||||
},
|
||||
"heart reaction group": {
|
||||
rg: ReactionGroup{
|
||||
Content: "HEART",
|
||||
Users: ReactionGroupUsers{TotalCount: 7},
|
||||
},
|
||||
emoji: "\u2764\ufe0f",
|
||||
count: 7,
|
||||
},
|
||||
"rocket reaction group": {
|
||||
rg: ReactionGroup{
|
||||
Content: "ROCKET",
|
||||
Users: ReactionGroupUsers{TotalCount: 8},
|
||||
},
|
||||
emoji: "\U0001f680",
|
||||
count: 8,
|
||||
},
|
||||
"eyes reaction group": {
|
||||
rg: ReactionGroup{
|
||||
Content: "EYES",
|
||||
Users: ReactionGroupUsers{TotalCount: 9},
|
||||
},
|
||||
emoji: "\U0001f440",
|
||||
count: 9,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Regexp(t, tt.output, tt.rgs.String())
|
||||
assert.Equal(t, tt.emoji, tt.rg.Emoji())
|
||||
assert.Equal(t, tt.count, tt.rg.Count())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/cli/cli/internal/ghrepo"
|
||||
)
|
||||
|
||||
func IssueWithCommentsFromArg(apiClient *api.Client, baseRepoFn func() (ghrepo.Interface, error), arg string, comments int) (*api.Issue, ghrepo.Interface, error) {
|
||||
func IssueFromArg(apiClient *api.Client, baseRepoFn func() (ghrepo.Interface, error), arg string) (*api.Issue, ghrepo.Interface, error) {
|
||||
issueNumber, baseRepo := issueMetadataFromURL(arg)
|
||||
|
||||
if baseRepo == nil {
|
||||
|
|
@ -30,14 +30,10 @@ func IssueWithCommentsFromArg(apiClient *api.Client, baseRepoFn func() (ghrepo.I
|
|||
}
|
||||
}
|
||||
|
||||
issue, err := issueFromNumber(apiClient, baseRepo, issueNumber, comments)
|
||||
issue, err := issueFromNumber(apiClient, baseRepo, issueNumber)
|
||||
return issue, baseRepo, err
|
||||
}
|
||||
|
||||
func IssueFromArg(apiClient *api.Client, baseRepoFn func() (ghrepo.Interface, error), arg string) (*api.Issue, ghrepo.Interface, error) {
|
||||
return IssueWithCommentsFromArg(apiClient, baseRepoFn, arg, 0)
|
||||
}
|
||||
|
||||
var issueURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/issues/(\d+)`)
|
||||
|
||||
func issueMetadataFromURL(s string) (int, ghrepo.Interface) {
|
||||
|
|
@ -60,6 +56,6 @@ func issueMetadataFromURL(s string) (int, ghrepo.Interface) {
|
|||
return issueNumber, repo
|
||||
}
|
||||
|
||||
func issueFromNumber(apiClient *api.Client, repo ghrepo.Interface, issueNumber, comments int) (*api.Issue, error) {
|
||||
return api.IssueByNumber(apiClient, repo, issueNumber, comments)
|
||||
func issueFromNumber(apiClient *api.Client, repo ghrepo.Interface, issueNumber int) (*api.Issue, error) {
|
||||
return api.IssueByNumber(apiClient, repo, issueNumber)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,81 +1,7 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"hasIssuesEnabled": true,
|
||||
"issue": {
|
||||
"number": 123,
|
||||
"body": "some body",
|
||||
"title": "some title",
|
||||
"state": "OPEN",
|
||||
"createdAt": "2020-01-01T12:00:00Z",
|
||||
"author": {
|
||||
"login": "marseilles"
|
||||
},
|
||||
"assignees": {
|
||||
"nodes": [],
|
||||
"totalCount": 0
|
||||
},
|
||||
"labels": {
|
||||
"nodes": [],
|
||||
"totalCount": 0
|
||||
},
|
||||
"projectcards": {
|
||||
"nodes": [],
|
||||
"totalCount": 0
|
||||
},
|
||||
"milestone": {
|
||||
"title": ""
|
||||
},
|
||||
"reactionGroups": [
|
||||
{
|
||||
"content": "CONFUSED",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "EYES",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HEART",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HOORAY",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "LAUGH",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "ROCKET",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_DOWN",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_UP",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"comments": {
|
||||
"nodes": [
|
||||
{
|
||||
|
|
@ -375,8 +301,7 @@
|
|||
}
|
||||
],
|
||||
"totalCount": 5
|
||||
},
|
||||
"url": "https://github.com/OWNER/REPO/issues/123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,265 +0,0 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"hasIssuesEnabled": true,
|
||||
"issue": {
|
||||
"number": 123,
|
||||
"body": "some body",
|
||||
"title": "some title",
|
||||
"state": "OPEN",
|
||||
"createdAt": "2020-01-01T12:00:00Z",
|
||||
"author": {
|
||||
"login": "marseilles"
|
||||
},
|
||||
"assignees": {
|
||||
"nodes": [],
|
||||
"totalCount": 0
|
||||
},
|
||||
"labels": {
|
||||
"nodes": [],
|
||||
"totalCount": 0
|
||||
},
|
||||
"projectcards": {
|
||||
"nodes": [],
|
||||
"totalCount": 0
|
||||
},
|
||||
"milestone": {
|
||||
"title": ""
|
||||
},
|
||||
"reactionGroups": [
|
||||
{
|
||||
"content": "CONFUSED",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "EYES",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HEART",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HOORAY",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "LAUGH",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "ROCKET",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_DOWN",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_UP",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"comments": {
|
||||
"nodes": [
|
||||
{
|
||||
"author": {
|
||||
"login": "elvisp"
|
||||
},
|
||||
"authorAssociation": "MEMBER",
|
||||
"body": "Comment 3",
|
||||
"createdAt": "2020-01-01T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
"content": "CONFUSED",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "EYES",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HEART",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HOORAY",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "LAUGH",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "ROCKET",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_DOWN",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_UP",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "loislane"
|
||||
},
|
||||
"authorAssociation": "OWNER",
|
||||
"body": "Comment 4",
|
||||
"createdAt": "2020-01-01T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
"content": "CONFUSED",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "EYES",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HEART",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HOORAY",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "LAUGH",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "ROCKET",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_DOWN",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_UP",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "marseilles"
|
||||
},
|
||||
"authorAssociation": "COLLABORATOR",
|
||||
"body": "Comment 5",
|
||||
"createdAt": "2020-01-01T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
"content": "CONFUSED",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "EYES",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HEART",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HOORAY",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "LAUGH",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "ROCKET",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_DOWN",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_UP",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"totalCount": 5
|
||||
},
|
||||
"url": "https://github.com/OWNER/REPO/issues/123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
|
|
@ -27,10 +28,9 @@ type ViewOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
SelectorArg string
|
||||
WebMode bool
|
||||
Comments int
|
||||
CommentsProvided bool
|
||||
SelectorArg string
|
||||
WebMode bool
|
||||
Comments bool
|
||||
}
|
||||
|
||||
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
|
||||
|
|
@ -55,8 +55,6 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
opts.CommentsProvided = cmd.Flags().Changed("comments")
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.SelectorArg = args[0]
|
||||
}
|
||||
|
|
@ -69,8 +67,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open an issue in the browser")
|
||||
cmd.Flags().IntVarP(&opts.Comments, "comments", "c", 1, "View issue comments sorted by most recent")
|
||||
cmd.Flags().Lookup("comments").NoOptDefVal = "30"
|
||||
cmd.Flags().BoolVarP(&opts.Comments, "comments", "c", false, "View issue comments")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -82,20 +79,37 @@ func viewRun(opts *ViewOptions) error {
|
|||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
issue, _, err := issueShared.IssueWithCommentsFromArg(apiClient, opts.BaseRepo, opts.SelectorArg, opts.Comments)
|
||||
issue, repo, err := issueShared.IssueFromArg(apiClient, opts.BaseRepo, opts.SelectorArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
openURL := issue.URL
|
||||
|
||||
if opts.WebMode {
|
||||
openURL := issue.URL
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||
}
|
||||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
if opts.Comments {
|
||||
var s *spinner.Spinner
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
s = utils.Spinner(opts.IO.ErrOut)
|
||||
utils.StartSpinner(s)
|
||||
}
|
||||
|
||||
comments, err := api.CommentsForIssue(apiClient, repo, issue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issue.Comments = *comments
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
utils.StopSpinner(s)
|
||||
}
|
||||
}
|
||||
|
||||
opts.IO.DetectTerminalTheme()
|
||||
|
||||
err = opts.IO.StartPager()
|
||||
|
|
@ -108,7 +122,7 @@ func viewRun(opts *ViewOptions) error {
|
|||
return printHumanIssuePreview(opts.IO, issue)
|
||||
}
|
||||
|
||||
if opts.CommentsProvided {
|
||||
if opts.Comments {
|
||||
return printRawIssueComments(opts.IO.Out, issue)
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +158,7 @@ func printRawIssueComments(out io.Writer, issue *api.Issue) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func rawIssueComment(comment api.IssueComment) string {
|
||||
func rawIssueComment(comment api.Comment) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "author:\t%s\n", comment.Author.Login)
|
||||
fmt.Fprintf(&b, "association:\t%s\n", strings.ToLower(comment.AuthorAssociation))
|
||||
|
|
@ -172,7 +186,7 @@ func printHumanIssuePreview(io *iostreams.IOStreams, issue *api.Issue) error {
|
|||
)
|
||||
|
||||
// Reactions
|
||||
if reactions := issue.ReactionGroups.String(); reactions != "" {
|
||||
if reactions := reactionGroupList(issue.ReactionGroups); reactions != "" {
|
||||
fmt.Fprint(out, reactions)
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
|
|
@ -210,7 +224,7 @@ func printHumanIssuePreview(io *iostreams.IOStreams, issue *api.Issue) error {
|
|||
|
||||
// Comments
|
||||
if issue.Comments.TotalCount > 0 {
|
||||
comments, err := issueComments(io, issue.Comments)
|
||||
comments, err := issueCommentList(io, issue.Comments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -223,20 +237,20 @@ func printHumanIssuePreview(io *iostreams.IOStreams, issue *api.Issue) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func issueComments(io *iostreams.IOStreams, comments api.IssueComments) (string, error) {
|
||||
func issueCommentList(io *iostreams.IOStreams, comments api.Comments) (string, error) {
|
||||
var b strings.Builder
|
||||
cs := io.ColorScheme()
|
||||
retrievedCount := len(comments.Nodes)
|
||||
hiddenCount := comments.TotalCount - retrievedCount
|
||||
|
||||
if hiddenCount > 0 {
|
||||
fmt.Fprint(&b, cs.Gray(fmt.Sprintf("———————— Hiding %v comments ————————", hiddenCount)))
|
||||
fmt.Fprint(&b, cs.Gray(fmt.Sprintf("———————— Not showing %s ————————", utils.Pluralize(hiddenCount, "comment"))))
|
||||
fmt.Fprintf(&b, "\n\n\n")
|
||||
}
|
||||
|
||||
for i, comment := range comments.Nodes {
|
||||
last := i+1 == retrievedCount
|
||||
cmt, err := issueComment(io, comment, last)
|
||||
cmt, err := formatIssueComment(io, comment, last)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -254,7 +268,7 @@ func issueComments(io *iostreams.IOStreams, comments api.IssueComments) (string,
|
|||
return b.String(), nil
|
||||
}
|
||||
|
||||
func issueComment(io *iostreams.IOStreams, comment api.IssueComment, newest bool) (string, error) {
|
||||
func formatIssueComment(io *iostreams.IOStreams, comment api.Comment, newest bool) (string, error) {
|
||||
var b strings.Builder
|
||||
cs := io.ColorScheme()
|
||||
|
||||
|
|
@ -274,7 +288,7 @@ func issueComment(io *iostreams.IOStreams, comment api.IssueComment, newest bool
|
|||
fmt.Fprintln(&b)
|
||||
|
||||
// Reactions
|
||||
if reactions := comment.ReactionGroups.String(); reactions != "" {
|
||||
if reactions := reactionGroupList(comment.ReactionGroups); reactions != "" {
|
||||
fmt.Fprint(&b, reactions)
|
||||
fmt.Fprintln(&b)
|
||||
}
|
||||
|
|
@ -334,3 +348,27 @@ func issueProjectList(issue api.Issue) string {
|
|||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func reactionGroupList(rgs api.ReactionGroups) string {
|
||||
var rs []string
|
||||
|
||||
for _, rg := range rgs {
|
||||
if r := formatReactionGroup(rg); r != "" {
|
||||
rs = append(rs, r)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(rs, " • ")
|
||||
}
|
||||
|
||||
func formatReactionGroup(rg api.ReactionGroup) string {
|
||||
c := rg.Count()
|
||||
if c == 0 {
|
||||
return ""
|
||||
}
|
||||
e := rg.Emoji()
|
||||
if e == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%v %s", c, e)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package view
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
|
|
@ -14,6 +16,7 @@ import (
|
|||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -337,26 +340,31 @@ func TestIssueView_web_urlArg(t *testing.T) {
|
|||
func TestIssueView_tty_Comments(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
cli string
|
||||
fixture string
|
||||
fixtures map[string]string
|
||||
expectedOutputs []string
|
||||
wantsErr bool
|
||||
}{
|
||||
"without comments flag": {
|
||||
cli: "123",
|
||||
fixture: "./fixtures/issueView_previewSingleComment.json",
|
||||
cli: "123",
|
||||
fixtures: map[string]string{
|
||||
"IssueByNumber": "./fixtures/issueView_previewSingleComment.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`some title`,
|
||||
`some body`,
|
||||
`———————— Hiding 4 comments ————————`,
|
||||
`———————— Not showing 4 comments ————————`,
|
||||
`marseilles \(collaborator\) • Jan 1, 2020 • Newest comment`,
|
||||
`Comment 5`,
|
||||
`Use --comments to view the full conversation`,
|
||||
`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
|
||||
},
|
||||
},
|
||||
"with default comments flag": {
|
||||
cli: "123 --comments",
|
||||
fixture: "./fixtures/issueView_previewFullComments.json",
|
||||
"with comments flag": {
|
||||
cli: "123 --comments",
|
||||
fixtures: map[string]string{
|
||||
"IssueByNumber": "./fixtures/issueView_previewSingleComment.json",
|
||||
"CommentsForIssue": "./fixtures/issueView_previewFullComments.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`some title`,
|
||||
`some body`,
|
||||
|
|
@ -374,35 +382,19 @@ func TestIssueView_tty_Comments(t *testing.T) {
|
|||
`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
|
||||
},
|
||||
},
|
||||
"with specified comments flag": {
|
||||
cli: "123 --comments=3",
|
||||
fixture: "./fixtures/issueView_previewThreeComments.json",
|
||||
expectedOutputs: []string{
|
||||
`some title`,
|
||||
`some body`,
|
||||
`———————— Hiding 2 comments ————————`,
|
||||
`elvisp \(member\) • Jan 1, 2020`,
|
||||
`Comment 3`,
|
||||
`loislane \(owner\) • Jan 1, 2020`,
|
||||
`Comment 4`,
|
||||
`marseilles \(collaborator\) • Jan 1, 2020 • Newest comment`,
|
||||
`Comment 5`,
|
||||
`Use --comments to view the full conversation`,
|
||||
`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
|
||||
},
|
||||
},
|
||||
"with incorrect comments flag": {
|
||||
"with invalid comments flag": {
|
||||
cli: "123 --comments 3",
|
||||
fixture: "",
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
stubSpinner()
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
if tc.fixture != "" {
|
||||
http.Register(httpmock.GraphQL(`query IssueByNumber\b`), httpmock.FileResponse(tc.fixture))
|
||||
for name, file := range tc.fixtures {
|
||||
name := fmt.Sprintf(`query %s\b`, name)
|
||||
http.Register(httpmock.GraphQL(name), httpmock.FileResponse(file))
|
||||
}
|
||||
output, err := runCommand(http, true, tc.cli)
|
||||
if tc.wantsErr {
|
||||
|
|
@ -419,13 +411,15 @@ func TestIssueView_tty_Comments(t *testing.T) {
|
|||
func TestIssueView_nontty_Comments(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
cli string
|
||||
fixture string
|
||||
fixtures map[string]string
|
||||
expectedOutputs []string
|
||||
wantsErr bool
|
||||
}{
|
||||
"without comments flag": {
|
||||
cli: "123",
|
||||
fixture: "./fixtures/issueView_previewSingleComment.json",
|
||||
cli: "123",
|
||||
fixtures: map[string]string{
|
||||
"IssueByNumber": "./fixtures/issueView_previewSingleComment.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tsome title`,
|
||||
`state:\tOPEN`,
|
||||
|
|
@ -434,9 +428,12 @@ func TestIssueView_nontty_Comments(t *testing.T) {
|
|||
`some body`,
|
||||
},
|
||||
},
|
||||
"with default comments flag": {
|
||||
cli: "123 --comments",
|
||||
fixture: "./fixtures/issueView_previewFullComments.json",
|
||||
"with comments flag": {
|
||||
cli: "123 --comments",
|
||||
fixtures: map[string]string{
|
||||
"IssueByNumber": "./fixtures/issueView_previewSingleComment.json",
|
||||
"CommentsForIssue": "./fixtures/issueView_previewFullComments.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`author:\tmonalisa`,
|
||||
`association:\t`,
|
||||
|
|
@ -460,27 +457,8 @@ func TestIssueView_nontty_Comments(t *testing.T) {
|
|||
`Comment 5`,
|
||||
},
|
||||
},
|
||||
"with specified comments flag": {
|
||||
cli: "123 --comments=3",
|
||||
fixture: "./fixtures/issueView_previewThreeComments.json",
|
||||
expectedOutputs: []string{
|
||||
`author:\telvisp`,
|
||||
`association:\tmember`,
|
||||
`edited:\tfalse`,
|
||||
`Comment 3`,
|
||||
`author:\tloislane`,
|
||||
`association:\towner`,
|
||||
`edited:\tfalse`,
|
||||
`Comment 4`,
|
||||
`author:\tmarseilles`,
|
||||
`association:\tcollaborator`,
|
||||
`edited:\tfalse`,
|
||||
`Comment 5`,
|
||||
},
|
||||
},
|
||||
"with incorrect comments flag": {
|
||||
"with invalid comments flag": {
|
||||
cli: "123 --comments 3",
|
||||
fixture: "",
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
|
@ -488,8 +466,9 @@ func TestIssueView_nontty_Comments(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
if tc.fixture != "" {
|
||||
http.Register(httpmock.GraphQL(`query IssueByNumber\b`), httpmock.FileResponse(tc.fixture))
|
||||
for name, file := range tc.fixtures {
|
||||
name := fmt.Sprintf(`query %s\b`, name)
|
||||
http.Register(httpmock.GraphQL(name), httpmock.FileResponse(file))
|
||||
}
|
||||
output, err := runCommand(http, false, tc.cli)
|
||||
if tc.wantsErr {
|
||||
|
|
@ -502,3 +481,8 @@ func TestIssueView_nontty_Comments(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stubSpinner() {
|
||||
utils.StartSpinner = func(_ *spinner.Spinner) {}
|
||||
utils.StopSpinner = func(_ *spinner.Spinner) {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue