Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support dry-run in gh pr create #8376

Merged
merged 19 commits into from Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 71 additions & 1 deletion pkg/cmd/pr/create/create.go
Expand Up @@ -23,6 +23,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"
)

Expand Down Expand Up @@ -63,6 +64,8 @@ type CreateOptions struct {

MaintainerCanModify bool
Template string

DryRun bool
}

type CreateContext struct {
Expand Down Expand Up @@ -181,6 +184,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`) 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)
}
Expand All @@ -206,6 +213,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, "Dry run mode")

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

Expand Down Expand Up @@ -273,7 +281,7 @@ func createRun(opts *CreateOptions) (err error) {
if err != nil && !errors.As(err, &notFound) {
return fmt.Errorf("error checking for existing pull request: %w", err)
}
if err == nil {
if err == nil && !opts.DryRun {
v1v marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("a pull request for branch %q into branch %q already exists:\n%s",
ctx.HeadBranchLabel, ctx.BaseBranch, existingPR.URL)
}
Expand Down Expand Up @@ -696,6 +704,68 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
return err
}

if opts.DryRun {
out := opts.IO.Out
if opts.IO.IsStdoutTTY() {
fmt.Fprint(out, "Would have created a Pull Request with:\n")
andyfeller marked this conversation as resolved.
Show resolved Hide resolved
fmt.Fprintf(out, "title:\t%s\n", params["title"])
andyfeller marked this conversation as resolved.
Show resolved Hide resolved
fmt.Fprintf(out, "draft:\t%t\n", params["draft"])
andyfeller marked this conversation as resolved.
Show resolved Hide resolved
if len(state.Labels) != 0 {
fmt.Fprintf(out, "labels:\t%v\n", state.Labels)
}
if len(state.Reviewers) != 0 {
fmt.Fprintf(out, "reviewers:\t%v\n", state.Reviewers)
}
if len(state.Assignees) != 0 {
fmt.Fprintf(out, "assignees:\t%v\n", state.Assignees)
}
if len(state.Milestones) != 0 {
fmt.Fprintf(out, "milestones:\t%v\n", state.Milestones)
}
if len(state.Projects) != 0 {
fmt.Fprintf(out, "projects:\t%v\n", state.Projects)
}
if len(params["body"].(string)) != 0 {
fmt.Fprintln(out, "--")
fmt.Fprintln(out, params["body"])
}
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, the main difference between these two branches is the printing of the "draft" state, and the markdown rendering. Is the markdown rendering important enough to diverge here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the same principles done in some other places, such as

if opts.IO.IsStdoutTTY() {
if err := renderReleaseTTY(opts.IO, release); err != nil {
return err
}
} else {
if err := renderReleasePlain(opts.IO.Out, release); err != nil {
return err
}
}

If tty, then

renderedDescription, err := markdown.Render(release.Body,
markdown.WithTheme(io.TerminalTheme()),
markdown.WithWrap(io.TerminalWidth()))

Otherwise:

fmt.Fprint(w, release.Body)
if !strings.HasSuffix(release.Body, "\n") {
fmt.Fprintf(w, "\n")
}

IMO, I prefer the same output regardless of the command.

cs := opts.IO.ColorScheme()
fmt.Fprintf(out, "%s\n", cs.Bold(params["title"].(string)))
if len(state.Labels) != 0 {
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Labels"), state.Labels)
}
if len(state.Reviewers) != 0 {
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Reviewers"), state.Reviewers)
}
if len(state.Assignees) != 0 {
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Assignees"), state.Assignees)
}
if len(state.Milestones) != 0 {
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Milestones"), state.Milestones)
}
if len(state.Projects) != 0 {
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Projects"), state.Projects)
}

// Body
var md string
var err error
if len(params["body"].(string)) == 0 {
md = fmt.Sprintf("\n %s\n\n", cs.Gray("No description provided"))
} else {
md, err = markdown.Render(params["body"].(string),
markdown.WithTheme(opts.IO.TerminalTheme()),
markdown.WithWrap(opts.IO.TerminalWidth()))
if err != nil {
return err
}
v1v marked this conversation as resolved.
Show resolved Hide resolved
}
fmt.Fprintf(out, "\n%s\n", md)
}
return nil
}

opts.IO.StartProgressIndicator()
pr, err := api.CreatePullRequest(client, ctx.BaseRepo, params)
opts.IO.StopProgressIndicator()
Expand Down
127 changes: 127 additions & 0 deletions pkg/cmd/pr/create/create_test.go
Expand Up @@ -194,6 +194,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) {
Expand Down Expand Up @@ -1225,6 +1231,127 @@ func Test_determineTrackingBranch(t *testing.T) {
}
}

func Test_draft(t *testing.T) {
v1v marked this conversation as resolved.
Show resolved Hide resolved
tests := []struct {
name string
setup func(*CreateOptions, *testing.T) func()
cmdStubs func(*run.CommandStubber)
promptStubs func(*prompter.PrompterMock)
httpStubs func(*httpmock.Registry, *testing.T)
expectedOutputs []string
expectedErrOut string
expectedBrowse string
wantErr string
tty bool
}{
{
name: "dry-run",
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`,
`--`,
`my body`,
},
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
branch := "feature"

reg := &httpmock.Registry{}
reg.StubRepoInfoResponse("OWNER", "REPO", "master")
defer reg.Verify(t)
if tt.httpStubs != nil {
tt.httpStubs(reg, t)
}

pm := &prompter.PrompterMock{}

if tt.promptStubs != nil {
tt.promptStubs(pm)
}

cs, cmdTeardown := run.Stub()
defer cmdTeardown(t)
cs.Register(`git status --porcelain`, 0, "")

if tt.cmdStubs != nil {
tt.cmdStubs(cs)
}

opts := CreateOptions{}
opts.Prompter = pm

ios, _, stdout, stderr := iostreams.Test()
// TODO do i need to bother with this
ios.SetStdoutTTY(tt.tty)
ios.SetStdinTTY(tt.tty)
ios.SetStderrTTY(tt.tty)
browser := &browser.Stub{}
opts.IO = ios
opts.Browser = browser
opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
opts.Config = func() (config.Config, error) {
return config.NewBlankConfig(), nil
}
opts.Remotes = func() (context.Remotes, error) {
return context.Remotes{
{
Remote: &git.Remote{
Name: "origin",
Resolved: "base",
},
Repo: ghrepo.New("OWNER", "REPO"),
},
}, nil
}
opts.Branch = func() (string, error) {
return branch, nil
}
opts.Finder = shared.NewMockFinder(branch, nil, nil)
opts.GitClient = &git.Client{
GhPath: "some/path/gh",
GitPath: "some/path/git",
}
cleanSetup := func() {}
if tt.setup != nil {
cleanSetup = tt.setup(&opts, t)
}
defer cleanSetup()

err := createRun(&opts)
output := &test.CmdOut{
OutBuf: stdout,
ErrBuf: stderr,
BrowsedURL: browser.BrowsedURL(),
}
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
} else {
assert.NoError(t, err)
//nolint:staticcheck // prefer exact matchers over ExpectLines
test.ExpectLines(t, output.String(), tt.expectedOutputs...)
assert.Equal(t, tt.expectedErrOut, output.Stderr())
assert.Equal(t, tt.expectedBrowse, output.BrowsedURL)
}
})
}
}

func Test_generateCompareURL(t *testing.T) {
tests := []struct {
name string
Expand Down