Merge branch 'trunk' into gh-attestation-cmd

This commit is contained in:
Meredith Lancaster 2024-03-14 10:18:05 -06:00
commit af7f6996b9
4 changed files with 345 additions and 16 deletions

2
go.mod
View file

@ -46,7 +46,7 @@ require (
golang.org/x/term v0.17.0
golang.org/x/text v0.14.0
google.golang.org/grpc v1.61.0
google.golang.org/protobuf v1.32.0
google.golang.org/protobuf v1.33.0
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v3 v3.0.1
)

4
go.sum
View file

@ -562,8 +562,8 @@ google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View file

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
@ -23,6 +24,7 @@ import (
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/markdown"
"github.com/spf13/cobra"
)
@ -64,6 +66,8 @@ type CreateOptions struct {
MaintainerCanModify bool
Template string
DryRun bool
}
type CreateContext struct {
@ -190,6 +194,10 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
return cmdutil.FlagErrorf("must provide `--title` and `--body` (or `--fill` or `fill-first` or `--fillverbose`) when not running interactively")
}
if opts.DryRun && opts.WebMode {
return cmdutil.FlagErrorf("`--dry-run` is not supported when using `--web`")
}
if runF != nil {
return runF(opts)
}
@ -216,6 +224,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
fl.Bool("no-maintainer-edit", false, "Disable maintainer's ability to modify pull request")
fl.StringVar(&opts.RecoverFile, "recover", "", "Recover input from a failed run of create")
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.")
_ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "base", "head")
@ -292,6 +301,9 @@ func createRun(opts *CreateOptions) (err error) {
if state.Draft {
message = "\nCreating draft pull request for %s into %s in %s\n\n"
}
if opts.DryRun {
message = "\nDry Running pull request for %s into %s in %s\n\n"
}
cs := opts.IO.ColorScheme()
@ -360,7 +372,7 @@ func createRun(opts *CreateOptions) (err error) {
return
}
allowPreview := !state.HasMetadata() && shared.ValidURL(openURL)
allowPreview := !state.HasMetadata() && shared.ValidURL(openURL) && !opts.DryRun
allowMetadata := ctx.BaseRepo.ViewerCanTriage()
action, err := shared.ConfirmPRSubmission(opts.Prompter, allowPreview, allowMetadata, state.Draft)
if err != nil {
@ -379,7 +391,7 @@ func createRun(opts *CreateOptions) (err error) {
return
}
action, err = shared.ConfirmPRSubmission(opts.Prompter, !state.HasMetadata(), false, state.Draft)
action, err = shared.ConfirmPRSubmission(opts.Prompter, !state.HasMetadata() && !opts.DryRun, false, state.Draft)
if err != nil {
return
}
@ -712,6 +724,14 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
return err
}
if opts.DryRun {
if opts.IO.IsStdoutTTY() {
return renderPullRequestTTY(opts.IO, params, &state)
} else {
return renderPullRequestPlain(opts.IO.Out, params, &state)
}
}
opts.IO.StartProgressIndicator()
pr, err := api.CreatePullRequest(client, ctx.BaseRepo, params)
opts.IO.StopProgressIndicator()
@ -727,6 +747,80 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
return nil
}
func renderPullRequestPlain(w io.Writer, params map[string]interface{}, state *shared.IssueMetadataState) error {
fmt.Fprint(w, "Would have created a Pull Request with:\n")
fmt.Fprintf(w, "title:\t%s\n", params["title"])
fmt.Fprintf(w, "draft:\t%t\n", params["draft"])
fmt.Fprintf(w, "base:\t%s\n", params["baseRefName"])
fmt.Fprintf(w, "head:\t%s\n", params["headRefName"])
if len(state.Labels) != 0 {
fmt.Fprintf(w, "labels:\t%v\n", strings.Join(state.Labels, ", "))
}
if len(state.Reviewers) != 0 {
fmt.Fprintf(w, "reviewers:\t%v\n", strings.Join(state.Reviewers, ", "))
}
if len(state.Assignees) != 0 {
fmt.Fprintf(w, "assignees:\t%v\n", strings.Join(state.Assignees, ", "))
}
if len(state.Milestones) != 0 {
fmt.Fprintf(w, "milestones:\t%v\n", strings.Join(state.Milestones, ", "))
}
if len(state.Projects) != 0 {
fmt.Fprintf(w, "projects:\t%v\n", strings.Join(state.Projects, ", "))
}
fmt.Fprintf(w, "maintainerCanModify:\t%t\n", params["maintainerCanModify"])
fmt.Fprint(w, "body:\n")
if len(params["body"].(string)) != 0 {
fmt.Fprintln(w, params["body"])
}
return nil
}
func renderPullRequestTTY(io *iostreams.IOStreams, params map[string]interface{}, state *shared.IssueMetadataState) error {
iofmt := io.ColorScheme()
out := io.Out
fmt.Fprint(out, "Would have created a Pull Request with:\n")
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Title"), params["title"].(string))
fmt.Fprintf(out, "%s: %t\n", iofmt.Bold("Draft"), params["draft"])
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Base"), params["baseRefName"])
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Head"), params["headRefName"])
if len(state.Labels) != 0 {
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Labels"), strings.Join(state.Labels, ", "))
}
if len(state.Reviewers) != 0 {
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Reviewers"), strings.Join(state.Reviewers, ", "))
}
if len(state.Assignees) != 0 {
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Assignees"), strings.Join(state.Assignees, ", "))
}
if len(state.Milestones) != 0 {
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Milestones"), strings.Join(state.Milestones, ", "))
}
if len(state.Projects) != 0 {
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Projects"), strings.Join(state.Projects, ", "))
}
fmt.Fprintf(out, "%s: %t\n", iofmt.Bold("MaintainerCanModify"), params["maintainerCanModify"])
fmt.Fprintf(out, "%s\n", iofmt.Bold("Body:"))
// Body
var md string
var err error
if len(params["body"].(string)) == 0 {
md = fmt.Sprintf("%s\n", iofmt.Gray("No description provided"))
} else {
md, err = markdown.Render(params["body"].(string),
markdown.WithTheme(io.TerminalTheme()),
markdown.WithWrap(io.TerminalWidth()))
if err != nil {
return err
}
}
fmt.Fprintf(out, "%s", md)
return nil
}
func previewPR(opts CreateOptions, openURL string) error {
if opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))

