1869 lines
59 KiB
Go
1869 lines
59 KiB
Go
package create
|
|
|
|
import (
|
|
ctx "context"
|
|
"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( .+)? 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() {}
|
|
},
|
|
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,
|
|
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 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 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 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 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 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 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( .+)? 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() {}
|
|
},
|
|
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() {}
|
|
},
|
|
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 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 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 -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( .+)? 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( .+)? 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() {}
|
|
},
|
|
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 -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 -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 -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 -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\|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
|
|
},
|
|
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\|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)
|
|
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 show-ref --verify -- HEAD`, 0, "abc HEAD")
|
|
},
|
|
expectedTrackingRef: trackingRef{},
|
|
expectedFound: false,
|
|
},
|
|
{
|
|
name: "no match",
|
|
cmdStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
|
|
cs.Register("git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature", 0, "abc HEAD\nbca refs/remotes/upstream/feature")
|
|
},
|
|
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 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
|
|
`))
|
|
},
|
|
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 show-ref --verify -- HEAD refs/remotes/origin/great-feat refs/remotes/origin/feature$`, 0, heredoc.Doc(`
|
|
deadbeef HEAD
|
|
deadb00f refs/remotes/origin/feature
|
|
`))
|
|
},
|
|
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",
|
|
}
|
|
headBranchConfig, _ := gitClient.ReadBranchConfig(ctx.Background(), "feature")
|
|
ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", 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
|