diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index f7190e7e7..7628184e5 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -6,6 +6,7 @@ import ( "path" "strings" + "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/git" "github.com/cli/cli/internal/config" @@ -23,13 +24,16 @@ type CreateOptions struct { Config func() (config.Config, error) IO *iostreams.IOStreams - Name string - Description string - Homepage string - Team string - EnableIssues bool - EnableWiki bool - Public bool + Name string + Description string + Homepage string + Team string + EnableIssues bool + EnableWiki bool + Public bool + Private bool + Internal bool + ConfirmSubmit bool } func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { @@ -78,7 +82,10 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co cmd.Flags().StringVarP(&opts.Team, "team", "t", "", "The name of the organization team to be granted access") cmd.Flags().BoolVar(&opts.EnableIssues, "enable-issues", true, "Enable issues in the new repository") cmd.Flags().BoolVar(&opts.EnableWiki, "enable-wiki", true, "Enable wiki in the new repository") - cmd.Flags().BoolVar(&opts.Public, "public", false, "Make the new repository public (default: private)") + cmd.Flags().BoolVar(&opts.Public, "public", false, "Make the new repository public") + cmd.Flags().BoolVar(&opts.Private, "private", false, "Make the new repository private") + cmd.Flags().BoolVar(&opts.Internal, "internal", false, "Make the new repository internal") + cmd.Flags().BoolVarP(&opts.ConfirmSubmit, "confirm", "y", false, "Confirm the submission directly") return cmd } @@ -88,7 +95,12 @@ func createRun(opts *CreateOptions) error { var repoToCreate ghrepo.Interface + isNameAnArg := false + isDescEmpty := opts.Description == "" + isVisibilityPassed := false + if opts.Name != "" { + isNameAnArg = true if strings.Contains(opts.Name, "/") { var err error repoToCreate, err = ghrepo.FromFullName(opts.Name) @@ -105,10 +117,52 @@ func createRun(opts *CreateOptions) error { repoToCreate = ghrepo.New("", path.Base(projectDir)) } - visibility := "PRIVATE" + enabledFlagCount := 0 + visibility := "" if opts.Public { + enabledFlagCount++ visibility = "PUBLIC" } + if opts.Private { + enabledFlagCount++ + visibility = "PRIVATE" + } + if opts.Internal { + enabledFlagCount++ + visibility = "INTERNAL" + } + + if enabledFlagCount > 1 { + return fmt.Errorf("expected exactly one of --public, --private, or --internal to be true") + } else if enabledFlagCount == 1 { + isVisibilityPassed = true + } + + // Trigger interactive prompt if name is not passed + if !isNameAnArg { + newName, newDesc, newVisibility, err := interactiveRepoCreate(isDescEmpty, isVisibilityPassed, repoToCreate.RepoName()) + if err != nil { + return err + } + if newName != "" { + opts.Name = newName + } + if newDesc != "" { + opts.Description = newDesc + } + if newVisibility != "" { + visibility = newVisibility + } + } else { + // Go for a prompt only if visibility isn't passed + if !isVisibilityPassed { + newVisibility, err := getVisibility() + if err != nil { + return nil + } + visibility = newVisibility + } + } input := repoCreateInput{ Name: repoToCreate.RepoName(), @@ -126,67 +180,186 @@ func createRun(opts *CreateOptions) error { return err } - repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input) - if err != nil { - return err - } - - stderr := opts.IO.ErrOut - stdout := opts.IO.Out - isTTY := opts.IO.IsStdoutTTY() - - if isTTY { - fmt.Fprintf(stderr, "%s Created repository %s on GitHub\n", utils.GreenCheck(), ghrepo.FullName(repo)) - } else { - fmt.Fprintln(stdout, repo.URL) - } - - // TODO This is overly wordy and I'd like to streamline this. - cfg, err := opts.Config() - if err != nil { - return err - } - protocol, err := cfg.Get(repo.RepoHost(), "git_protocol") - if err != nil { - return err - } - remoteURL := ghrepo.FormatRemoteURL(repo, protocol) - - if projectDirErr == nil { - _, err = git.AddRemote("origin", remoteURL) + createLocalDirectory := opts.ConfirmSubmit + if !opts.ConfirmSubmit { + opts.ConfirmSubmit, err = confirmSubmission(input.Name, input.OwnerID, &opts.ConfirmSubmit) if err != nil { return err } + } + + if opts.ConfirmSubmit { + repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input) + if err != nil { + return err + } + + stderr := opts.IO.ErrOut + stdout := opts.IO.Out + greenCheck := utils.Green("✓") + isTTY := opts.IO.IsStdoutTTY() + if isTTY { - fmt.Fprintf(stderr, "%s Added remote %s\n", utils.GreenCheck(), remoteURL) + fmt.Fprintf(stderr, "%s Created repository %s on GitHub\n", greenCheck, ghrepo.FullName(repo)) + } else { + fmt.Fprintln(stdout, repo.URL) } - } else if isTTY { - doSetup := false - err := prompt.Confirm(fmt.Sprintf("Create a local project directory for %s?", ghrepo.FullName(repo)), &doSetup) + + // TODO This is overly wordy and I'd like to streamline this. + cfg, err := opts.Config() if err != nil { return err } - - if doSetup { - path := repo.Name - - gitInit := git.GitCommand("init", path) - gitInit.Stdout = stdout - gitInit.Stderr = stderr - err = run.PrepareCmd(gitInit).Run() - if err != nil { - return err - } - gitRemoteAdd := git.GitCommand("-C", path, "remote", "add", "origin", remoteURL) - gitRemoteAdd.Stdout = stdout - gitRemoteAdd.Stderr = stderr - err = run.PrepareCmd(gitRemoteAdd).Run() - if err != nil { - return err - } - - fmt.Fprintf(stderr, "%s Initialized repository in './%s/'\n", utils.GreenCheck(), path) + protocol, err := cfg.Get(repo.RepoHost(), "git_protocol") + if err != nil { + return err } + remoteURL := ghrepo.FormatRemoteURL(repo, protocol) + + if projectDirErr == nil { + _, err = git.AddRemote("origin", remoteURL) + if err != nil { + return err + } + if isTTY { + fmt.Fprintf(stderr, "%s Added remote %s\n", greenCheck, remoteURL) + } + } else if isTTY { + doSetup := createLocalDirectory + if !doSetup { + err := prompt.Confirm(fmt.Sprintf("Create a local project directory for %s?", ghrepo.FullName(repo)), &doSetup) + if err != nil { + return err + } + } + if doSetup { + path := repo.Name + + gitInit := git.GitCommand("init", path) + gitInit.Stdout = stdout + gitInit.Stderr = stderr + err = run.PrepareCmd(gitInit).Run() + if err != nil { + return err + } + gitRemoteAdd := git.GitCommand("-C", path, "remote", "add", "origin", remoteURL) + gitRemoteAdd.Stdout = stdout + gitRemoteAdd.Stderr = stderr + err = run.PrepareCmd(gitRemoteAdd).Run() + if err != nil { + return err + } + + fmt.Fprintf(stderr, "%s Initialized repository in './%s/'\n", utils.GreenCheck(), path) + } + } + return nil } + fmt.Fprintln(opts.IO.Out, "Discarding...") return nil } + +func interactiveRepoCreate(isDescEmpty bool, isVisibilityPassed bool, repoName string) (string, string, string, error) { + qs := []*survey.Question{} + + repoOwnerQuestion := &survey.Question{ + Name: "repoOwner", + Prompt: &survey.Input{ + Message: "Repository name", + Default: repoName, + }, + } + qs = append(qs, repoOwnerQuestion) + + if isDescEmpty { + repoDescriptionQuestion := &survey.Question{ + Name: "repoDescription", + Prompt: &survey.Input{ + Message: "Repository description", + }, + } + + qs = append(qs, repoDescriptionQuestion) + } + + if !isVisibilityPassed { + repoVisibilityQuestion := &survey.Question{ + Name: "repoVisibility", + Prompt: &survey.Select{ + Message: "Visibility", + Options: []string{"Public", "Private", "Internal"}, + }, + } + qs = append(qs, repoVisibilityQuestion) + } + + answers := struct { + RepoOwner string + RepoDescription string + RepoVisibility string + }{} + + err := prompt.SurveyAsk(qs, &answers) + + if err != nil { + return "", "", "", err + } + + return answers.RepoOwner, answers.RepoDescription, strings.ToUpper(answers.RepoVisibility), nil + +} + +func confirmSubmission(repoName string, repoOwner string, isConfirmFlagPassed *bool) (bool, error) { + qs := []*survey.Question{} + + promptString := "" + if repoOwner != "" { + promptString = fmt.Sprintf("This will create '%s/%s' in your current directory. Continue? ", repoOwner, repoName) + } else { + promptString = fmt.Sprintf("This will create '%s' in your current directory. Continue? ", repoName) + } + + confirmSubmitQuestion := &survey.Question{ + Name: "confirmSubmit", + Prompt: &survey.Confirm{ + Message: promptString, + Default: true, + }, + } + qs = append(qs, confirmSubmitQuestion) + + answer := struct { + ConfirmSubmit bool + }{} + + err := prompt.SurveyAsk(qs, &answer) + if err != nil { + return false, err + } + + return answer.ConfirmSubmit, nil +} + +func getVisibility() (string, error) { + qs := []*survey.Question{} + + getVisibilityQuestion := &survey.Question{ + Name: "repoVisibility", + Prompt: &survey.Select{ + Message: "Visibility", + Options: []string{"Public", "Private", "Internal"}, + }, + } + qs = append(qs, getVisibilityQuestion) + + answer := struct { + RepoVisibility string + }{} + + err := prompt.SurveyAsk(qs, &answer) + if err != nil { + return "", err + } + + return strings.ToUpper(answer.RepoVisibility), nil +} diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go index d33ad5d25..25f5519ab 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -14,6 +14,7 @@ import ( "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/httpmock" "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/pkg/prompt" "github.com/cli/cli/test" "github.com/google/shlex" "github.com/stretchr/testify/assert" @@ -87,6 +88,22 @@ func TestRepoCreate(t *testing.T) { }) defer restoreCmd() + as, surveyTearDown := prompt.InitAskStubber() + defer surveyTearDown() + + as.Stub([]*prompt.QuestionStub{ + { + Name: "repoVisibility", + Value: "PRIVATE", + }, + }) + as.Stub([]*prompt.QuestionStub{ + { + Name: "confirmSubmit", + Value: true, + }, + }) + output, err := runCommand(httpClient, "REPO") if err != nil { t.Errorf("error running command `repo create`: %v", err) @@ -153,6 +170,22 @@ func TestRepoCreate_org(t *testing.T) { }) defer restoreCmd() + as, surveyTearDown := prompt.InitAskStubber() + defer surveyTearDown() + + as.Stub([]*prompt.QuestionStub{ + { + Name: "repoVisibility", + Value: "PRIVATE", + }, + }) + as.Stub([]*prompt.QuestionStub{ + { + Name: "confirmSubmit", + Value: true, + }, + }) + output, err := runCommand(httpClient, "ORG/REPO") if err != nil { t.Errorf("error running command `repo create`: %v", err) @@ -219,6 +252,22 @@ func TestRepoCreate_orgWithTeam(t *testing.T) { }) defer restoreCmd() + as, surveyTearDown := prompt.InitAskStubber() + defer surveyTearDown() + + as.Stub([]*prompt.QuestionStub{ + { + Name: "repoVisibility", + Value: "PRIVATE", + }, + }) + as.Stub([]*prompt.QuestionStub{ + { + Name: "confirmSubmit", + Value: true, + }, + }) + output, err := runCommand(httpClient, "ORG/REPO --team monkeys") if err != nil { t.Errorf("error running command `repo create`: %v", err)