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 }