Add JSON output support to pr create by natsu0529 · Pull Request #13798 · cli/cli · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions acceptance/testdata/pr/pr-create-json-output.txtar
3 changes: 2 additions & 1 deletion api/queries_pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,10 +468,11 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
createPullRequest(input: $input) {
pullRequest {
id
number
url
}
}
}`
}`

inputParams := map[string]interface{}{
"repositoryId": repo.ID,
Expand Down
21 changes: 19 additions & 2 deletions pkg/cmd/pr/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,14 @@ type CreateOptions struct {
MaintainerCanModify bool
Template string

DryRun bool
DryRun bool
Exporter cmdutil.Exporter
}

// createOutputFields are the fields that can be exported with `--json` after
// creating a pull request.
var createOutputFields = []string{"id", "number", "url"}

// creationRefs is an interface that provides the necessary information for creating a pull request in the API.
// Upcasting to concrete implementations can provide further context on other operations (forking and pushing).
type creationRefs interface {
Expand Down Expand Up @@ -331,6 +336,10 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
return cmdutil.FlagErrorf("`--dry-run` is not supported when using `--web`")
}

if opts.DryRun && opts.Exporter != nil {
return cmdutil.FlagErrorf("`--dry-run` is not supported when using `--json`")
}

if runF != nil {
return runF(opts)
}
Expand Down Expand Up @@ -360,6 +369,11 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
fl.StringVarP(&opts.Template, "template", "T", "", "Template `file` to use as starting body text")
fl.BoolVar(&opts.DryRun, "dry-run", false, "Print details instead of creating the PR. May still push git changes.")

// `pr create` already uses `--template` (and `-T`) for PR templates, so only
// the `--json` and `--jq` flags are added here, without the `--template`
// output formatting flag.
cmdutil.AddJSONAndJQFlags(cmd, &opts.Exporter, createOutputFields)

_ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "base", "head")

_ = cmd.RegisterFlagCompletionFunc("reviewer", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
Expand Down Expand Up @@ -1065,7 +1079,7 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
opts.IO.StartProgressIndicator()
pr, err := api.CreatePullRequest(client, ctx.PRRefs.BaseRepo(), params)
opts.IO.StopProgressIndicator()
if pr != nil {
if pr != nil && opts.Exporter == nil {
fmt.Fprintln(opts.IO.Out, pr.URL)
}
if err != nil {
Expand All @@ -1074,6 +1088,9 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
}
return fmt.Errorf("pull request create failed: %w", err)
}
if opts.Exporter != nil {
return opts.Exporter.Write(opts.IO, pr)
}
return nil
}

Expand Down
157 changes: 157 additions & 0 deletions pkg/cmd/pr/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,21 @@ import (
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/jsonfieldstest"
"github.com/cli/cli/v2/test"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestJSONFields(t *testing.T) {
jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdCreate, []string{
"id",
"number",
"url",
})
}

func TestNewCmdCreate(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "my-body.md")
err := os.WriteFile(tmpFile, []byte("a body from file"), 0600)
Expand Down Expand Up @@ -203,6 +212,44 @@ func TestNewCmdCreate(t *testing.T) {
cli: "--web --dry-run",
wantsErr: true,
},
{
name: "dry-run and json",
tty: false,
cli: "--title mytitle --body '' --dry-run --json number",
wantsErr: true,
},
{
name: "web and json",
tty: false,
cli: "--title mytitle --body '' --web --json number",
wantsErr: true,
},
{
name: "jq without json",
tty: false,
cli: "--title mytitle --body '' --jq .number",
wantsErr: true,
},
{
name: "unknown json field",
tty: false,
cli: "--title mytitle --body '' --json headRefName",
wantsErr: true,
},
{
name: "json with pr template",
tty: true,
cli: "--template bug_fix.md --json number",
wantsErr: false,
wantsOpts: CreateOptions{
Title: "",
TitleProvided: false,
Body: "",
BodyProvided: false,
MaintainerCanModify: true,
Template: "bug_fix.md",
},
},
{
name: "editor by cli",
tty: true,
Expand Down Expand Up @@ -384,6 +431,116 @@ func Test_createRun(t *testing.T) {
},
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
},
{
name: "nontty json output",
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`mutation PullRequestCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createPullRequest": { "pullRequest": {
"ID": "NEWPULLID",
"Number": 12,
"URL": "https://github.com/OWNER/REPO/pull/12"
} } } }`,
func(input map[string]interface{}) {
assert.Equal(t, "REPOID", input["repositoryId"])
assert.Equal(t, "my title", input["title"])
assert.Equal(t, "my body", input["body"])
}))
},
setup: func(opts *CreateOptions, t *testing.T) func() {
opts.TitleProvided = true
opts.BodyProvided = true
opts.Title = "my title"
opts.Body = "my body"
opts.HeadBranch = "feature"
exporter := cmdutil.NewJSONExporter()
exporter.SetFields([]string{"id", "number", "url"})
opts.Exporter = exporter
return func() {}
},
expectedOut: "{\"id\":\"NEWPULLID\",\"number\":12,\"url\":\"https://github.com/OWNER/REPO/pull/12\"}\n",
},
{
name: "tty json output",
tty: true,
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`mutation PullRequestCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createPullRequest": { "pullRequest": {
"ID": "NEWPULLID",
"Number": 12,
"URL": "https://github.com/OWNER/REPO/pull/12"
} } } }`,
func(input map[string]interface{}) {}))
},
setup: func(opts *CreateOptions, t *testing.T) func() {
opts.TitleProvided = true
opts.BodyProvided = true
opts.Title = "my title"
opts.Body = "my body"
opts.HeadBranch = "feature"
exporter := cmdutil.NewJSONExporter()
exporter.SetFields([]string{"id", "number", "url"})
opts.Exporter = exporter
return func() {}
},
expectedOut: "{\"id\":\"NEWPULLID\",\"number\":12,\"url\":\"https://github.com/OWNER/REPO/pull/12\"}\n",
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
},
{
name: "json output with jq filter",
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`mutation PullRequestCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createPullRequest": { "pullRequest": {
"ID": "NEWPULLID",
"Number": 12,
"URL": "https://github.com/OWNER/REPO/pull/12"
} } } }`,
func(input map[string]interface{}) {}))
},
setup: func(opts *CreateOptions, t *testing.T) func() {
opts.TitleProvided = true
opts.BodyProvided = true
opts.Title = "my title"
opts.Body = "my body"
opts.HeadBranch = "feature"
exporter := cmdutil.NewJSONExporter()
exporter.SetFields([]string{"id", "number", "url"})
exporter.SetFilter(".number")
opts.Exporter = exporter
return func() {}
},
expectedOut: "12\n",
},
{
name: "json output does not print URL",
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`mutation PullRequestCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createPullRequest": { "pullRequest": {
"Number": 12,
"URL": "https://github.com/OWNER/REPO/pull/12"
} } } }`,
func(input map[string]interface{}) {}))
},
setup: func(opts *CreateOptions, t *testing.T) func() {
opts.TitleProvided = true
opts.BodyProvided = true
opts.Title = "my title"
opts.Body = "my body"
opts.HeadBranch = "feature"
exporter := cmdutil.NewJSONExporter()
exporter.SetFields([]string{"number"})
opts.Exporter = exporter
return func() {}
},
expectedOut: "{\"number\":12}\n",
},
{
name: "same head and base branch should error",
setup: func(opts *CreateOptions, t *testing.T) func() {
Expand Down
42 changes: 34 additions & 8 deletions pkg/cmdutil/json_flags.go
Loading