A recent refactor caused the API for ReadBranchConfig to change, resulting in changes for its consumers. Additionally, there was a large refactor of the tests associated with ReadBranchConfig and its consumers to gain confidence in this refactor. This commit attempts to resolve the conflicts between the refactor and this effort as well as massage the changes introduced here to reflect the refactor. The refactor PR can be found here: https://github.com/cli/cli/pull/10197 I'll note that there are still a few failing tests in status_test.go. I haven't had a chance to fully grok while they are failing, yet, and suspect that some insights from the original PR author may be helpful here. Full disclaimer: I haven't verified any of this is working locally yet. My primary motivation is to get these new changes working together in a manner that unblocks further iteration on this effort. * trunk: (79 commits) Enhance help docs on ext upgrade notices chore: fix some function names in comment Expand docs on cleaning extension update dir Simplifying cleanExtensionUpdateDir logic Separate logic for checking updates Capture greater detail on updaterEnabled Restore old error functionality of prSelectorForCurrentBranch Change error handling on ReadBranchConfig to respect git Exit Codes fix: add back colon that I removed fix: actually read how MaxFunc work and simplify the code fix: padded display Collapse dryrun checks in ext bin upgrade Bump github.com/mattn/go-colorable from 0.1.13 to 0.1.14 Rename test user in tests Change pr number in test Surface and handle error from ReadBranchConfig in parseCurrentBranch Directly stub headBranchConfig in Test_tryDetermineTrackingRef Refactor error handling in ReadBranchConfig to avoid panic Refine error handling of ReadBranchConfig Add test for empty BranchConfig in prSelectorForCurrentBranch ...
1956 lines
64 KiB
Go
1956 lines
64 KiB
Go
package create
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/api"
|
|
"github.com/cli/cli/v2/context"
|
|
"github.com/cli/cli/v2/git"
|
|
"github.com/cli/cli/v2/internal/browser"
|
|
"github.com/cli/cli/v2/internal/config"
|
|
"github.com/cli/cli/v2/internal/gh"
|
|
"github.com/cli/cli/v2/internal/ghrepo"
|
|
"github.com/cli/cli/v2/internal/prompter"
|
|
"github.com/cli/cli/v2/internal/run"
|
|
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
|
"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/test"
|
|
"github.com/google/shlex"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewCmdCreate(t *testing.T) {
|
|
tmpFile := filepath.Join(t.TempDir(), "my-body.md")
|
|
err := os.WriteFile(tmpFile, []byte("a body from file"), 0600)
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
tty bool
|
|
stdin string
|
|
cli string
|
|
config string
|
|
wantsErr bool
|
|
wantsOpts CreateOptions
|
|
}{
|
|
{
|
|
name: "empty non-tty",
|
|
tty: false,
|
|
cli: "",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "only title non-tty",
|
|
tty: false,
|
|
cli: "--title mytitle",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "minimum non-tty",
|
|
tty: false,
|
|
cli: "--title mytitle --body ''",
|
|
wantsErr: false,
|
|
wantsOpts: CreateOptions{
|
|
Title: "mytitle",
|
|
TitleProvided: true,
|
|
Body: "",
|
|
BodyProvided: true,
|
|
Autofill: false,
|
|
RecoverFile: "",
|
|
WebMode: false,
|
|
IsDraft: false,
|
|
BaseBranch: "",
|
|
HeadBranch: "",
|
|
MaintainerCanModify: true,
|
|
},
|
|
},
|
|
{
|
|
name: "empty tty",
|
|
tty: true,
|
|
cli: "",
|
|
wantsErr: false,
|
|
wantsOpts: CreateOptions{
|
|
Title: "",
|
|
TitleProvided: false,
|
|
Body: "",
|
|
BodyProvided: false,
|
|
Autofill: false,
|
|
RecoverFile: "",
|
|
WebMode: false,
|
|
IsDraft: false,
|
|
BaseBranch: "",
|
|
HeadBranch: "",
|
|
MaintainerCanModify: true,
|
|
},
|
|
},
|
|
{
|
|
name: "body from stdin",
|
|
tty: false,
|
|
stdin: "this is on standard input",
|
|
cli: "-t mytitle -F -",
|
|
wantsErr: false,
|
|
wantsOpts: CreateOptions{
|
|
Title: "mytitle",
|
|
TitleProvided: true,
|
|
Body: "this is on standard input",
|
|
BodyProvided: true,
|
|
Autofill: false,
|
|
RecoverFile: "",
|
|
WebMode: false,
|
|
IsDraft: false,
|
|
BaseBranch: "",
|
|
HeadBranch: "",
|
|
MaintainerCanModify: true,
|
|
},
|
|
},
|
|
{
|
|
name: "body from file",
|
|
tty: false,
|
|
cli: fmt.Sprintf("-t mytitle -F '%s'", tmpFile),
|
|
wantsErr: false,
|
|
wantsOpts: CreateOptions{
|
|
Title: "mytitle",
|
|
TitleProvided: true,
|
|
Body: "a body from file",
|
|
BodyProvided: true,
|
|
Autofill: false,
|
|
RecoverFile: "",
|
|
WebMode: false,
|
|
IsDraft: false,
|
|
BaseBranch: "",
|
|
HeadBranch: "",
|
|
MaintainerCanModify: true,
|
|
},
|
|
},
|
|
{
|
|
name: "template from file name tty",
|
|
tty: true,
|
|
cli: "-t mytitle --template bug_fix.md",
|
|
wantsErr: false,
|
|
wantsOpts: CreateOptions{
|
|
Title: "mytitle",
|
|
TitleProvided: true,
|
|
Body: "",
|
|
BodyProvided: false,
|
|
Autofill: false,
|
|
RecoverFile: "",
|
|
WebMode: false,
|
|
IsDraft: false,
|
|
BaseBranch: "",
|
|
HeadBranch: "",
|
|
MaintainerCanModify: true,
|
|
Template: "bug_fix.md",
|
|
},
|
|
},
|
|
{
|
|
name: "template from file name non-tty",
|
|
tty: false,
|
|
cli: "-t mytitle --template bug_fix.md",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "template and body",
|
|
tty: false,
|
|
cli: `-t mytitle --template bug_fix.md --body "pr body"`,
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "template and body file",
|
|
tty: false,
|
|
cli: "-t mytitle --template bug_fix.md --body-file body_file.md",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "fill-first",
|
|
tty: false,
|
|
cli: "--fill-first",
|
|
wantsErr: false,
|
|
wantsOpts: CreateOptions{
|
|
Title: "",
|
|
TitleProvided: false,
|
|
Body: "",
|
|
BodyProvided: false,
|
|
Autofill: false,
|
|
FillFirst: true,
|
|
RecoverFile: "",
|
|
WebMode: false,
|
|
IsDraft: false,
|
|
BaseBranch: "",
|
|
HeadBranch: "",
|
|
MaintainerCanModify: true,
|
|
},
|
|
},
|
|
{
|
|
name: "fill and fill-first",
|
|
tty: false,
|
|
cli: "--fill --fill-first",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "dry-run and web",
|
|
tty: false,
|
|
cli: "--web --dry-run",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "editor by cli",
|
|
tty: true,
|
|
cli: "--editor",
|
|
wantsErr: false,
|
|
wantsOpts: CreateOptions{
|
|
Title: "",
|
|
Body: "",
|
|
RecoverFile: "",
|
|
WebMode: false,
|
|
EditorMode: true,
|
|
MaintainerCanModify: true,
|
|
},
|
|
},
|
|
{
|
|
name: "editor by config",
|
|
tty: true,
|
|
cli: "",
|
|
config: "prefer_editor_prompt: enabled",
|
|
wantsErr: false,
|
|
wantsOpts: CreateOptions{
|
|
Title: "",
|
|
Body: "",
|
|
RecoverFile: "",
|
|
WebMode: false,
|
|
EditorMode: true,
|
|
MaintainerCanModify: true,
|
|
},
|
|
},
|
|
{
|
|
name: "editor and web",
|
|
tty: true,
|
|
cli: "--editor --web",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "can use web even though editor is enabled by config",
|
|
tty: true,
|
|
cli: `--web --title mytitle --body "issue body"`,
|
|
config: "prefer_editor_prompt: enabled",
|
|
wantsErr: false,
|
|
wantsOpts: CreateOptions{
|
|
Title: "mytitle",
|
|
Body: "issue body",
|
|
TitleProvided: true,
|
|
BodyProvided: true,
|
|
RecoverFile: "",
|
|
WebMode: true,
|
|
EditorMode: false,
|
|
MaintainerCanModify: true,
|
|
},
|
|
},
|
|
{
|
|
name: "editor with non-tty",
|
|
tty: false,
|
|
cli: "--editor",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "fill and base",
|
|
cli: "--fill --base trunk",
|
|
wantsOpts: CreateOptions{
|
|
Autofill: true,
|
|
BaseBranch: "trunk",
|
|
MaintainerCanModify: true,
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ios, stdin, stdout, stderr := iostreams.Test()
|
|
if tt.stdin != "" {
|
|
_, _ = stdin.WriteString(tt.stdin)
|
|
} else if tt.tty {
|
|
ios.SetStdinTTY(true)
|
|
ios.SetStdoutTTY(true)
|
|
}
|
|
|
|
f := &cmdutil.Factory{
|
|
IOStreams: ios,
|
|
Config: func() (gh.Config, error) {
|
|
if tt.config != "" {
|
|
return config.NewFromString(tt.config), nil
|
|
}
|
|
return config.NewBlankConfig(), nil
|
|
},
|
|
}
|
|
|
|
var opts *CreateOptions
|
|
cmd := NewCmdCreate(f, func(o *CreateOptions) error {
|
|
opts = o
|
|
return nil
|
|
})
|
|
|
|
args, err := shlex.Split(tt.cli)
|
|
require.NoError(t, err)
|
|
cmd.SetArgs(args)
|
|
cmd.SetOut(stderr)
|
|
cmd.SetErr(stderr)
|
|
_, err = cmd.ExecuteC()
|
|
if tt.wantsErr {
|
|
assert.Error(t, err)
|
|
return
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
assert.Equal(t, "", stdout.String())
|
|
assert.Equal(t, "", stderr.String())
|
|
|
|
assert.Equal(t, tt.wantsOpts.Body, opts.Body)
|
|
assert.Equal(t, tt.wantsOpts.BodyProvided, opts.BodyProvided)
|
|
assert.Equal(t, tt.wantsOpts.Title, opts.Title)
|
|
assert.Equal(t, tt.wantsOpts.TitleProvided, opts.TitleProvided)
|
|
assert.Equal(t, tt.wantsOpts.Autofill, opts.Autofill)
|
|
assert.Equal(t, tt.wantsOpts.FillFirst, opts.FillFirst)
|
|
assert.Equal(t, tt.wantsOpts.WebMode, opts.WebMode)
|
|
assert.Equal(t, tt.wantsOpts.RecoverFile, opts.RecoverFile)
|
|
assert.Equal(t, tt.wantsOpts.IsDraft, opts.IsDraft)
|
|
assert.Equal(t, tt.wantsOpts.MaintainerCanModify, opts.MaintainerCanModify)
|
|
assert.Equal(t, tt.wantsOpts.BaseBranch, opts.BaseBranch)
|
|
assert.Equal(t, tt.wantsOpts.HeadBranch, opts.HeadBranch)
|
|
assert.Equal(t, tt.wantsOpts.Template, opts.Template)
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
expectedOutputs []string
|
|
expectedOut string
|
|
expectedErrOut string
|
|
expectedBrowse string
|
|
wantErr string
|
|
tty bool
|
|
customBranchConfig bool
|
|
}{
|
|
{
|
|
name: "nontty web",
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.WebMode = true
|
|
opts.HeadBranch = "feature"
|
|
return func() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
|
|
},
|
|
expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1",
|
|
},
|
|
{
|
|
name: "nontty",
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"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"])
|
|
assert.Equal(t, "master", input["baseRefName"])
|
|
assert.Equal(t, "feature", input["headRefName"])
|
|
}))
|
|
},
|
|
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"
|
|
return func() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
},
|
|
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() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
},
|
|
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() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
},
|
|
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() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
},
|
|
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() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
},
|
|
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() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
},
|
|
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,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.TitleProvided = true
|
|
opts.BodyProvided = true
|
|
opts.Title = "my title"
|
|
opts.Body = "my body"
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.StubRepoResponse("OWNER", "REPO")
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }`, func(input map[string]interface{}) {
|
|
assert.Equal(t, "REPOID", input["repositoryId"].(string))
|
|
assert.Equal(t, "my title", input["title"].(string))
|
|
assert.Equal(t, "my body", input["body"].(string))
|
|
assert.Equal(t, "master", input["baseRefName"].(string))
|
|
assert.Equal(t, "feature", input["headRefName"].(string))
|
|
assert.Equal(t, false, input["draft"].(bool))
|
|
}))
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
|
|
cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
|
|
},
|
|
promptStubs: func(pm *prompter.PrompterMock) {
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
if p == "Where should we push the 'feature' branch?" {
|
|
return 0, nil
|
|
} else {
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "project v2",
|
|
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.Projects = []string{"RoadmapV2"}
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.StubRepoResponse("OWNER", "REPO")
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
|
|
mockRetrieveProjects(t, reg)
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"id": "PullRequest#1",
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }
|
|
`, func(input map[string]interface{}) {
|
|
assert.Equal(t, "REPOID", input["repositoryId"].(string))
|
|
assert.Equal(t, "my title", input["title"].(string))
|
|
assert.Equal(t, "my body", input["body"].(string))
|
|
assert.Equal(t, "master", input["baseRefName"].(string))
|
|
assert.Equal(t, "feature", input["headRefName"].(string))
|
|
assert.Equal(t, false, input["draft"].(bool))
|
|
}))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation UpdateProjectV2Items\b`),
|
|
httpmock.GraphQLQuery(`
|
|
{ "data": { "add_000": { "item": {
|
|
"id": "1"
|
|
} } } }
|
|
`, func(mutations string, inputs map[string]interface{}) {
|
|
variables, err := json.Marshal(inputs)
|
|
assert.NoError(t, err)
|
|
expectedMutations := "mutation UpdateProjectV2Items($input_000: AddProjectV2ItemByIdInput!) {add_000: addProjectV2ItemById(input: $input_000) { item { id } }}"
|
|
expectedVariables := `{"input_000":{"contentId":"PullRequest#1","projectId":"ROADMAPV2ID"}}`
|
|
assert.Equal(t, expectedMutations, mutations)
|
|
assert.Equal(t, expectedVariables, string(variables))
|
|
}))
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
|
|
cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
|
|
},
|
|
promptStubs: func(pm *prompter.PrompterMock) {
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
if p == "Where should we push the 'feature' branch?" {
|
|
return 0, nil
|
|
} else {
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "no maintainer modify",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.TitleProvided = true
|
|
opts.BodyProvided = true
|
|
opts.Title = "my title"
|
|
opts.Body = "my body"
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.StubRepoResponse("OWNER", "REPO")
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }
|
|
`, func(input map[string]interface{}) {
|
|
assert.Equal(t, false, input["maintainerCanModify"].(bool))
|
|
assert.Equal(t, "REPOID", input["repositoryId"].(string))
|
|
assert.Equal(t, "my title", input["title"].(string))
|
|
assert.Equal(t, "my body", input["body"].(string))
|
|
assert.Equal(t, "master", input["baseRefName"].(string))
|
|
assert.Equal(t, "feature", input["headRefName"].(string))
|
|
}))
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
|
|
cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
|
|
},
|
|
promptStubs: func(pm *prompter.PrompterMock) {
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
if p == "Where should we push the 'feature' branch?" {
|
|
return 0, nil
|
|
} else {
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "create fork",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.TitleProvided = true
|
|
opts.BodyProvided = true
|
|
opts.Title = "title"
|
|
opts.Body = "body"
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.StubRepoResponse("OWNER", "REPO")
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`))
|
|
reg.Register(
|
|
httpmock.REST("POST", "repos/OWNER/REPO/forks"),
|
|
httpmock.StatusStringResponse(201, `
|
|
{ "node_id": "NODEID",
|
|
"name": "REPO",
|
|
"owner": {"login": "monalisa"}
|
|
}`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
}}}}`, func(input map[string]interface{}) {
|
|
assert.Equal(t, "REPOID", input["repositoryId"].(string))
|
|
assert.Equal(t, "master", input["baseRefName"].(string))
|
|
assert.Equal(t, "monalisa:feature", input["headRefName"].(string))
|
|
}))
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
|
|
cs.Register("git remote rename origin upstream", 0, "")
|
|
cs.Register(`git remote add origin https://github.com/monalisa/REPO.git`, 0, "")
|
|
cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
|
|
},
|
|
promptStubs: func(pm *prompter.PrompterMock) {
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
if p == "Where should we push the 'feature' branch?" {
|
|
return prompter.IndexFor(opts, "Create a fork of OWNER/REPO")
|
|
} else {
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\nChanged OWNER/REPO remote to \"upstream\"\nAdded monalisa/REPO as remote \"origin\"\n",
|
|
},
|
|
{
|
|
name: "pushed to non base repo",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.TitleProvided = true
|
|
opts.BodyProvided = true
|
|
opts.Title = "title"
|
|
opts.Body = "body"
|
|
opts.Remotes = func() (context.Remotes, error) {
|
|
return context.Remotes{
|
|
{
|
|
Remote: &git.Remote{
|
|
Name: "upstream",
|
|
Resolved: "base",
|
|
},
|
|
Repo: ghrepo.New("OWNER", "REPO"),
|
|
},
|
|
{
|
|
Remote: &git.Remote{
|
|
Name: "origin",
|
|
Resolved: "base",
|
|
},
|
|
Repo: ghrepo.New("monalisa", "REPO"),
|
|
},
|
|
}, nil
|
|
}
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }`, func(input map[string]interface{}) {
|
|
assert.Equal(t, "REPOID", input["repositoryId"].(string))
|
|
assert.Equal(t, "master", input["baseRefName"].(string))
|
|
assert.Equal(t, "monalisa:feature", input["headRefName"].(string))
|
|
}))
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register("git show-ref --verify", 0, heredoc.Doc(`
|
|
deadbeef HEAD
|
|
deadb00f refs/remotes/upstream/feature
|
|
deadbeef refs/remotes/origin/feature`)) // determineTrackingBranch
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "pushed to different branch name",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.TitleProvided = true
|
|
opts.BodyProvided = true
|
|
opts.Title = "title"
|
|
opts.Body = "body"
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }
|
|
`, func(input map[string]interface{}) {
|
|
assert.Equal(t, "REPOID", input["repositoryId"].(string))
|
|
assert.Equal(t, "master", input["baseRefName"].(string))
|
|
assert.Equal(t, "my-feat2", input["headRefName"].(string))
|
|
}))
|
|
},
|
|
customBranchConfig: true,
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config --get-regexp \^branch\\\.feature\\\.`, 0, heredoc.Doc(`
|
|
branch.feature.remote origin
|
|
branch.feature.merge refs/heads/my-feat2
|
|
`)) // determineTrackingBranch
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/my-feat2")
|
|
cs.Register("git show-ref --verify", 0, heredoc.Doc(`
|
|
deadbeef HEAD
|
|
deadbeef refs/remotes/origin/my-feat2
|
|
`)) // determineTrackingBranch
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for my-feat2 into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "non legacy template",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.TitleProvided = true
|
|
opts.Title = "my title"
|
|
opts.HeadBranch = "feature"
|
|
opts.RootDirOverride = "./fixtures/repoWithNonLegacyPRTemplates"
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query PullRequestTemplates\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "pullRequestTemplates": [
|
|
{ "filename": "template1",
|
|
"body": "this is a bug" },
|
|
{ "filename": "template2",
|
|
"body": "this is a enhancement" }
|
|
] } } }`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }
|
|
`, func(input map[string]interface{}) {
|
|
assert.Equal(t, "my title", input["title"].(string))
|
|
assert.Equal(t, "- **commit 1**\n- **commit 0**\n\nthis is a bug", input["body"].(string))
|
|
}))
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "d3476a1\u0000commit 0\u0000\u0000\n7a6ea13\u0000commit 1\u0000\u0000")
|
|
},
|
|
promptStubs: func(pm *prompter.PrompterMock) {
|
|
pm.MarkdownEditorFunc = func(p, d string, ba bool) (string, error) {
|
|
if p == "Body" {
|
|
return d, nil
|
|
} else {
|
|
return "", prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
switch p {
|
|
case "What's next?":
|
|
return 0, nil
|
|
case "Choose a template":
|
|
return prompter.IndexFor(opts, "template1")
|
|
default:
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "metadata",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.TitleProvided = true
|
|
opts.Title = "TITLE"
|
|
opts.BodyProvided = true
|
|
opts.Body = "BODY"
|
|
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"
|
|
return func() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
},
|
|
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)
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"id": "NEWPULLID",
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }
|
|
`, func(inputs map[string]interface{}) {
|
|
assert.Equal(t, "TITLE", inputs["title"])
|
|
assert.Equal(t, "BODY", inputs["body"])
|
|
if v, ok := inputs["assigneeIds"]; ok {
|
|
t.Errorf("did not expect assigneeIds: %v", v)
|
|
}
|
|
if v, ok := inputs["userIds"]; ok {
|
|
t.Errorf("did not expect userIds: %v", v)
|
|
}
|
|
}))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreateMetadata\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "updatePullRequest": {
|
|
"clientMutationId": ""
|
|
} } }
|
|
`, func(inputs map[string]interface{}) {
|
|
assert.Equal(t, "NEWPULLID", inputs["pullRequestId"])
|
|
assert.Equal(t, []interface{}{"MONAID"}, inputs["assigneeIds"])
|
|
assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
|
|
assert.Equal(t, []interface{}{"ROADMAPID"}, inputs["projectIds"])
|
|
assert.Equal(t, "BIGONEID", inputs["milestoneId"])
|
|
}))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "requestReviews": {
|
|
"clientMutationId": ""
|
|
} } }
|
|
`, func(inputs map[string]interface{}) {
|
|
assert.Equal(t, "NEWPULLID", inputs["pullRequestId"])
|
|
assert.Equal(t, []interface{}{"HUBOTID", "MONAID"}, inputs["userIds"])
|
|
assert.Equal(t, []interface{}{"COREID", "ROBOTID"}, inputs["teamIds"])
|
|
assert.Equal(t, true, inputs["union"])
|
|
}))
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "already exists",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.TitleProvided = true
|
|
opts.BodyProvided = true
|
|
opts.Title = "title"
|
|
opts.Body = "body"
|
|
opts.HeadBranch = "feature"
|
|
opts.Finder = shared.NewMockFinder("feature", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO"))
|
|
return func() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
},
|
|
wantErr: "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123",
|
|
},
|
|
{
|
|
name: "web",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.WebMode = true
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.StubRepoResponse("OWNER", "REPO")
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
|
|
cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
|
|
cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
|
|
},
|
|
promptStubs: func(pm *prompter.PrompterMock) {
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
if p == "Where should we push the 'feature' branch?" {
|
|
return 0, nil
|
|
} else {
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
},
|
|
expectedErrOut: "Opening https://github.com/OWNER/REPO/compare/master...feature in your browser.\n",
|
|
expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1",
|
|
},
|
|
{
|
|
name: "web project",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.WebMode = true
|
|
opts.Projects = []string{"Triage"}
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.StubRepoResponse("OWNER", "REPO")
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
|
|
mockRetrieveProjects(t, reg)
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
|
|
cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
|
|
cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
|
|
},
|
|
promptStubs: func(pm *prompter.PrompterMock) {
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
if p == "Where should we push the 'feature' branch?" {
|
|
return 0, nil
|
|
} else {
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
},
|
|
expectedErrOut: "Opening https://github.com/OWNER/REPO/compare/master...feature in your browser.\n",
|
|
expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1&projects=ORG%2F1",
|
|
},
|
|
{
|
|
name: "draft",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.TitleProvided = true
|
|
opts.Title = "my title"
|
|
opts.HeadBranch = "feature"
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query PullRequestTemplates\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "pullRequestTemplates": [
|
|
{ "filename": "template1",
|
|
"body": "this is a bug" },
|
|
{ "filename": "template2",
|
|
"body": "this is a enhancement" }
|
|
] } } }`),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }
|
|
`, func(input map[string]interface{}) {
|
|
assert.Equal(t, true, input["draft"].(bool))
|
|
}))
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature`, 0, "")
|
|
cs.Register(`git rev-parse --show-toplevel`, 0, "")
|
|
},
|
|
promptStubs: func(pm *prompter.PrompterMock) {
|
|
pm.MarkdownEditorFunc = func(p, d string, ba bool) (string, error) {
|
|
if p == "Body" {
|
|
return d, nil
|
|
} else {
|
|
return "", prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
switch p {
|
|
case "What's next?":
|
|
return prompter.IndexFor(opts, "Submit as draft")
|
|
case "Choose a template":
|
|
return 0, nil
|
|
default:
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "recover",
|
|
tty: true,
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": {
|
|
"u000": { "login": "jillValentine", "id": "JILLID" },
|
|
"repository": {},
|
|
"organization": {}
|
|
} }
|
|
`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "requestReviews": {
|
|
"clientMutationId": ""
|
|
} } }
|
|
`, func(inputs map[string]interface{}) {
|
|
assert.Equal(t, []interface{}{"JILLID"}, inputs["userIds"])
|
|
}))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }
|
|
`, func(input map[string]interface{}) {
|
|
assert.Equal(t, "recovered title", input["title"].(string))
|
|
assert.Equal(t, "recovered body", input["body"].(string))
|
|
}))
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
|
|
},
|
|
promptStubs: func(pm *prompter.PrompterMock) {
|
|
pm.InputFunc = func(p, d string) (string, error) {
|
|
if p == "Title (required)" {
|
|
return d, nil
|
|
} else {
|
|
return "", prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
pm.MarkdownEditorFunc = func(p, d string, ba bool) (string, error) {
|
|
if p == "Body" {
|
|
return d, nil
|
|
} else {
|
|
return "", prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
if p == "What's next?" {
|
|
return 0, nil
|
|
} else {
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
},
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
tmpfile, err := os.CreateTemp(t.TempDir(), "testrecover*")
|
|
assert.NoError(t, err)
|
|
state := shared.IssueMetadataState{
|
|
Title: "recovered title",
|
|
Body: "recovered body",
|
|
Reviewers: []string{"jillValentine"},
|
|
}
|
|
data, err := json.Marshal(state)
|
|
assert.NoError(t, err)
|
|
_, err = tmpfile.Write(data)
|
|
assert.NoError(t, err)
|
|
|
|
opts.RecoverFile = tmpfile.Name()
|
|
opts.HeadBranch = "feature"
|
|
return func() { tmpfile.Close() }
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "web long URL",
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
|
|
},
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
longBody := make([]byte, 9216)
|
|
opts.Body = string(longBody)
|
|
opts.BodyProvided = true
|
|
opts.WebMode = true
|
|
opts.HeadBranch = "feature"
|
|
return func() {}
|
|
},
|
|
wantErr: "cannot open in browser: maximum URL length exceeded",
|
|
},
|
|
{
|
|
name: "no local git repo",
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.Title = "My PR"
|
|
opts.TitleProvided = true
|
|
opts.Body = ""
|
|
opts.BodyProvided = true
|
|
opts.HeadBranch = "feature"
|
|
opts.RepoOverride = "OWNER/REPO"
|
|
opts.Remotes = func() (context.Remotes, error) {
|
|
return nil, errors.New("not a git repository")
|
|
}
|
|
return func() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }
|
|
`))
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
},
|
|
{
|
|
name: "single commit title and body are used",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.HeadBranch = "feature"
|
|
return func() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(
|
|
"git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature",
|
|
0,
|
|
"3a9b48085046d156c5acce8f3b3a0532cd706a4a\u0000first commit of pr\u0000first commit description\u0000\n",
|
|
)
|
|
cs.Register(`git rev-parse --show-toplevel`, 0, "")
|
|
},
|
|
promptStubs: func(pm *prompter.PrompterMock) {
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
if p == "Where should we push the 'feature' branch?" {
|
|
return 0, nil
|
|
} else {
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
|
|
pm.InputFunc = func(p, d string) (string, error) {
|
|
if p == "Title (required)" {
|
|
return d, nil
|
|
} else if p == "Body" {
|
|
return d, nil
|
|
} else {
|
|
return "", prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
|
|
pm.MarkdownEditorFunc = func(p, d string, ba bool) (string, error) {
|
|
if p == "Body" {
|
|
return d, nil
|
|
} else {
|
|
return "", prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
|
|
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
|
if p == "What's next?" {
|
|
return 0, nil
|
|
} else {
|
|
return -1, prompter.NoSuchPromptErr(p)
|
|
}
|
|
}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query PullRequestTemplates\b`),
|
|
httpmock.StringResponse(`{ "data": { "repository": { "pullRequestTemplates": [] } } }`),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{
|
|
"data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } }
|
|
}
|
|
`,
|
|
func(input map[string]interface{}) {
|
|
assert.Equal(t, "first commit of pr", input["title"], "pr title should be first commit message")
|
|
assert.Equal(t, "first commit description", input["body"], "pr body should be first commit description")
|
|
},
|
|
),
|
|
)
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "fill-first flag provided",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.FillFirst = true
|
|
opts.HeadBranch = "feature"
|
|
return func() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(
|
|
"git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature",
|
|
0,
|
|
"56b6f8bb7c9e3a30093cd17e48934ce354148e80\u0000second commit of pr\u0000\u0000\n"+
|
|
"3a9b48085046d156c5acce8f3b3a0532cd706a4a\u0000first commit of pr\u0000first commit description\u0000\n",
|
|
)
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{
|
|
"data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } }
|
|
}
|
|
`,
|
|
func(input map[string]interface{}) {
|
|
assert.Equal(t, "first commit of pr", input["title"], "pr title should be first commit message")
|
|
assert.Equal(t, "first commit description", input["body"], "pr body should be first commit description")
|
|
},
|
|
),
|
|
)
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "fillverbose flag provided",
|
|
tty: true,
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.FillVerbose = true
|
|
opts.HeadBranch = "feature"
|
|
return func() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(
|
|
"git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature",
|
|
0,
|
|
"56b6f8bb7c9e3a30093cd17e48934ce354148e80\u0000second commit of pr\u0000second commit description\u0000\n"+
|
|
"3a9b48085046d156c5acce8f3b3a0532cd706a4a\u0000first commit of pr\u0000first commit with super long description, with super long description, with super long description, with super long description.\u0000\n",
|
|
)
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{
|
|
"data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } }
|
|
}
|
|
`,
|
|
func(input map[string]interface{}) {
|
|
assert.Equal(t, "feature", input["title"], "pr title should be branch name")
|
|
assert.Equal(t, "- **first commit of pr**\n first commit with super long description, with super long description, with super long description, with super long description.\n\n- **second commit of pr**\n second commit description", input["body"], "pr body should be commits msg+body")
|
|
},
|
|
),
|
|
)
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
|
},
|
|
{
|
|
name: "editor",
|
|
httpStubs: func(r *httpmock.Registry, t *testing.T) {
|
|
r.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{
|
|
"data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } }
|
|
}
|
|
`, func(inputs map[string]interface{}) {
|
|
assert.Equal(t, "title", inputs["title"])
|
|
assert.Equal(t, "body", inputs["body"])
|
|
}))
|
|
},
|
|
setup: func(opts *CreateOptions, t *testing.T) func() {
|
|
opts.EditorMode = true
|
|
opts.HeadBranch = "feature"
|
|
opts.TitledEditSurvey = func(string, string) (string, string, error) { return "title", "body", nil }
|
|
return func() {}
|
|
},
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register("git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, "")
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
},
|
|
{
|
|
name: "gh-merge-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.Branch = func() (string, error) {
|
|
return "task1", nil
|
|
}
|
|
opts.Remotes = func() (context.Remotes, error) {
|
|
return context.Remotes{
|
|
{
|
|
Remote: &git.Remote{
|
|
Name: "upstream",
|
|
Resolved: "base",
|
|
},
|
|
Repo: ghrepo.New("OWNER", "REPO"),
|
|
},
|
|
{
|
|
Remote: &git.Remote{
|
|
Name: "origin",
|
|
},
|
|
Repo: ghrepo.New("monalisa", "REPO"),
|
|
},
|
|
}, nil
|
|
}
|
|
return func() {}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "createPullRequest": { "pullRequest": {
|
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
|
} } } }
|
|
`, func(input map[string]interface{}) {
|
|
assert.Equal(t, "REPOID", input["repositoryId"].(string))
|
|
assert.Equal(t, "my title", input["title"].(string))
|
|
assert.Equal(t, "my body", input["body"].(string))
|
|
assert.Equal(t, "feature/feat2", input["baseRefName"].(string))
|
|
assert.Equal(t, "monalisa:task1", input["headRefName"].(string))
|
|
}))
|
|
},
|
|
customBranchConfig: true,
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config --get-regexp \^branch\\\.task1\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 0, heredoc.Doc(`
|
|
branch.task1.remote origin
|
|
branch.task1.merge refs/heads/task1
|
|
branch.task1.gh-merge-base feature/feat2`)) // ReadBranchConfig
|
|
cs.Register(`git show-ref --verify`, 0, heredoc.Doc(`
|
|
deadbeef HEAD
|
|
deadb00f refs/remotes/upstream/feature/feat2
|
|
deadbeef refs/remotes/origin/task1`)) // determineTrackingBranch
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref task1@\{push\}`, 1, "")
|
|
},
|
|
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
|
expectedErrOut: "\nCreating pull request for monalisa:task1 into feature/feat2 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.customBranchConfig {
|
|
cs.Register(`git config --get-regexp \^branch\\\..+\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 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() (gh.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)
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_tryDetermineTrackingRef(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cmdStubs func(*run.CommandStubber)
|
|
headBranchConfig git.BranchConfig
|
|
remotes context.Remotes
|
|
expectedTrackingRef trackingRef
|
|
expectedFound bool
|
|
}{
|
|
{
|
|
name: "empty",
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD")
|
|
},
|
|
headBranchConfig: git.BranchConfig{},
|
|
expectedTrackingRef: trackingRef{},
|
|
expectedFound: false,
|
|
},
|
|
{
|
|
name: "no match",
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "")
|
|
cs.Register("git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature", 0, "abc HEAD\nbca refs/remotes/upstream/feature")
|
|
},
|
|
headBranchConfig: git.BranchConfig{},
|
|
remotes: context.Remotes{
|
|
&context.Remote{
|
|
Remote: &git.Remote{Name: "upstream"},
|
|
Repo: ghrepo.New("octocat", "Spoon-Knife"),
|
|
},
|
|
&context.Remote{
|
|
Remote: &git.Remote{Name: "origin"},
|
|
Repo: ghrepo.New("hubot", "Spoon-Knife"),
|
|
},
|
|
},
|
|
expectedTrackingRef: trackingRef{},
|
|
expectedFound: false,
|
|
},
|
|
{
|
|
name: "match",
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/feature")
|
|
cs.Register(`git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature$`, 0, heredoc.Doc(`
|
|
deadbeef HEAD
|
|
deadb00f refs/remotes/upstream/feature
|
|
deadbeef refs/remotes/origin/feature
|
|
`))
|
|
},
|
|
headBranchConfig: git.BranchConfig{},
|
|
remotes: context.Remotes{
|
|
&context.Remote{
|
|
Remote: &git.Remote{Name: "upstream"},
|
|
Repo: ghrepo.New("octocat", "Spoon-Knife"),
|
|
},
|
|
&context.Remote{
|
|
Remote: &git.Remote{Name: "origin"},
|
|
Repo: ghrepo.New("hubot", "Spoon-Knife"),
|
|
},
|
|
},
|
|
expectedTrackingRef: trackingRef{
|
|
remoteName: "origin",
|
|
branchName: "feature",
|
|
},
|
|
expectedFound: true,
|
|
},
|
|
{
|
|
name: "respect tracking config",
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, heredoc.Doc(`
|
|
branch.feature.remote origin
|
|
branch.feature.merge refs/heads/great-feat
|
|
`))
|
|
cs.Register(`git config remote.pushDefault`, 1, "")
|
|
cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/great-feat")
|
|
cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/great-feat refs/remotes/origin/feature$`, 0, heredoc.Doc(`
|
|
deadbeef HEAD
|
|
deadb00f refs/remotes/origin/feature
|
|
`))
|
|
},
|
|
headBranchConfig: git.BranchConfig{
|
|
RemoteName: "origin",
|
|
MergeRef: "refs/heads/great-feat",
|
|
},
|
|
remotes: context.Remotes{
|
|
&context.Remote{
|
|
Remote: &git.Remote{Name: "origin"},
|
|
Repo: ghrepo.New("hubot", "Spoon-Knife"),
|
|
},
|
|
},
|
|
expectedTrackingRef: trackingRef{},
|
|
expectedFound: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cs, cmdTeardown := run.Stub()
|
|
defer cmdTeardown(t)
|
|
|
|
tt.cmdStubs(cs)
|
|
|
|
gitClient := &git.Client{
|
|
GhPath: "some/path/gh",
|
|
GitPath: "some/path/git",
|
|
}
|
|
|
|
ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", tt.headBranchConfig)
|
|
|
|
assert.Equal(t, tt.expectedTrackingRef, ref)
|
|
assert.Equal(t, tt.expectedFound, found)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_generateCompareURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ctx CreateContext
|
|
state shared.IssueMetadataState
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "basic",
|
|
ctx: CreateContext{
|
|
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
|
|
BaseBranch: "main",
|
|
HeadBranchLabel: "feature",
|
|
},
|
|
want: "https://github.com/OWNER/REPO/compare/main...feature?body=&expand=1",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with labels",
|
|
ctx: CreateContext{
|
|
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
|
|
BaseBranch: "a",
|
|
HeadBranchLabel: "b",
|
|
},
|
|
state: shared.IssueMetadataState{
|
|
Labels: []string{"one", "two three"},
|
|
},
|
|
want: "https://github.com/OWNER/REPO/compare/a...b?body=&expand=1&labels=one%2Ctwo+three",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "'/'s in branch names/labels are percent-encoded",
|
|
ctx: CreateContext{
|
|
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
|
|
BaseBranch: "main/trunk",
|
|
HeadBranchLabel: "owner:feature",
|
|
},
|
|
want: "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner:feature?body=&expand=1",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Any of !'(),; but none of $&+=@ and : in branch names/labels are percent-encoded ",
|
|
/*
|
|
- Technically, per section 3.3 of RFC 3986, none of !$&'()*+,;= (sub-delims) and :[]@ (part of gen-delims) in path segments are optionally percent-encoded, but url.PathEscape percent-encodes !'(),; anyway
|
|
- !$&'()+,;=@ is a valid Git branch name—essentially RFC 3986 sub-delims without * and gen-delims without :/?#[]
|
|
- : is GitHub separator between a fork name and a branch name
|
|
- See https://github.com/golang/go/issues/27559.
|
|
*/
|
|
ctx: CreateContext{
|
|
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
|
|
BaseBranch: "main/trunk",
|
|
HeadBranchLabel: "owner:!$&'()+,;=@",
|
|
},
|
|
want: "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner:%21$&%27%28%29+%2C%3B=@?body=&expand=1",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with template",
|
|
ctx: CreateContext{
|
|
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
|
|
BaseBranch: "main",
|
|
HeadBranchLabel: "feature",
|
|
},
|
|
state: shared.IssueMetadataState{
|
|
Template: "story.md",
|
|
},
|
|
want: "https://github.com/OWNER/REPO/compare/main...feature?body=&expand=1&template=story.md",
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := generateCompareURL(tt.ctx, tt.state)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("generateCompareURL() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("generateCompareURL() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func mockRetrieveProjects(_ *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query RepositoryProjectList\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "projects": {
|
|
"nodes": [
|
|
{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" },
|
|
{ "name": "Roadmap", "id": "ROADMAPID", "resourcePath": "/OWNER/REPO/projects/2" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "projectsV2": {
|
|
"nodes": [
|
|
{ "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/3" },
|
|
{ "title": "RoadmapV2", "id": "ROADMAPV2ID", "resourcePath": "/OWNER/REPO/projects/4" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query OrganizationProjectList\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "organization": { "projects": {
|
|
"nodes": [
|
|
{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "organization": { "projectsV2": {
|
|
"nodes": [
|
|
{ "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserProjectV2List\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "viewer": { "projectsV2": {
|
|
"nodes": [
|
|
{ "title": "MonalisaV2", "id": "MONALISAV2ID", "resourcePath": "/user/MONALISA/projects/2" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
}
|
|
|
|
// TODO interactive metadata tests once: 1) we have test utils for Prompter and 2) metadata questions use Prompter
|