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:
Mislav Marohnić 2021-04-13 20:29:31 +02:00
parent 19ea49b5a9
commit abe452bb19
16 changed files with 521 additions and 118 deletions

View file

@ -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(),

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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{}{}

View file

@ -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)

View file

@ -155,6 +155,8 @@ type FilterOptions struct {
Mention string
Milestone string
Search string
Fields []string
}
func (opts *FilterOptions) IsDefault() bool {

View file

@ -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)
}