A new GitHub feature landed where the API client can specify the desired name of the new fork. This avoids the necessity of subsequently having to rename the forked repo after the fork operation has created one. For backwards compatibility, the renaming logic is still here, but activates only if the resulting repo name is not the desired name.
497 lines
16 KiB
Go
497 lines
16 KiB
Go
package create
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/cli/cli/v2/internal/config"
|
|
"github.com/cli/cli/v2/internal/run"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/httpmock"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/cli/cli/v2/pkg/prompt"
|
|
"github.com/google/shlex"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewCmdCreate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tty bool
|
|
cli string
|
|
wantsErr bool
|
|
errMsg string
|
|
wantsOpts CreateOptions
|
|
}{
|
|
{
|
|
name: "no args tty",
|
|
tty: true,
|
|
cli: "",
|
|
wantsOpts: CreateOptions{Interactive: true},
|
|
},
|
|
{
|
|
name: "no args no-tty",
|
|
tty: false,
|
|
cli: "",
|
|
wantsErr: true,
|
|
errMsg: "at least one argument required in non-interactive mode",
|
|
},
|
|
{
|
|
name: "new repo from remote",
|
|
cli: "NEWREPO --public --clone",
|
|
wantsOpts: CreateOptions{
|
|
Name: "NEWREPO",
|
|
Public: true,
|
|
Clone: true},
|
|
},
|
|
{
|
|
name: "no visibility",
|
|
tty: true,
|
|
cli: "NEWREPO",
|
|
wantsErr: true,
|
|
errMsg: "`--public`, `--private`, or `--internal` required when not running interactively",
|
|
},
|
|
{
|
|
name: "multiple visibility",
|
|
tty: true,
|
|
cli: "NEWREPO --public --private",
|
|
wantsErr: true,
|
|
errMsg: "expected exactly one of `--public`, `--private`, or `--internal`",
|
|
},
|
|
{
|
|
name: "new remote from local",
|
|
cli: "--source=/path/to/repo --private",
|
|
wantsOpts: CreateOptions{
|
|
Private: true,
|
|
Source: "/path/to/repo"},
|
|
},
|
|
{
|
|
name: "new remote from local with remote",
|
|
cli: "--source=/path/to/repo --public --remote upstream",
|
|
wantsOpts: CreateOptions{
|
|
Public: true,
|
|
Source: "/path/to/repo",
|
|
Remote: "upstream",
|
|
},
|
|
},
|
|
{
|
|
name: "new remote from local with push",
|
|
cli: "--source=/path/to/repo --push --public",
|
|
wantsOpts: CreateOptions{
|
|
Public: true,
|
|
Source: "/path/to/repo",
|
|
Push: true,
|
|
},
|
|
},
|
|
{
|
|
name: "new remote from local without visibility",
|
|
cli: "--source=/path/to/repo --push",
|
|
wantsOpts: CreateOptions{
|
|
Source: "/path/to/repo",
|
|
Push: true,
|
|
},
|
|
wantsErr: true,
|
|
errMsg: "`--public`, `--private`, or `--internal` required when not running interactively",
|
|
},
|
|
{
|
|
name: "source with template",
|
|
cli: "--source=/path/to/repo --private --template mytemplate",
|
|
wantsErr: true,
|
|
errMsg: "the `--source` option is not supported with `--clone`, `--template`, `--license`, or `--gitignore`",
|
|
},
|
|
{
|
|
name: "include all branches without template",
|
|
cli: "--source=/path/to/repo --private --include-all-branches",
|
|
wantsErr: true,
|
|
errMsg: "the `--include-all-branches` option is only supported when using `--template`",
|
|
},
|
|
{
|
|
name: "new remote from template with include all branches",
|
|
cli: "template-repo --template https://github.com/OWNER/REPO --public --include-all-branches",
|
|
wantsOpts: CreateOptions{
|
|
Name: "template-repo",
|
|
Public: true,
|
|
Template: "https://github.com/OWNER/REPO",
|
|
IncludeAllBranches: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ios, _, _, _ := iostreams.Test()
|
|
ios.SetStdinTTY(tt.tty)
|
|
ios.SetStdoutTTY(tt.tty)
|
|
|
|
f := &cmdutil.Factory{
|
|
IOStreams: ios,
|
|
}
|
|
|
|
var opts *CreateOptions
|
|
cmd := NewCmdCreate(f, func(o *CreateOptions) error {
|
|
opts = o
|
|
return nil
|
|
})
|
|
|
|
// TODO STUPID HACK
|
|
// cobra aggressively adds help to all commands. since we're not running through the root command
|
|
// (which manages help when running for real) and since create has a '-h' flag (for homepage),
|
|
// cobra blows up when it tried to add a help flag and -h is already in use. This hack adds a
|
|
// dummy help flag with a random shorthand to get around this.
|
|
cmd.Flags().BoolP("help", "x", false, "")
|
|
|
|
args, err := shlex.Split(tt.cli)
|
|
require.NoError(t, err)
|
|
cmd.SetArgs(args)
|
|
cmd.SetIn(&bytes.Buffer{})
|
|
cmd.SetOut(&bytes.Buffer{})
|
|
cmd.SetErr(&bytes.Buffer{})
|
|
|
|
_, err = cmd.ExecuteC()
|
|
if tt.wantsErr {
|
|
assert.Error(t, err)
|
|
assert.Equal(t, tt.errMsg, err.Error())
|
|
return
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
assert.Equal(t, tt.wantsOpts.Interactive, opts.Interactive)
|
|
assert.Equal(t, tt.wantsOpts.Source, opts.Source)
|
|
assert.Equal(t, tt.wantsOpts.Name, opts.Name)
|
|
assert.Equal(t, tt.wantsOpts.Public, opts.Public)
|
|
assert.Equal(t, tt.wantsOpts.Internal, opts.Internal)
|
|
assert.Equal(t, tt.wantsOpts.Private, opts.Private)
|
|
assert.Equal(t, tt.wantsOpts.Clone, opts.Clone)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_createRun(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tty bool
|
|
opts *CreateOptions
|
|
httpStubs func(*httpmock.Registry)
|
|
askStubs func(*prompt.AskStubber)
|
|
execStubs func(*run.CommandStubber)
|
|
wantStdout string
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "interactive create from scratch with gitignore and license",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n",
|
|
askStubs: func(as *prompt.AskStubber) {
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne("Create a new repository on GitHub from scratch")
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "repoName", Value: "REPO"},
|
|
{Name: "repoDescription", Value: "my new repo"},
|
|
{Name: "repoVisibility", Value: "Private"},
|
|
})
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "addGitIgnore", Value: true}})
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "chooseGitIgnore", Value: "Go"}})
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "addLicense", Value: true}})
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "chooseLicense", Value: "GNU Lesser General Public License v3.0"}})
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "confirmSubmit", Value: true}})
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne(true) //clone locally?
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "gitignore/templates"),
|
|
httpmock.StringResponse(`["Actionscript","Android","AppceleratorTitanium","Autotools","Bancha","C","C++","Go"]`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "licenses"),
|
|
httpmock.StringResponse(`[{"key": "mit","name": "MIT License"},{"key": "lgpl-3.0","name": "GNU Lesser General Public License v3.0"}]`))
|
|
reg.Register(
|
|
httpmock.REST("POST", "user/repos"),
|
|
httpmock.StringResponse(`{"name":"REPO", "owner":{"login": "OWNER"}, "html_url":"https://github.com/OWNER/REPO"}`))
|
|
|
|
},
|
|
execStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "")
|
|
},
|
|
},
|
|
{
|
|
name: "interactive create from scratch but cancel before submit",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
askStubs: func(as *prompt.AskStubber) {
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne("Create a new repository on GitHub from scratch")
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "repoName", Value: "REPO"},
|
|
{Name: "repoDescription", Value: "my new repo"},
|
|
{Name: "repoVisibility", Value: "Private"},
|
|
})
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "addGitIgnore", Value: false}})
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "addLicense", Value: false}})
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "confirmSubmit", Value: false}})
|
|
},
|
|
wantStdout: "",
|
|
wantErr: true,
|
|
errMsg: "CancelError",
|
|
},
|
|
{
|
|
name: "interactive with existing repository public",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
askStubs: func(as *prompt.AskStubber) {
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne("Push an existing local repository to GitHub")
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne(".")
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "repoName", Value: "REPO"},
|
|
{Name: "repoDescription", Value: "my new repo"},
|
|
{Name: "repoVisibility", Value: "Private"},
|
|
})
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne(false)
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation RepositoryCreate\b`),
|
|
httpmock.StringResponse(`
|
|
{
|
|
"data": {
|
|
"createRepository": {
|
|
"repository": {
|
|
"id": "REPOID",
|
|
"name": "REPO",
|
|
"owner": {"login":"OWNER"},
|
|
"url": "https://github.com/OWNER/REPO"
|
|
}
|
|
}
|
|
}
|
|
}`))
|
|
},
|
|
execStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
|
|
cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
|
|
},
|
|
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n",
|
|
},
|
|
{
|
|
name: "interactive with existing repository public add remote",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
askStubs: func(as *prompt.AskStubber) {
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne("Push an existing local repository to GitHub")
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne(".")
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "repoName", Value: "REPO"},
|
|
{Name: "repoDescription", Value: "my new repo"},
|
|
{Name: "repoVisibility", Value: "Private"},
|
|
})
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne(true) //ask for adding a remote
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne("origin") //ask for remote name
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne(false) //ask to push to remote
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation RepositoryCreate\b`),
|
|
httpmock.StringResponse(`
|
|
{
|
|
"data": {
|
|
"createRepository": {
|
|
"repository": {
|
|
"id": "REPOID",
|
|
"name": "REPO",
|
|
"owner": {"login":"OWNER"},
|
|
"url": "https://github.com/OWNER/REPO"
|
|
}
|
|
}
|
|
}
|
|
}`))
|
|
},
|
|
execStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
|
|
cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
|
|
cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
|
|
},
|
|
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n",
|
|
},
|
|
{
|
|
name: "interactive with existing repository public, add remote, and push",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
askStubs: func(as *prompt.AskStubber) {
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne("Push an existing local repository to GitHub")
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne(".")
|
|
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "repoName", Value: "REPO"},
|
|
{Name: "repoDescription", Value: "my new repo"},
|
|
{Name: "repoVisibility", Value: "Private"},
|
|
})
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne(true) //ask for adding a remote
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne("origin") //ask for remote name
|
|
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
|
|
as.StubOne(true) //ask to push to remote
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation RepositoryCreate\b`),
|
|
httpmock.StringResponse(`
|
|
{
|
|
"data": {
|
|
"createRepository": {
|
|
"repository": {
|
|
"id": "REPOID",
|
|
"name": "REPO",
|
|
"owner": {"login":"OWNER"},
|
|
"url": "https://github.com/OWNER/REPO"
|
|
}
|
|
}
|
|
}
|
|
}`))
|
|
},
|
|
execStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
|
|
cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
|
|
cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
|
|
cs.Register(`git -C . push -u origin HEAD`, 0, "")
|
|
},
|
|
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n",
|
|
},
|
|
{
|
|
name: "noninteractive create from scratch",
|
|
opts: &CreateOptions{
|
|
Interactive: false,
|
|
Name: "REPO",
|
|
Visibility: "PRIVATE",
|
|
},
|
|
tty: false,
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation RepositoryCreate\b`),
|
|
httpmock.StringResponse(`
|
|
{
|
|
"data": {
|
|
"createRepository": {
|
|
"repository": {
|
|
"id": "REPOID",
|
|
"name": "REPO",
|
|
"owner": {"login":"OWNER"},
|
|
"url": "https://github.com/OWNER/REPO"
|
|
}
|
|
}
|
|
}
|
|
}`))
|
|
},
|
|
wantStdout: "https://github.com/OWNER/REPO\n",
|
|
},
|
|
{
|
|
name: "noninteractive create from source",
|
|
opts: &CreateOptions{
|
|
Interactive: false,
|
|
Source: ".",
|
|
Name: "REPO",
|
|
Visibility: "PRIVATE",
|
|
},
|
|
tty: false,
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation RepositoryCreate\b`),
|
|
httpmock.StringResponse(`
|
|
{
|
|
"data": {
|
|
"createRepository": {
|
|
"repository": {
|
|
"id": "REPOID",
|
|
"name": "REPO",
|
|
"owner": {"login":"OWNER"},
|
|
"url": "https://github.com/OWNER/REPO"
|
|
}
|
|
}
|
|
}
|
|
}`))
|
|
},
|
|
execStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
|
|
cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
|
|
cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
|
|
},
|
|
wantStdout: "https://github.com/OWNER/REPO\n",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
|
|
q, teardown := prompt.InitAskStubber()
|
|
defer teardown()
|
|
if tt.askStubs != nil {
|
|
tt.askStubs(q)
|
|
}
|
|
|
|
reg := &httpmock.Registry{}
|
|
if tt.httpStubs != nil {
|
|
tt.httpStubs(reg)
|
|
}
|
|
tt.opts.HttpClient = func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
}
|
|
tt.opts.Config = func() (config.Config, error) {
|
|
return config.NewBlankConfig(), nil
|
|
}
|
|
|
|
cs, restoreRun := run.Stub()
|
|
defer restoreRun(t)
|
|
if tt.execStubs != nil {
|
|
tt.execStubs(cs)
|
|
}
|
|
|
|
ios, _, stdout, stderr := iostreams.Test()
|
|
ios.SetStdinTTY(tt.tty)
|
|
ios.SetStdoutTTY(tt.tty)
|
|
tt.opts.IO = ios
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
defer reg.Verify(t)
|
|
err := createRun(tt.opts)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
assert.Equal(t, tt.errMsg, err.Error())
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.wantStdout, stdout.String())
|
|
assert.Equal(t, "", stderr.String())
|
|
})
|
|
}
|
|
}
|