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

89
api/export_pr.go Normal file
View 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)
})
}

View file

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

View file

@ -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 = `

View file

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

View file

@ -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
View 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, ",")
}

View file

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

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

101
pkg/cmdutil/json_flags.go Normal file
View 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
}