Add --json export flag for issues and pull requests
The `--json` flag accepts a list of GraphQL fields to query for and output in JSON format. To get the list of available flags, run the command with a blank value for `--json`. Additional `--jq` and `--template` flags are available just like in `gh api`.
This commit is contained in:
parent
19ea49b5a9
commit
abe452bb19
16 changed files with 521 additions and 118 deletions
89
api/export_pr.go
Normal file
89
api/export_pr.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
|
||||
v := reflect.ValueOf(issue).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
||||
for _, f := range fields {
|
||||
switch f {
|
||||
case "milestone":
|
||||
if issue.Milestone.Title != "" {
|
||||
data[f] = &issue.Milestone
|
||||
} else {
|
||||
data[f] = nil
|
||||
}
|
||||
case "comments":
|
||||
data[f] = issue.Comments.Nodes
|
||||
case "assignees":
|
||||
data[f] = issue.Assignees.Nodes
|
||||
case "labels":
|
||||
data[f] = issue.Labels.Nodes
|
||||
case "projectCards":
|
||||
data[f] = issue.ProjectCards.Nodes
|
||||
default:
|
||||
sf := fieldByName(v, f)
|
||||
data[f] = sf.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return &data
|
||||
}
|
||||
|
||||
func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
|
||||
v := reflect.ValueOf(pr).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
||||
for _, f := range fields {
|
||||
switch f {
|
||||
case "headRepository":
|
||||
data[f] = map[string]string{"name": pr.HeadRepository.Name}
|
||||
case "milestone":
|
||||
if pr.Milestone.Title != "" {
|
||||
data[f] = &pr.Milestone
|
||||
} else {
|
||||
data[f] = nil
|
||||
}
|
||||
case "statusCheckRollup":
|
||||
if n := pr.Commits.Nodes; len(n) > 0 {
|
||||
data[f] = n[0].Commit.StatusCheckRollup.Contexts.Nodes
|
||||
} else {
|
||||
data[f] = nil
|
||||
}
|
||||
case "comments":
|
||||
data[f] = pr.Comments.Nodes
|
||||
case "assignees":
|
||||
data[f] = pr.Assignees.Nodes
|
||||
case "labels":
|
||||
data[f] = pr.Labels.Nodes
|
||||
case "projectCards":
|
||||
data[f] = pr.ProjectCards.Nodes
|
||||
case "reviews":
|
||||
data[f] = pr.Reviews.Nodes
|
||||
case "reviewRequests":
|
||||
requests := make([]interface{}, 0, len(pr.ReviewRequests.Nodes))
|
||||
for _, req := range pr.ReviewRequests.Nodes {
|
||||
if req.RequestedReviewer.TypeName == "" {
|
||||
continue
|
||||
}
|
||||
requests = append(requests, req.RequestedReviewer)
|
||||
}
|
||||
data[f] = &requests
|
||||
default:
|
||||
sf := fieldByName(v, f)
|
||||
data[f] = sf.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return &data
|
||||
}
|
||||
|
||||
func fieldByName(v reflect.Value, field string) reflect.Value {
|
||||
return v.FieldByNameFunc(func(s string) bool {
|
||||
return strings.EqualFold(field, s)
|
||||
})
|
||||
}
|
||||
|
|
@ -16,14 +16,14 @@ type Comments struct {
|
|||
}
|
||||
|
||||
type Comment struct {
|
||||
Author Author
|
||||
AuthorAssociation string
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
IncludesCreatedEdit bool
|
||||
IsMinimized bool
|
||||
MinimizedReason string
|
||||
ReactionGroups ReactionGroups
|
||||
Author Author `json:"author"`
|
||||
AuthorAssociation string `json:"authorAssociation"`
|
||||
Body string `json:"body"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
IncludesCreatedEdit bool `json:"includesCreatedEdit"`
|
||||
IsMinimized bool `json:"isMinimized"`
|
||||
MinimizedReason string `json:"minimizedReason"`
|
||||
ReactionGroups ReactionGroups `json:"reactionGroups"`
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type Issue struct {
|
|||
Body string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ClosedAt *time.Time
|
||||
Comments Comments
|
||||
Author Author
|
||||
Assignees Assignees
|
||||
|
|
@ -41,7 +42,7 @@ type Issue struct {
|
|||
|
||||
type Assignees struct {
|
||||
Nodes []struct {
|
||||
Login string
|
||||
Login string `json:"login"`
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
|
|
@ -56,7 +57,7 @@ func (a Assignees) Logins() []string {
|
|||
|
||||
type Labels struct {
|
||||
Nodes []struct {
|
||||
Name string
|
||||
Name string `json:"name"`
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
|
|
@ -72,11 +73,11 @@ func (l Labels) Names() []string {
|
|||
type ProjectCards struct {
|
||||
Nodes []struct {
|
||||
Project struct {
|
||||
Name string
|
||||
}
|
||||
Name string `json:"name"`
|
||||
} `json:"project"`
|
||||
Column struct {
|
||||
Name string
|
||||
}
|
||||
Name string `json:"name"`
|
||||
} `json:"column"`
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
|
|
@ -90,7 +91,7 @@ func (p ProjectCards) ProjectNames() []string {
|
|||
}
|
||||
|
||||
type Milestone struct {
|
||||
Title string
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type IssuesDisabledError struct {
|
||||
|
|
@ -98,7 +99,7 @@ type IssuesDisabledError struct {
|
|||
}
|
||||
|
||||
type Author struct {
|
||||
Login string
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
const fragments = `
|
||||
|
|
|
|||
|
|
@ -42,12 +42,14 @@ type PullRequest struct {
|
|||
Additions int
|
||||
Deletions int
|
||||
MergeStateStatus string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ClosedAt *time.Time
|
||||
MergedAt *time.Time
|
||||
|
||||
Author struct {
|
||||
Login string
|
||||
}
|
||||
Author Author
|
||||
HeadRepositoryOwner struct {
|
||||
Login string
|
||||
Login string `json:"login"`
|
||||
}
|
||||
HeadRepository struct {
|
||||
Name string
|
||||
|
|
@ -75,15 +77,16 @@ type PullRequest struct {
|
|||
StatusCheckRollup struct {
|
||||
Contexts struct {
|
||||
Nodes []struct {
|
||||
Name string
|
||||
Context string
|
||||
State string
|
||||
Status string
|
||||
Conclusion string
|
||||
StartedAt time.Time
|
||||
CompletedAt time.Time
|
||||
DetailsURL string
|
||||
TargetURL string
|
||||
TypeName string `json:"__typename"`
|
||||
Name string `json:"name"`
|
||||
Context string `json:"context,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
CompletedAt time.Time `json:"completedAt"`
|
||||
DetailsURL string `json:"detailsUrl"`
|
||||
TargetURL string `json:"targetUrl,omitempty"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,8 +107,8 @@ type ReviewRequests struct {
|
|||
Nodes []struct {
|
||||
RequestedReviewer struct {
|
||||
TypeName string `json:"__typename"`
|
||||
Login string
|
||||
Name string
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
}
|
||||
TotalCount int
|
||||
|
|
@ -972,10 +975,3 @@ func BranchDeleteRemote(client *Client, repo ghrepo.Interface, branch string) er
|
|||
path := fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.RepoOwner(), repo.RepoName(), branch)
|
||||
return client.REST(repo.RepoHost(), "DELETE", path, nil, nil)
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,14 +28,14 @@ type PullRequestReviews struct {
|
|||
}
|
||||
|
||||
type PullRequestReview struct {
|
||||
Author Author
|
||||
AuthorAssociation string
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
IncludesCreatedEdit bool
|
||||
ReactionGroups ReactionGroups
|
||||
State string
|
||||
URL string
|
||||
Author Author `json:"author"`
|
||||
AuthorAssociation string `json:"authorAssociation"`
|
||||
Body string `json:"body"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
IncludesCreatedEdit bool `json:"includesCreatedEdit"`
|
||||
ReactionGroups ReactionGroups `json:"reactionGroups"`
|
||||
State string `json:"state"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func AddReview(client *Client, repo ghrepo.Interface, pr *PullRequest, input *PullRequestReviewInput) error {
|
||||
|
|
|
|||
149
api/query_builder.go
Normal file
149
api/query_builder.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func squeeze(r rune) rune {
|
||||
switch r {
|
||||
case '\n', '\t':
|
||||
return -1
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
func shortenQuery(q string) string {
|
||||
return strings.Map(squeeze, q)
|
||||
}
|
||||
|
||||
var issueComments = shortenQuery(`
|
||||
comments(last: 100) {
|
||||
nodes {
|
||||
author{login},
|
||||
authorAssociation,
|
||||
body,
|
||||
createdAt,
|
||||
includesCreatedEdit,
|
||||
isMinimized,
|
||||
minimizedReason,
|
||||
reactionGroups{content,users{totalCount}}
|
||||
},
|
||||
totalCount
|
||||
}
|
||||
`)
|
||||
|
||||
var prReviewRequests = shortenQuery(`
|
||||
reviewRequests(last: 100) {
|
||||
nodes {
|
||||
requestedReviewer {
|
||||
__typename,
|
||||
...on User{login},
|
||||
...on Team{name}
|
||||
}
|
||||
},
|
||||
totalCount
|
||||
}
|
||||
`)
|
||||
|
||||
var prStatusCheckRollup = shortenQuery(`
|
||||
commits(last: 1) {
|
||||
totalCount,
|
||||
nodes {
|
||||
commit {
|
||||
oid,
|
||||
statusCheckRollup {
|
||||
contexts(last: 100) {
|
||||
nodes {
|
||||
__typename
|
||||
...on StatusContext {
|
||||
context,
|
||||
state,
|
||||
targetUrl
|
||||
},
|
||||
...on CheckRun {
|
||||
name,
|
||||
status,
|
||||
conclusion,
|
||||
startedAt,
|
||||
completedAt,
|
||||
detailsUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
var IssueFields = []string{
|
||||
"assignees",
|
||||
"author",
|
||||
"body",
|
||||
"closed",
|
||||
"comments",
|
||||
"createdAt",
|
||||
"closedAt",
|
||||
"id",
|
||||
"labels",
|
||||
"milestone",
|
||||
"number",
|
||||
"projectCards",
|
||||
"reactionGroups",
|
||||
"state",
|
||||
"title",
|
||||
"updatedAt",
|
||||
"url",
|
||||
}
|
||||
|
||||
var PullRequestFields = append(IssueFields,
|
||||
"additions",
|
||||
"baseRefName",
|
||||
"deletions",
|
||||
"headRefName",
|
||||
"headRepository",
|
||||
"headRepositoryOwner",
|
||||
"isCrossRepository",
|
||||
"isDraft",
|
||||
"maintainerCanModify",
|
||||
"mergeable",
|
||||
"mergedAt",
|
||||
"mergeStateStatus",
|
||||
"reviewDecision",
|
||||
"reviewRequests",
|
||||
"statusCheckRollup",
|
||||
)
|
||||
|
||||
func PullRequestGraphQL(fields []string) string {
|
||||
var q []string
|
||||
for _, field := range fields {
|
||||
switch field {
|
||||
case "author":
|
||||
q = append(q, `author{login}`)
|
||||
case "headRepositoryOwner":
|
||||
q = append(q, `headRepositoryOwner{login}`)
|
||||
case "headRepository":
|
||||
q = append(q, `headRepository{name}`)
|
||||
case "assignees":
|
||||
q = append(q, `assignees(first:100){nodes{login},totalCount}`)
|
||||
case "labels":
|
||||
q = append(q, `labels(first:100){nodes{name},totalCount}`)
|
||||
case "projectCards":
|
||||
q = append(q, `projectCards(first:100){nodes{project{name}column{name}},totalCount}`)
|
||||
case "milestone":
|
||||
q = append(q, `milestone{title}`)
|
||||
case "reactionGroups":
|
||||
q = append(q, `reactionGroups{content,users{totalCount}}`)
|
||||
case "comments":
|
||||
q = append(q, issueComments)
|
||||
case "reviewRequests":
|
||||
q = append(q, prReviewRequests)
|
||||
case "statusCheckRollup":
|
||||
q = append(q, prStatusCheckRollup)
|
||||
default:
|
||||
q = append(q, field)
|
||||
}
|
||||
}
|
||||
return strings.Join(q, ",")
|
||||
}
|
||||
|
|
@ -1,14 +1,42 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type ReactionGroups []ReactionGroup
|
||||
|
||||
func (rg ReactionGroups) MarshalJSON() ([]byte, error) {
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteRune('[')
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
|
||||
hasPrev := false
|
||||
for _, g := range rg {
|
||||
if g.Users.TotalCount == 0 {
|
||||
continue
|
||||
}
|
||||
if hasPrev {
|
||||
buf.WriteRune(',')
|
||||
}
|
||||
if err := encoder.Encode(&g); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasPrev = true
|
||||
}
|
||||
buf.WriteRune(']')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type ReactionGroup struct {
|
||||
Content string
|
||||
Users ReactionGroupUsers
|
||||
Content string `json:"content"`
|
||||
Users ReactionGroupUsers `json:"users"`
|
||||
}
|
||||
|
||||
type ReactionGroupUsers struct {
|
||||
TotalCount int
|
||||
TotalCount int `json:"totalCount"`
|
||||
}
|
||||
|
||||
func (rg ReactionGroup) Count() int {
|
||||
|
|
|
|||
|
|
@ -8,27 +8,12 @@ import (
|
|||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
)
|
||||
|
||||
const fragments = `
|
||||
fragment issue on Issue {
|
||||
number
|
||||
title
|
||||
url
|
||||
state
|
||||
updatedAt
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func IssueList(client *api.Client, repo ghrepo.Interface, state string, assigneeString string, limit int, authorString string, mentionString string, milestoneString string) (*api.IssuesAndTotalCount, error) {
|
||||
func listIssues(client *api.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) {
|
||||
var states []string
|
||||
switch state {
|
||||
switch filters.State {
|
||||
case "open", "":
|
||||
states = []string{"OPEN"}
|
||||
case "closed":
|
||||
|
|
@ -36,9 +21,10 @@ func IssueList(client *api.Client, repo ghrepo.Interface, state string, assignee
|
|||
case "all":
|
||||
states = []string{"OPEN", "CLOSED"}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid state: %s", state)
|
||||
return nil, fmt.Errorf("invalid state: %s", filters.State)
|
||||
}
|
||||
|
||||
fragments := fmt.Sprintf("fragment issue on Issue {%s}", api.PullRequestGraphQL(filters.Fields))
|
||||
query := fragments + `
|
||||
query IssueList($owner: String!, $repo: String!, $limit: Int, $endCursor: String, $states: [IssueState!] = OPEN, $assignee: String, $author: String, $mention: String, $milestone: String) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
|
|
@ -62,25 +48,25 @@ func IssueList(client *api.Client, repo ghrepo.Interface, state string, assignee
|
|||
"repo": repo.RepoName(),
|
||||
"states": states,
|
||||
}
|
||||
if assigneeString != "" {
|
||||
variables["assignee"] = assigneeString
|
||||
if filters.Assignee != "" {
|
||||
variables["assignee"] = filters.Assignee
|
||||
}
|
||||
if authorString != "" {
|
||||
variables["author"] = authorString
|
||||
if filters.Author != "" {
|
||||
variables["author"] = filters.Author
|
||||
}
|
||||
if mentionString != "" {
|
||||
variables["mention"] = mentionString
|
||||
if filters.Mention != "" {
|
||||
variables["mention"] = filters.Mention
|
||||
}
|
||||
|
||||
if milestoneString != "" {
|
||||
if filters.Milestone != "" {
|
||||
var milestone *api.RepoMilestone
|
||||
if milestoneNumber, err := strconv.ParseInt(milestoneString, 10, 32); err == nil {
|
||||
if milestoneNumber, err := strconv.ParseInt(filters.Milestone, 10, 32); err == nil {
|
||||
milestone, err = api.MilestoneByNumber(client, repo, int32(milestoneNumber))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
milestone, err = api.MilestoneByTitle(client, repo, "all", milestoneString)
|
||||
milestone, err = api.MilestoneByTitle(client, repo, "all", filters.Milestone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -143,7 +129,8 @@ loop:
|
|||
return &res, nil
|
||||
}
|
||||
|
||||
func IssueSearch(client *api.Client, repo ghrepo.Interface, searchQuery string, limit int) (*api.IssuesAndTotalCount, error) {
|
||||
func searchIssues(client *api.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) {
|
||||
fragments := fmt.Sprintf("fragment issue on Issue {%s}", api.PullRequestGraphQL(filters.Fields))
|
||||
query := fragments +
|
||||
`query IssueSearch($repo: String!, $owner: String!, $type: SearchType!, $limit: Int, $after: String, $query: String!) {
|
||||
repository(name: $repo, owner: $owner) {
|
||||
|
|
@ -174,7 +161,7 @@ func IssueSearch(client *api.Client, repo ghrepo.Interface, searchQuery string,
|
|||
}
|
||||
|
||||
perPage := min(limit, 100)
|
||||
searchQuery = fmt.Sprintf("repo:%s/%s %s", repo.RepoOwner(), repo.RepoName(), searchQuery)
|
||||
searchQuery := fmt.Sprintf("repo:%s/%s %s", repo.RepoOwner(), repo.RepoName(), prShared.SearchQueryBuild(filters))
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
)
|
||||
|
||||
|
|
@ -48,7 +49,11 @@ func TestIssueList(t *testing.T) {
|
|||
)
|
||||
|
||||
repo, _ := ghrepo.FromFullName("OWNER/REPO")
|
||||
_, err := IssueList(client, repo, "open", "", 251, "", "", "")
|
||||
filters := prShared.FilterOptions{
|
||||
Entity: "issue",
|
||||
State: "open",
|
||||
}
|
||||
_, err := listIssues(client, repo, filters, 251)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -128,7 +133,7 @@ func TestIssueList_pagination(t *testing.T) {
|
|||
)
|
||||
|
||||
repo := ghrepo.New("OWNER", "REPO")
|
||||
res, err := IssueList(client, repo, "", "", 0, "", "", "")
|
||||
res, err := listIssues(client, repo, prShared.FilterOptions{}, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("IssueList() error = %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ type ListOptions struct {
|
|||
Browser browser
|
||||
|
||||
WebMode bool
|
||||
Export *cmdutil.ExportFormat
|
||||
|
||||
Assignee string
|
||||
Labels []string
|
||||
|
|
@ -86,10 +87,20 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
cmd.Flags().StringVar(&opts.Mention, "mention", "", "Filter by mention")
|
||||
cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Filter by milestone `number` or `title`")
|
||||
cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search issues with `query`")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Export, api.IssueFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
var defaultFields = []string{
|
||||
"number",
|
||||
"title",
|
||||
"url",
|
||||
"state",
|
||||
"updatedAt",
|
||||
"labels",
|
||||
}
|
||||
|
||||
func listRun(opts *ListOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
|
|
@ -110,6 +121,7 @@ func listRun(opts *ListOptions) error {
|
|||
Mention: opts.Mention,
|
||||
Milestone: opts.Milestone,
|
||||
Search: opts.Search,
|
||||
Fields: defaultFields,
|
||||
}
|
||||
|
||||
isTerminal := opts.IO.IsStdoutTTY()
|
||||
|
|
@ -127,6 +139,10 @@ func listRun(opts *ListOptions) error {
|
|||
return opts.Browser.Browse(openURL)
|
||||
}
|
||||
|
||||
if opts.Export != nil {
|
||||
filterOptions.Fields = opts.Export.Fields
|
||||
}
|
||||
|
||||
listResult, err := issueList(httpClient, baseRepo, filterOptions, opts.LimitResults)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -138,6 +154,14 @@ func listRun(opts *ListOptions) error {
|
|||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Export != nil {
|
||||
data := make([]interface{}, len(listResult.Issues))
|
||||
for i, issue := range listResult.Issues {
|
||||
data[i] = issue.ExportData(opts.Export.Fields)
|
||||
}
|
||||
return opts.Export.Write(opts.IO.Out, &data, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if isTerminal {
|
||||
title := prShared.ListHeader(ghrepo.FullName(baseRepo), "issue", len(listResult.Issues), listResult.TotalCount, !filterOptions.IsDefault())
|
||||
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)
|
||||
|
|
@ -160,32 +184,23 @@ func issueList(client *http.Client, repo ghrepo.Interface, filters prShared.Filt
|
|||
filters.Milestone = milestone.Title
|
||||
}
|
||||
|
||||
searchQuery := prShared.SearchQueryBuild(filters)
|
||||
return IssueSearch(apiClient, repo, searchQuery, limit)
|
||||
return searchIssues(apiClient, repo, filters, limit)
|
||||
}
|
||||
|
||||
var err error
|
||||
meReplacer := shared.NewMeReplacer(apiClient, repo.RepoHost())
|
||||
filterAssignee, err := meReplacer.Replace(filters.Assignee)
|
||||
filters.Assignee, err = meReplacer.Replace(filters.Assignee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filterAuthor, err := meReplacer.Replace(filters.Author)
|
||||
filters.Author, err = meReplacer.Replace(filters.Author)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filterMention, err := meReplacer.Replace(filters.Mention)
|
||||
filters.Mention, err = meReplacer.Replace(filters.Mention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return IssueList(
|
||||
apiClient,
|
||||
repo,
|
||||
filters.State,
|
||||
filterAssignee,
|
||||
limit,
|
||||
filterAuthor,
|
||||
filterMention,
|
||||
filters.Milestone,
|
||||
)
|
||||
return listIssues(apiClient, repo, filters, limit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ type ViewOptions struct {
|
|||
SelectorArg string
|
||||
WebMode bool
|
||||
Comments bool
|
||||
Export *cmdutil.ExportFormat
|
||||
|
||||
Now func() time.Time
|
||||
}
|
||||
|
|
@ -71,6 +72,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().BoolVarP(&opts.Comments, "comments", "c", false, "View issue comments")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Export, api.IssueFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -113,6 +115,11 @@ func viewRun(opts *ViewOptions) error {
|
|||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Export != nil {
|
||||
exportIssue := issue.ExportData(opts.Export.Fields)
|
||||
return opts.Export.Write(opts.IO.Out, exportIssue, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
return printHumanIssuePreview(opts, issue)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,6 @@ import (
|
|||
"github.com/cli/cli/pkg/githubsearch"
|
||||
)
|
||||
|
||||
const fragment = `fragment pr on PullRequest {
|
||||
number
|
||||
title
|
||||
state
|
||||
url
|
||||
headRefName
|
||||
headRepositoryOwner {
|
||||
login
|
||||
}
|
||||
isCrossRepository
|
||||
isDraft
|
||||
}`
|
||||
|
||||
func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) {
|
||||
if filters.Author != "" || filters.Assignee != "" || filters.Search != "" || len(filters.Labels) > 0 {
|
||||
return searchPullRequests(httpClient, repo, filters, limit)
|
||||
|
|
@ -41,6 +28,7 @@ func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters pr
|
|||
}
|
||||
}
|
||||
|
||||
fragment := fmt.Sprintf("fragment pr on PullRequest{%s}", api.PullRequestGraphQL(filters.Fields))
|
||||
query := fragment + `
|
||||
query PullRequestList(
|
||||
$owner: String!,
|
||||
|
|
@ -109,7 +97,7 @@ loop:
|
|||
res.TotalCount = prData.TotalCount
|
||||
|
||||
for _, pr := range prData.Nodes {
|
||||
if _, exists := check[pr.Number]; exists {
|
||||
if _, exists := check[pr.Number]; exists && pr.Number > 0 {
|
||||
continue
|
||||
}
|
||||
check[pr.Number] = struct{}{}
|
||||
|
|
@ -143,6 +131,7 @@ func searchPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters
|
|||
}
|
||||
}
|
||||
|
||||
fragment := fmt.Sprintf("fragment pr on PullRequest{%s}", api.PullRequestGraphQL(filters.Fields))
|
||||
query := fragment + `
|
||||
query PullRequestSearch(
|
||||
$q: String!,
|
||||
|
|
@ -209,7 +198,7 @@ loop:
|
|||
res.TotalCount = prData.IssueCount
|
||||
|
||||
for _, pr := range prData.Nodes {
|
||||
if _, exists := check[pr.Number]; exists {
|
||||
if _, exists := check[pr.Number]; exists && pr.Number > 0 {
|
||||
continue
|
||||
}
|
||||
check[pr.Number] = struct{}{}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,14 @@ type ListOptions struct {
|
|||
|
||||
WebMode bool
|
||||
LimitResults int
|
||||
State string
|
||||
BaseBranch string
|
||||
Labels []string
|
||||
Author string
|
||||
Assignee string
|
||||
Search string
|
||||
Export *cmdutil.ExportFormat
|
||||
|
||||
State string
|
||||
BaseBranch string
|
||||
Labels []string
|
||||
Author string
|
||||
Assignee string
|
||||
Search string
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
|
||||
|
|
@ -76,10 +78,22 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
cmd.Flags().StringVarP(&opts.Author, "author", "A", "", "Filter by author")
|
||||
cmd.Flags().StringVarP(&opts.Assignee, "assignee", "a", "", "Filter by assignee")
|
||||
cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search pull requests with `query`")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Export, api.PullRequestFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
var defaultFields = []string{
|
||||
"number",
|
||||
"title",
|
||||
"state",
|
||||
"url",
|
||||
"headRefName",
|
||||
"headRepositoryOwner",
|
||||
"isCrossRepository",
|
||||
"isDraft",
|
||||
}
|
||||
|
||||
func listRun(opts *ListOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
|
|
@ -99,6 +113,10 @@ func listRun(opts *ListOptions) error {
|
|||
Labels: opts.Labels,
|
||||
BaseBranch: opts.BaseBranch,
|
||||
Search: opts.Search,
|
||||
Fields: defaultFields,
|
||||
}
|
||||
if opts.Export != nil {
|
||||
filters.Fields = opts.Export.Fields
|
||||
}
|
||||
|
||||
if opts.WebMode {
|
||||
|
|
@ -121,10 +139,18 @@ func listRun(opts *ListOptions) error {
|
|||
|
||||
err = opts.IO.StartPager()
|
||||
if err != nil {
|
||||
return err
|
||||
fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v", err)
|
||||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Export != nil {
|
||||
data := make([]interface{}, len(listResult.PullRequests))
|
||||
for i, pr := range listResult.PullRequests {
|
||||
data[i] = pr.ExportData(opts.Export.Fields)
|
||||
}
|
||||
return opts.Export.Write(opts.IO.Out, &data, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
title := shared.ListHeader(ghrepo.FullName(baseRepo), "pull request", len(listResult.PullRequests), listResult.TotalCount, !filters.IsDefault())
|
||||
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)
|
||||
|
|
|
|||
|
|
@ -155,6 +155,8 @@ type FilterOptions struct {
|
|||
Mention string
|
||||
Milestone string
|
||||
Search string
|
||||
|
||||
Fields []string
|
||||
}
|
||||
|
||||
func (opts *FilterOptions) IsDefault() bool {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ type ViewOptions struct {
|
|||
Remotes func() (context.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
|
||||
Export *cmdutil.ExportFormat
|
||||
|
||||
SelectorArg string
|
||||
BrowserMode bool
|
||||
Comments bool
|
||||
|
|
@ -83,6 +85,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
|
||||
cmd.Flags().BoolVarP(&opts.BrowserMode, "web", "w", false, "Open a pull request in the browser")
|
||||
cmd.Flags().BoolVarP(&opts.Comments, "comments", "c", false, "View pull request comments")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Export, api.PullRequestFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -113,6 +116,11 @@ func viewRun(opts *ViewOptions) error {
|
|||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Export != nil {
|
||||
exportPR := pr.ExportData(opts.Export.Fields)
|
||||
return opts.Export.Write(opts.IO.Out, exportPR, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if connectedToTerminal {
|
||||
return printHumanPrPreview(opts, pr)
|
||||
}
|
||||
|
|
|
|||
101
pkg/cmdutil/json_flags.go
Normal file
101
pkg/cmdutil/json_flags.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package cmdutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/pkg/export"
|
||||
"github.com/cli/cli/pkg/jsoncolor"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type JSONFlagError struct {
|
||||
error
|
||||
}
|
||||
|
||||
func AddJSONFlags(cmd *cobra.Command, exportTarget **ExportFormat, fields []string) {
|
||||
f := cmd.Flags()
|
||||
f.StringSlice("json", nil, "Output JSON with the specified `fields`")
|
||||
f.StringP("jq", "q", "", "Filter JSON output using a jq `expression`")
|
||||
f.StringP("template", "t", "", "Format JSON output using a Go template")
|
||||
|
||||
oldPreRun := cmd.PreRunE
|
||||
cmd.PreRunE = func(c *cobra.Command, args []string) error {
|
||||
if oldPreRun != nil {
|
||||
if err := oldPreRun(c, args); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if export, err := checkJSONFlags(c); err == nil {
|
||||
*exportTarget = export
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd.SetFlagErrorFunc(func(c *cobra.Command, e error) error {
|
||||
if e.Error() == "flag needs an argument: --json" {
|
||||
sort.Strings(fields)
|
||||
return JSONFlagError{fmt.Errorf("Specify one or more comma-separated fields for `--json`:\n %s", strings.Join(fields, "\n "))}
|
||||
}
|
||||
return c.Parent().FlagErrorFunc()(c, e)
|
||||
})
|
||||
}
|
||||
|
||||
func checkJSONFlags(cmd *cobra.Command) (*ExportFormat, error) {
|
||||
f := cmd.Flags()
|
||||
jsonFlag := f.Lookup("json")
|
||||
jqFlag := f.Lookup("jq")
|
||||
tplFlag := f.Lookup("template")
|
||||
webFlag := f.Lookup("web")
|
||||
|
||||
if jsonFlag.Changed {
|
||||
if webFlag != nil && webFlag.Changed {
|
||||
return nil, errors.New("cannot use `--web` with `--json`")
|
||||
}
|
||||
jv := jsonFlag.Value.(pflag.SliceValue)
|
||||
return &ExportFormat{
|
||||
Fields: jv.GetSlice(),
|
||||
Filter: jqFlag.Value.String(),
|
||||
Template: tplFlag.Value.String(),
|
||||
}, nil
|
||||
} else if jqFlag.Changed {
|
||||
return nil, errors.New("cannot use `--jq` without specifying `--json`")
|
||||
} else if tplFlag.Changed {
|
||||
return nil, errors.New("cannot use `--template` without specifying `--json`")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type ExportFormat struct {
|
||||
Fields []string
|
||||
Filter string
|
||||
Template string
|
||||
}
|
||||
|
||||
func (e *ExportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) error {
|
||||
buf := bytes.Buffer{}
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.Filter != "" {
|
||||
return export.FilterJSON(w, &buf, e.Filter)
|
||||
} else if e.Template != "" {
|
||||
return export.ExecuteTemplate(w, &buf, e.Template, colorEnabled)
|
||||
} else if colorEnabled {
|
||||
return jsoncolor.Write(w, &buf, " ")
|
||||
}
|
||||
|
||||
_, err := io.Copy(w, &buf)
|
||||
return err
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue