Prompt for owner when interactively creating repos (#6578)

This commit is contained in:
Skip Baney 2022-12-08 00:52:36 -06:00 committed by GitHub
parent 33779630da
commit 06360429de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 174 additions and 6 deletions

View file

@ -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 {

View file

@ -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,

View file

@ -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(`

View file

@ -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)
}