View file

@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"github.com/MakeNowJust/heredoc"
@ -194,6 +195,12 @@ func TestNewCmdCreate(t *testing.T) {
cli: "--fill --fill-first",
wantsErr: true,
},
{
name: "dry-run and web",
tty: false,
cli: "--web --dry-run",
wantsErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -250,16 +257,17 @@ func TestNewCmdCreate(t *testing.T) {
func Test_createRun(t *testing.T) {
tests := []struct {
name string
setup func(*CreateOptions, *testing.T) func()
cmdStubs func(*run.CommandStubber)
promptStubs func(*prompter.PrompterMock)
httpStubs func(*httpmock.Registry, *testing.T)
expectedOut string
expectedErrOut string
expectedBrowse string
wantErr string
tty bool
name string
setup func(*CreateOptions, *testing.T) func()
cmdStubs func(*run.CommandStubber)
promptStubs func(*prompter.PrompterMock)
httpStubs func(*httpmock.Registry, *testing.T)
expectedOutputs []string
expectedOut string
expectedErrOut string
expectedBrowse string
wantErr string
tty bool
}{
{
name: "nontty web",
@ -300,6 +308,228 @@ func Test_createRun(t *testing.T) {
},
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
},
{
name: "dry-run-nontty-with-default-base",
tty: false,
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"
opts.DryRun = true
return func() {}
},
expectedOutputs: []string{
"Would have created a Pull Request with:",
`title: my title`,
`draft: false`,
`base: master`,
`head: feature`,
`maintainerCanModify: false`,
`body:`,
`my body`,
``,
},
expectedErrOut: "",
},
{
name: "dry-run-nontty-with-all-opts",
tty: false,
setup: func(opts *CreateOptions, t *testing.T) func() {
opts.TitleProvided = true
opts.BodyProvided = true
opts.Title = "TITLE"
opts.Body = "BODY"
opts.BaseBranch = "trunk"
opts.HeadBranch = "feature"
opts.Assignees = []string{"monalisa"}
opts.Labels = []string{"bug", "todo"}
opts.Projects = []string{"roadmap"}
opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"}
opts.Milestone = "big one.oh"
opts.DryRun = true
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
httpmock.StringResponse(`
{ "data": {
"u000": { "login": "MonaLisa", "id": "MONAID" },
"u001": { "login": "hubot", "id": "HUBOTID" },
"repository": {
"l000": { "name": "bug", "id": "BUGID" },
"l001": { "name": "TODO", "id": "TODOID" }
},
"organization": {
"t000": { "slug": "core", "id": "COREID" },
"t001": { "slug": "robots", "id": "ROBOTID" }
}
} }
`))
reg.Register(
httpmock.GraphQL(`query RepositoryMilestoneList\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "milestones": {
"nodes": [
{ "title": "GA", "id": "GAID" },
{ "title": "Big One.oh", "id": "BIGONEID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
mockRetrieveProjects(t, reg)
},
expectedOutputs: []string{
"Would have created a Pull Request with:",
`title: TITLE`,
`draft: false`,
`base: trunk`,
`head: feature`,
`labels: bug, todo`,
`reviewers: hubot, monalisa, /core, /robots`,
`assignees: monalisa`,
`milestones: big one.oh`,
`projects: roadmap`,
`maintainerCanModify: false`,
`body:`,
`BODY`,
``,
},
expectedErrOut: "",
},
{
name: "dry-run-tty-with-default-base",
tty: true,
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"
opts.DryRun = true
return func() {}
},
expectedOutputs: []string{
`Would have created a Pull Request with:`,
`Title: my title`,
`Draft: false`,
`Base: master`,
`Head: feature`,
`MaintainerCanModify: false`,
`Body:`,
``,
` my body `,
``,
``,
},
expectedErrOut: heredoc.Doc(`
Dry Running pull request for feature into master in OWNER/REPO
`),
},
{
name: "dry-run-tty-with-all-opts",
tty: true,
setup: func(opts *CreateOptions, t *testing.T) func() {
opts.TitleProvided = true
opts.BodyProvided = true
opts.Title = "TITLE"
opts.Body = "BODY"
opts.BaseBranch = "trunk"
opts.HeadBranch = "feature"
opts.Assignees = []string{"monalisa"}
opts.Labels = []string{"bug", "todo"}
opts.Projects = []string{"roadmap"}
opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"}
opts.Milestone = "big one.oh"
opts.DryRun = true
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
httpmock.StringResponse(`
{ "data": {
"u000": { "login": "MonaLisa", "id": "MONAID" },
"u001": { "login": "hubot", "id": "HUBOTID" },
"repository": {
"l000": { "name": "bug", "id": "BUGID" },
"l001": { "name": "TODO", "id": "TODOID" }
},
"organization": {
"t000": { "slug": "core", "id": "COREID" },
"t001": { "slug": "robots", "id": "ROBOTID" }
}
} }
`))
reg.Register(
httpmock.GraphQL(`query RepositoryMilestoneList\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "milestones": {
"nodes": [
{ "title": "GA", "id": "GAID" },
{ "title": "Big One.oh", "id": "BIGONEID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
mockRetrieveProjects(t, reg)
},
expectedOutputs: []string{
`Would have created a Pull Request with:`,
`Title: TITLE`,
`Draft: false`,
`Base: trunk`,
`Head: feature`,
`Labels: bug, todo`,
`Reviewers: hubot, monalisa, /core, /robots`,
`Assignees: monalisa`,
`Milestones: big one.oh`,
`Projects: roadmap`,
`MaintainerCanModify: false`,
`Body:`,
``,
` BODY `,
``,
``,
},
expectedErrOut: heredoc.Doc(`
Dry Running pull request for feature into trunk in OWNER/REPO
`),
},
{
name: "dry-run-tty-with-empty-body",
tty: true,
setup: func(opts *CreateOptions, t *testing.T) func() {
opts.TitleProvided = true
opts.BodyProvided = true
opts.Title = "TITLE"
opts.Body = ""
opts.HeadBranch = "feature"
opts.DryRun = true
return func() {}
},
expectedOut: heredoc.Doc(`
Would have created a Pull Request with:
Title: TITLE
Draft: false
Base: master
Head: feature
MaintainerCanModify: false
Body:
No description provided
`),
expectedErrOut: heredoc.Doc(`
Dry Running pull request for feature into master in OWNER/REPO
`),
},
{
name: "survey",
tty: true,
@ -1219,7 +1449,12 @@ func Test_createRun(t *testing.T) {
assert.EqualError(t, err, tt.wantErr)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedOut, output.String())
if tt.expectedOut != "" {
assert.Equal(t, tt.expectedOut, output.String())
}
if len(tt.expectedOutputs) > 0 {
assert.Equal(t, tt.expectedOutputs, strings.Split(output.String(), "\n"))
}
assert.Equal(t, tt.expectedErrOut, output.Stderr())
assert.Equal(t, tt.expectedBrowse, output.BrowsedURL)
}