Prompt for owner when interactively creating repos (#6578)
This commit is contained in:
parent
33779630da
commit
06360429de
4 changed files with 174 additions and 6 deletions
|
|
@ -1,5 +1,9 @@
|
|||
package api
|
||||
|
||||
type Organization struct {
|
||||
Login string
|
||||
}
|
||||
|
||||
func CurrentLoginName(client *Client, hostname string) (string, error) {
|
||||
var query struct {
|
||||
Viewer struct {
|
||||
|
|
@ -10,6 +14,26 @@ func CurrentLoginName(client *Client, hostname string) (string, error) {
|
|||
return query.Viewer.Login, err
|
||||
}
|
||||
|
||||
func CurrentLoginNameAndOrgs(client *Client, hostname string) (string, []string, error) {
|
||||
var query struct {
|
||||
Viewer struct {
|
||||
Login string
|
||||
Organizations struct {
|
||||
Nodes []Organization
|
||||
} `graphql:"organizations(first: 100)"`
|
||||
}
|
||||
}
|
||||
err := client.Query(hostname, "UserCurrent", &query, nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
orgNames := []string{}
|
||||
for _, org := range query.Viewer.Organizations.Nodes {
|
||||
orgNames = append(orgNames, org.Login)
|
||||
}
|
||||
return query.Viewer.Login, orgNames, err
|
||||
}
|
||||
|
||||
func CurrentUserID(client *Client, hostname string) (string, error) {
|
||||
var query struct {
|
||||
Viewer struct {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
|
|
@ -273,7 +274,7 @@ func createFromScratch(opts *CreateOptions) error {
|
|||
host, _ := cfg.DefaultHost()
|
||||
|
||||
if opts.Interactive {
|
||||
opts.Name, opts.Description, opts.Visibility, err = interactiveRepoInfo(opts.Prompter, "")
|
||||
opts.Name, opts.Description, opts.Visibility, err = interactiveRepoInfo(httpClient, host, opts.Prompter, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -291,8 +292,8 @@ func createFromScratch(opts *CreateOptions) error {
|
|||
}
|
||||
|
||||
targetRepo := shared.NormalizeRepoName(opts.Name)
|
||||
if idx := strings.IndexRune(targetRepo, '/'); idx > 0 {
|
||||
targetRepo = targetRepo[0:idx+1] + shared.NormalizeRepoName(targetRepo[idx+1:])
|
||||
if idx := strings.IndexRune(opts.Name, '/'); idx > 0 {
|
||||
targetRepo = opts.Name[0:idx+1] + shared.NormalizeRepoName(opts.Name[idx+1:])
|
||||
}
|
||||
confirmed, err := opts.Prompter.Confirm(fmt.Sprintf(`This will create "%s" as a %s repository on GitHub. Continue?`, targetRepo, strings.ToLower(opts.Visibility)), true)
|
||||
if err != nil {
|
||||
|
|
@ -467,7 +468,7 @@ func createFromLocal(opts *CreateOptions) error {
|
|||
}
|
||||
|
||||
if opts.Interactive {
|
||||
opts.Name, opts.Description, opts.Visibility, err = interactiveRepoInfo(opts.Prompter, filepath.Base(absPath))
|
||||
opts.Name, opts.Description, opts.Visibility, err = interactiveRepoInfo(httpClient, host, opts.Prompter, filepath.Base(absPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -699,11 +700,14 @@ func interactiveLicense(client *http.Client, hostname string, prompter iprompter
|
|||
}
|
||||
|
||||
// name, description, and visibility
|
||||
func interactiveRepoInfo(prompter iprompter, defaultName string) (string, string, string, error) {
|
||||
name, err := prompter.Input("Repository name", defaultName)
|
||||
func interactiveRepoInfo(client *http.Client, hostname string, prompter iprompter, defaultName string) (string, string, string, error) {
|
||||
name, owner, err := interactiveRepoNameAndOwner(client, hostname, prompter, defaultName)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
if owner != "" {
|
||||
name = fmt.Sprintf("%s/%s", owner, name)
|
||||
}
|
||||
|
||||
description, err := prompter.Input("Description", defaultName)
|
||||
if err != nil {
|
||||
|
|
@ -719,6 +723,57 @@ func interactiveRepoInfo(prompter iprompter, defaultName string) (string, string
|
|||
return name, description, strings.ToUpper(visibilityOptions[selected]), nil
|
||||
}
|
||||
|
||||
func interactiveRepoNameAndOwner(client *http.Client, hostname string, prompter iprompter, defaultName string) (string, string, error) {
|
||||
name, err := prompter.Input("Repository name", defaultName)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
name, owner, err := splitNameAndOwner(name)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if owner != "" {
|
||||
// User supplied an explicit owner prefix.
|
||||
return name, owner, nil
|
||||
}
|
||||
|
||||
username, orgs, err := userAndOrgs(client, hostname)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if len(orgs) == 0 {
|
||||
// User doesn't belong to any orgs.
|
||||
// Leave the owner blank to indicate a personal repo.
|
||||
return name, "", nil
|
||||
}
|
||||
|
||||
owners := append(orgs, username)
|
||||
sort.Strings(owners)
|
||||
selected, err := prompter.Select("Repository owner", username, owners)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
owner = owners[selected]
|
||||
if owner == username {
|
||||
// Leave the owner blank to indicate a personal repo.
|
||||
return name, "", nil
|
||||
}
|
||||
return name, owner, nil
|
||||
}
|
||||
|
||||
func splitNameAndOwner(name string) (string, string, error) {
|
||||
if !strings.Contains(name, "/") {
|
||||
return name, "", nil
|
||||
}
|
||||
repo, err := ghrepo.FromFullName(name)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("argument error: %w", err)
|
||||
}
|
||||
return repo.RepoName(), repo.RepoOwner(), nil
|
||||
}
|
||||
|
||||
func cloneGitClient(c *git.Client) *git.Client {
|
||||
return &git.Client{
|
||||
GhPath: c.GhPath,
|
||||
|
|
|
|||
|
|
@ -231,6 +231,9 @@ func Test_createRun(t *testing.T) {
|
|||
}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "gitignore/templates"),
|
||||
httpmock.StringResponse(`["Actionscript","Android","AppceleratorTitanium","Autotools","Bancha","C","C++","Go"]`))
|
||||
|
|
@ -246,6 +249,75 @@ func Test_createRun(t *testing.T) {
|
|||
cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "interactive create from scratch but with prompted owner",
|
||||
opts: &CreateOptions{Interactive: true},
|
||||
tty: true,
|
||||
wantStdout: "✓ Created repository org1/REPO on GitHub\n",
|
||||
promptStubs: func(p *prompter.PrompterMock) {
|
||||
p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
|
||||
switch message {
|
||||
case "Would you like to add a README file?":
|
||||
return false, nil
|
||||
case "Would you like to add a .gitignore?":
|
||||
return false, nil
|
||||
case "Would you like to add a license?":
|
||||
return false, nil
|
||||
case `This will create "org1/REPO" as a private repository on GitHub. Continue?`:
|
||||
return true, nil
|
||||
case "Clone the new repository locally?":
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected confirm prompt: %s", message)
|
||||
}
|
||||
}
|
||||
p.InputFunc = func(message, defaultValue string) (string, error) {
|
||||
switch message {
|
||||
case "Repository name":
|
||||
return "REPO", nil
|
||||
case "Description":
|
||||
return "my new repo", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unexpected input prompt: %s", message)
|
||||
}
|
||||
}
|
||||
p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
|
||||
switch message {
|
||||
case "Repository owner":
|
||||
return prompter.IndexFor(options, "org1")
|
||||
case "What would you like to do?":
|
||||
return prompter.IndexFor(options, "Create a new repository on GitHub from scratch")
|
||||
case "Visibility":
|
||||
return prompter.IndexFor(options, "Private")
|
||||
default:
|
||||
return 0, fmt.Errorf("unexpected select prompt: %s", message)
|
||||
}
|
||||
}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": [{"login": "org1"}, {"login": "org2"}]}}}}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "users/org1"),
|
||||
httpmock.StringResponse(`{"login":"org1","type":"Organization"}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation RepositoryCreate\b`),
|
||||
httpmock.StringResponse(`
|
||||
{
|
||||
"data": {
|
||||
"createRepository": {
|
||||
"repository": {
|
||||
"id": "REPOID",
|
||||
"name": "REPO",
|
||||
"owner": {"login":"org1"},
|
||||
"url": "https://github.com/org1/REPO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "interactive create from scratch but cancel before submit",
|
||||
opts: &CreateOptions{Interactive: true},
|
||||
|
|
@ -286,6 +358,11 @@ func Test_createRun(t *testing.T) {
|
|||
}
|
||||
}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`))
|
||||
},
|
||||
wantStdout: "",
|
||||
wantErr: true,
|
||||
errMsg: "CancelError",
|
||||
|
|
@ -327,6 +404,9 @@ func Test_createRun(t *testing.T) {
|
|||
}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation RepositoryCreate\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -390,6 +470,9 @@ func Test_createRun(t *testing.T) {
|
|||
}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation RepositoryCreate\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
|
|||
|
|
@ -257,3 +257,9 @@ func listLicenseTemplates(httpClient *http.Client, hostname string) ([]api.Licen
|
|||
}
|
||||
return licenseTemplates, nil
|
||||
}
|
||||
|
||||
// Returns the current username and any orgs that user is a member of.
|
||||
func userAndOrgs(httpClient *http.Client, hostname string) (string, []string, error) {
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
return api.CurrentLoginNameAndOrgs(client, hostname)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue