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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue