github.com/secman-team/gh-api@v1.8.2/pkg/cmd/repo/create/create.go (about)

     1  package create
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"path"
     8  	"strings"
     9  
    10  	"github.com/AlecAivazis/survey/v2"
    11  	"github.com/MakeNowJust/heredoc"
    12  	"github.com/secman-team/gh-api/api"
    13  	"github.com/secman-team/gh-api/git"
    14  	"github.com/secman-team/gh-api/core/config"
    15  	"github.com/secman-team/gh-api/core/ghinstance"
    16  	"github.com/secman-team/gh-api/core/ghrepo"
    17  	"github.com/secman-team/gh-api/core/run"
    18  	"github.com/secman-team/gh-api/pkg/cmdutil"
    19  	"github.com/secman-team/gh-api/pkg/iostreams"
    20  	"github.com/secman-team/gh-api/pkg/prompt"
    21  	"github.com/spf13/cobra"
    22  )
    23  
    24  type CreateOptions struct {
    25  	HttpClient func() (*http.Client, error)
    26  	Config     func() (config.Config, error)
    27  	IO         *iostreams.IOStreams
    28  
    29  	Name          string
    30  	Description   string
    31  	Homepage      string
    32  	Team          string
    33  	Template      string
    34  	EnableIssues  bool
    35  	EnableWiki    bool
    36  	Public        bool
    37  	Private       bool
    38  	Internal      bool
    39  	ConfirmSubmit bool
    40  }
    41  
    42  func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
    43  	opts := &CreateOptions{
    44  		IO:         f.IOStreams,
    45  		HttpClient: f.HttpClient,
    46  		Config:     f.Config,
    47  	}
    48  
    49  	cmd := &cobra.Command{
    50  		Use:   "create [<name>]",
    51  		Short: "Create a new repository",
    52  		Long: heredoc.Docf(`
    53  			Create a new GitHub repository.
    54  
    55  			When the current directory is a local git repository, the new repository will be added
    56  			as the "origin" git remote. Otherwise, the command will prompt to clone the new
    57  			repository into a sub-directory.
    58  
    59  			To create a repository non-interactively, supply the following:
    60  			- the name argument;
    61  			- the %[1]s--confirm%[1]s flag;
    62  			- one of %[1]s--public%[1]s, %[1]s--private%[1]s, or %[1]s--internal%[1]s.
    63  
    64  			To toggle off %[1]s--enable-issues%[1]s or %[1]s--enable-wiki%[1]s, which are enabled
    65  			by default, use the %[1]s--enable-issues=false%[1]s syntax.
    66  		`, "`"),
    67  		Args: cobra.MaximumNArgs(1),
    68  		Example: heredoc.Doc(`
    69  			# create a repository under your account using the current directory name
    70  			$ git init my-project
    71  			$ cd my-project
    72  			$ gh repo create
    73  
    74  			# create a repository with a specific name
    75  			$ gh repo create my-project
    76  
    77  			# create a repository in an organization
    78  			$ gh repo create cli/my-project
    79  
    80  			# disable issues and wiki
    81  			$ gh repo create --enable-issues=false --enable-wiki=false
    82  	  `),
    83  		Annotations: map[string]string{
    84  			"help:arguments": heredoc.Doc(`
    85  				A repository can be supplied as an argument in any of the following formats:
    86  				- "OWNER/REPO"
    87  				- by URL, e.g. "https://github.com/OWNER/REPO"
    88  			`),
    89  		},
    90  		RunE: func(cmd *cobra.Command, args []string) error {
    91  			if len(args) > 0 {
    92  				opts.Name = args[0]
    93  			}
    94  
    95  			if !opts.IO.CanPrompt() {
    96  				if opts.Name == "" {
    97  					return &cmdutil.FlagError{Err: errors.New("name argument required when not running interactively")}
    98  				}
    99  
   100  				if !opts.Internal && !opts.Private && !opts.Public {
   101  					return &cmdutil.FlagError{Err: errors.New("`--public`, `--private`, or `--internal` required when not running interactively")}
   102  				}
   103  			}
   104  
   105  			if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || cmd.Flags().Changed("enable-issues") || cmd.Flags().Changed("enable-wiki")) {
   106  				return &cmdutil.FlagError{Err: errors.New("The `--template` option is not supported with `--homepage`, `--team`, `--enable-issues`, or `--enable-wiki`")}
   107  			}
   108  
   109  			if runF != nil {
   110  				return runF(opts)
   111  			}
   112  			return createRun(opts)
   113  		},
   114  	}
   115  
   116  	cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Description of the repository")
   117  	cmd.Flags().StringVarP(&opts.Homepage, "homepage", "h", "", "Repository home page `URL`")
   118  	cmd.Flags().StringVarP(&opts.Team, "team", "t", "", "The `name` of the organization team to be granted access")
   119  	cmd.Flags().StringVarP(&opts.Template, "template", "p", "", "Make the new repository based on a template `repository`")
   120  	cmd.Flags().BoolVar(&opts.EnableIssues, "enable-issues", true, "Enable issues in the new repository")
   121  	cmd.Flags().BoolVar(&opts.EnableWiki, "enable-wiki", true, "Enable wiki in the new repository")
   122  	cmd.Flags().BoolVar(&opts.Public, "public", false, "Make the new repository public")
   123  	cmd.Flags().BoolVar(&opts.Private, "private", false, "Make the new repository private")
   124  	cmd.Flags().BoolVar(&opts.Internal, "internal", false, "Make the new repository internal")
   125  	cmd.Flags().BoolVarP(&opts.ConfirmSubmit, "confirm", "y", false, "Skip the confirmation prompt")
   126  
   127  	return cmd
   128  }
   129  
   130  func createRun(opts *CreateOptions) error {
   131  	projectDir, projectDirErr := git.ToplevelDir()
   132  	isNameAnArg := false
   133  	isDescEmpty := opts.Description == ""
   134  	isVisibilityPassed := false
   135  	inLocalRepo := projectDirErr == nil
   136  
   137  	if opts.Name != "" {
   138  		isNameAnArg = true
   139  	} else {
   140  		if projectDirErr != nil {
   141  			return projectDirErr
   142  		}
   143  		opts.Name = path.Base(projectDir)
   144  	}
   145  
   146  	enabledFlagCount := 0
   147  	visibility := ""
   148  	if opts.Public {
   149  		enabledFlagCount++
   150  		visibility = "PUBLIC"
   151  	}
   152  	if opts.Private {
   153  		enabledFlagCount++
   154  		visibility = "PRIVATE"
   155  	}
   156  	if opts.Internal {
   157  		enabledFlagCount++
   158  		visibility = "INTERNAL"
   159  	}
   160  
   161  	if enabledFlagCount > 1 {
   162  		return fmt.Errorf("expected exactly one of `--public`, `--private`, or `--internal` to be true")
   163  	} else if enabledFlagCount == 1 {
   164  		isVisibilityPassed = true
   165  	}
   166  
   167  	// Trigger interactive prompt if name is not passed
   168  	if !isNameAnArg {
   169  		newName, newDesc, newVisibility, err := interactiveRepoCreate(isDescEmpty, isVisibilityPassed, opts.Name)
   170  		if err != nil {
   171  			return err
   172  		}
   173  		if newName != "" {
   174  			opts.Name = newName
   175  		}
   176  		if newDesc != "" {
   177  			opts.Description = newDesc
   178  		}
   179  		if newVisibility != "" {
   180  			visibility = newVisibility
   181  		}
   182  	} else {
   183  		// Go for a prompt only if visibility isn't passed
   184  		if !isVisibilityPassed {
   185  			newVisibility, err := getVisibility()
   186  			if err != nil {
   187  				return nil
   188  			}
   189  			visibility = newVisibility
   190  		}
   191  	}
   192  
   193  	cfg, err := opts.Config()
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	var repoToCreate ghrepo.Interface
   199  
   200  	if strings.Contains(opts.Name, "/") {
   201  		var err error
   202  		repoToCreate, err = ghrepo.FromFullName(opts.Name)
   203  		if err != nil {
   204  			return fmt.Errorf("argument error: %w", err)
   205  		}
   206  	} else {
   207  		host, err := cfg.DefaultHost()
   208  		if err != nil {
   209  			return err
   210  		}
   211  		repoToCreate = ghrepo.NewWithHost("", opts.Name, host)
   212  	}
   213  
   214  	var templateRepoMainBranch string
   215  	// Find template repo ID
   216  	if opts.Template != "" {
   217  		httpClient, err := opts.HttpClient()
   218  		if err != nil {
   219  			return err
   220  		}
   221  
   222  		var toClone ghrepo.Interface
   223  		apiClient := api.NewClientFromHTTP(httpClient)
   224  
   225  		cloneURL := opts.Template
   226  		if !strings.Contains(cloneURL, "/") {
   227  			currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default())
   228  			if err != nil {
   229  				return err
   230  			}
   231  			cloneURL = currentUser + "/" + cloneURL
   232  		}
   233  		toClone, err = ghrepo.FromFullName(cloneURL)
   234  		if err != nil {
   235  			return fmt.Errorf("argument error: %w", err)
   236  		}
   237  
   238  		repo, err := api.GitHubRepo(apiClient, toClone)
   239  		if err != nil {
   240  			return err
   241  		}
   242  
   243  		opts.Template = repo.ID
   244  		templateRepoMainBranch = repo.DefaultBranchRef.Name
   245  	}
   246  
   247  	input := repoCreateInput{
   248  		Name:             repoToCreate.RepoName(),
   249  		Visibility:       visibility,
   250  		OwnerID:          repoToCreate.RepoOwner(),
   251  		TeamID:           opts.Team,
   252  		Description:      opts.Description,
   253  		HomepageURL:      opts.Homepage,
   254  		HasIssuesEnabled: opts.EnableIssues,
   255  		HasWikiEnabled:   opts.EnableWiki,
   256  	}
   257  
   258  	httpClient, err := opts.HttpClient()
   259  	if err != nil {
   260  		return err
   261  	}
   262  
   263  	createLocalDirectory := opts.ConfirmSubmit
   264  	if !opts.ConfirmSubmit {
   265  		opts.ConfirmSubmit, err = confirmSubmission(input.Name, input.OwnerID, inLocalRepo)
   266  		if err != nil {
   267  			return err
   268  		}
   269  	}
   270  
   271  	if opts.ConfirmSubmit {
   272  		repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input, opts.Template)
   273  		if err != nil {
   274  			return err
   275  		}
   276  
   277  		stderr := opts.IO.ErrOut
   278  		stdout := opts.IO.Out
   279  		cs := opts.IO.ColorScheme()
   280  		isTTY := opts.IO.IsStdoutTTY()
   281  
   282  		if isTTY {
   283  			fmt.Fprintf(stderr, "%s Created repository %s on GitHub\n", cs.SuccessIconWithColor(cs.Green), ghrepo.FullName(repo))
   284  		} else {
   285  			fmt.Fprintln(stdout, repo.URL)
   286  		}
   287  
   288  		protocol, err := cfg.Get(repo.RepoHost(), "git_protocol")
   289  		if err != nil {
   290  			return err
   291  		}
   292  		remoteURL := ghrepo.FormatRemoteURL(repo, protocol)
   293  
   294  		if inLocalRepo {
   295  			_, err = git.AddRemote("origin", remoteURL)
   296  			if err != nil {
   297  				return err
   298  			}
   299  			if isTTY {
   300  				fmt.Fprintf(stderr, "%s Added remote %s\n", cs.SuccessIcon(), remoteURL)
   301  			}
   302  		} else {
   303  			if opts.IO.CanPrompt() {
   304  				if !createLocalDirectory {
   305  					err := prompt.Confirm(fmt.Sprintf(`Create a local project directory for "%s"?`, ghrepo.FullName(repo)), &createLocalDirectory)
   306  					if err != nil {
   307  						return err
   308  					}
   309  				}
   310  			}
   311  			if createLocalDirectory {
   312  				path := repo.Name
   313  				checkoutBranch := ""
   314  				if opts.Template != "" {
   315  					// NOTE: we cannot read `defaultBranchRef` from the newly created repository as it will
   316  					// be null at this time. Instead, we assume that the main branch name of the new
   317  					// repository will be the same as that of the template repository.
   318  					checkoutBranch = templateRepoMainBranch
   319  				}
   320  				if err := localInit(opts.IO, remoteURL, path, checkoutBranch); err != nil {
   321  					return err
   322  				}
   323  				if isTTY {
   324  					fmt.Fprintf(stderr, "%s Initialized repository in \"%s\"\n", cs.SuccessIcon(), path)
   325  				}
   326  			}
   327  		}
   328  
   329  		return nil
   330  	}
   331  	fmt.Fprintln(opts.IO.Out, "Discarding...")
   332  	return nil
   333  }
   334  
   335  func localInit(io *iostreams.IOStreams, remoteURL, path, checkoutBranch string) error {
   336  	gitInit, err := git.GitCommand("init", path)
   337  	if err != nil {
   338  		return err
   339  	}
   340  	isTTY := io.IsStdoutTTY()
   341  	if isTTY {
   342  		gitInit.Stdout = io.Out
   343  	}
   344  	gitInit.Stderr = io.ErrOut
   345  	err = run.PrepareCmd(gitInit).Run()
   346  	if err != nil {
   347  		return err
   348  	}
   349  
   350  	gitRemoteAdd, err := git.GitCommand("-C", path, "remote", "add", "origin", remoteURL)
   351  	if err != nil {
   352  		return err
   353  	}
   354  	gitRemoteAdd.Stdout = io.Out
   355  	gitRemoteAdd.Stderr = io.ErrOut
   356  	err = run.PrepareCmd(gitRemoteAdd).Run()
   357  	if err != nil {
   358  		return err
   359  	}
   360  
   361  	if checkoutBranch == "" {
   362  		return nil
   363  	}
   364  
   365  	gitFetch, err := git.GitCommand("-C", path, "fetch", "origin", fmt.Sprintf("+refs/heads/%[1]s:refs/remotes/origin/%[1]s", checkoutBranch))
   366  	if err != nil {
   367  		return err
   368  	}
   369  	gitFetch.Stdout = io.Out
   370  	gitFetch.Stderr = io.ErrOut
   371  	err = run.PrepareCmd(gitFetch).Run()
   372  	if err != nil {
   373  		return err
   374  	}
   375  
   376  	gitCheckout, err := git.GitCommand("-C", path, "checkout", checkoutBranch)
   377  	if err != nil {
   378  		return err
   379  	}
   380  	gitCheckout.Stdout = io.Out
   381  	gitCheckout.Stderr = io.ErrOut
   382  	return run.PrepareCmd(gitCheckout).Run()
   383  }
   384  
   385  func interactiveRepoCreate(isDescEmpty bool, isVisibilityPassed bool, repoName string) (string, string, string, error) {
   386  	qs := []*survey.Question{}
   387  
   388  	repoNameQuestion := &survey.Question{
   389  		Name: "repoName",
   390  		Prompt: &survey.Input{
   391  			Message: "Repository name",
   392  			Default: repoName,
   393  		},
   394  	}
   395  	qs = append(qs, repoNameQuestion)
   396  
   397  	if isDescEmpty {
   398  		repoDescriptionQuestion := &survey.Question{
   399  			Name: "repoDescription",
   400  			Prompt: &survey.Input{
   401  				Message: "Repository description",
   402  			},
   403  		}
   404  
   405  		qs = append(qs, repoDescriptionQuestion)
   406  	}
   407  
   408  	if !isVisibilityPassed {
   409  		repoVisibilityQuestion := &survey.Question{
   410  			Name: "repoVisibility",
   411  			Prompt: &survey.Select{
   412  				Message: "Visibility",
   413  				Options: []string{"Public", "Private", "Internal"},
   414  			},
   415  		}
   416  		qs = append(qs, repoVisibilityQuestion)
   417  	}
   418  
   419  	answers := struct {
   420  		RepoName        string
   421  		RepoDescription string
   422  		RepoVisibility  string
   423  	}{}
   424  
   425  	err := prompt.SurveyAsk(qs, &answers)
   426  
   427  	if err != nil {
   428  		return "", "", "", err
   429  	}
   430  
   431  	return answers.RepoName, answers.RepoDescription, strings.ToUpper(answers.RepoVisibility), nil
   432  }
   433  
   434  func confirmSubmission(repoName string, repoOwner string, inLocalRepo bool) (bool, error) {
   435  	qs := []*survey.Question{}
   436  
   437  	promptString := ""
   438  	if inLocalRepo {
   439  		promptString = `This will add an "origin" git remote to your local repository. Continue?`
   440  	} else {
   441  		targetRepo := repoName
   442  		if repoOwner != "" {
   443  			targetRepo = fmt.Sprintf("%s/%s", repoOwner, repoName)
   444  		}
   445  		promptString = fmt.Sprintf(`This will create the "%s" repository on GitHub. Continue?`, targetRepo)
   446  	}
   447  
   448  	confirmSubmitQuestion := &survey.Question{
   449  		Name: "confirmSubmit",
   450  		Prompt: &survey.Confirm{
   451  			Message: promptString,
   452  			Default: true,
   453  		},
   454  	}
   455  	qs = append(qs, confirmSubmitQuestion)
   456  
   457  	answer := struct {
   458  		ConfirmSubmit bool
   459  	}{}
   460  
   461  	err := prompt.SurveyAsk(qs, &answer)
   462  	if err != nil {
   463  		return false, err
   464  	}
   465  
   466  	return answer.ConfirmSubmit, nil
   467  }
   468  
   469  func getVisibility() (string, error) {
   470  	qs := []*survey.Question{}
   471  
   472  	getVisibilityQuestion := &survey.Question{
   473  		Name: "repoVisibility",
   474  		Prompt: &survey.Select{
   475  			Message: "Visibility",
   476  			Options: []string{"Public", "Private", "Internal"},
   477  		},
   478  	}
   479  	qs = append(qs, getVisibilityQuestion)
   480  
   481  	answer := struct {
   482  		RepoVisibility string
   483  	}{}
   484  
   485  	err := prompt.SurveyAsk(qs, &answer)
   486  	if err != nil {
   487  		return "", err
   488  	}
   489  
   490  	return strings.ToUpper(answer.RepoVisibility), nil
   491  }