package create import ( "errors" "fmt" "net/http" "path" "strings" "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/api" "github.com/cli/cli/git" "github.com/cli/cli/internal/config" "github.com/cli/cli/internal/ghinstance" "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/internal/run" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/prompt" "github.com/cli/cli/utils" "github.com/spf13/cobra" ) type CreateOptions struct { HttpClient func() (*http.Client, error) Config func() (config.Config, error) IO *iostreams.IOStreams Name string Description string Homepage string Team string Template string EnableIssues bool EnableWiki bool Public bool Private bool Internal bool ConfirmSubmit bool } func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { opts := &CreateOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, Config: f.Config, } cmd := &cobra.Command{ Use: "create []", Short: "Create a new repository", Long: `Create a new GitHub repository.`, Args: cobra.MaximumNArgs(1), Example: heredoc.Doc(` # create a repository under your account using the current directory name $ gh repo create # create a repository with a specific name $ gh repo create my-project # create a repository in an organization $ gh repo create cli/my-project `), Annotations: map[string]string{ "help:arguments": heredoc.Doc( `A repository can be supplied as an argument in any of the following formats: - - by URL, e.g. "https://github.com/OWNER/REPO"`), }, RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { opts.Name = args[0] } if !opts.IO.CanPrompt() { if opts.Name == "" { return &cmdutil.FlagError{Err: errors.New("name argument required when not running interactively")} } if !opts.Internal && !opts.Private && !opts.Public { return &cmdutil.FlagError{Err: errors.New("--public, --private, or --internal required when not running interactively")} } } if runF != nil { return runF(opts) } if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || !opts.EnableIssues || !opts.EnableWiki) { return &cmdutil.FlagError{Err: errors.New(`The '--template' option is not supported with '--homepage, --team, --enable-issues or --enable-wiki'`)} } return createRun(opts) }, } cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Description of repository") cmd.Flags().StringVarP(&opts.Homepage, "homepage", "h", "", "Repository home page URL") cmd.Flags().StringVarP(&opts.Team, "team", "t", "", "The name of the organization team to be granted access") cmd.Flags().StringVarP(&opts.Template, "template", "p", "", "Make the new repository based on a template repository") 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") 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 } func createRun(opts *CreateOptions) error { projectDir, projectDirErr := git.ToplevelDir() isNameAnArg := false isDescEmpty := opts.Description == "" isVisibilityPassed := false if opts.Name != "" { isNameAnArg = true } else { if projectDirErr != nil { return projectDirErr } opts.Name = path.Base(projectDir) } 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, opts.Name) 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 } } var repoToCreate ghrepo.Interface if strings.Contains(opts.Name, "/") { var err error repoToCreate, err = ghrepo.FromFullName(opts.Name) if err != nil { return fmt.Errorf("argument error: %w", err) } } else { repoToCreate = ghrepo.New("", opts.Name) } // Find template repo ID if opts.Template != "" { httpClient, err := opts.HttpClient() if err != nil { return err } var toClone ghrepo.Interface apiClient := api.NewClientFromHTTP(httpClient) cloneURL := opts.Template if !strings.Contains(cloneURL, "/") { currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default()) if err != nil { return err } cloneURL = currentUser + "/" + cloneURL } toClone, err = ghrepo.FromFullName(cloneURL) if err != nil { return fmt.Errorf("argument error: %w", err) } repo, err := api.GitHubRepo(apiClient, toClone) if err != nil { return err } opts.Template = repo.ID } input := repoCreateInput{ Name: repoToCreate.RepoName(), Visibility: visibility, OwnerID: repoToCreate.RepoOwner(), TeamID: opts.Team, Description: opts.Description, HomepageURL: opts.Homepage, HasIssuesEnabled: opts.EnableIssues, HasWikiEnabled: opts.EnableWiki, } httpClient, err := opts.HttpClient() if err != nil { return err } 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, opts.Template) 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 Created repository %s on GitHub\n", 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) if err != nil { return err } if isTTY { fmt.Fprintf(stderr, "%s Added remote %s\n", greenCheck, remoteURL) } } else if opts.IO.CanPrompt() { 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{} repoNameQuestion := &survey.Question{ Name: "repoName", Prompt: &survey.Input{ Message: "Repository name", Default: repoName, }, } qs = append(qs, repoNameQuestion) 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 { RepoName string RepoDescription string RepoVisibility string }{} err := prompt.SurveyAsk(qs, &answers) if err != nil { return "", "", "", err } return answers.RepoName, 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 }