github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/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/cli/cli/api" 13 "github.com/cli/cli/git" 14 "github.com/cli/cli/internal/config" 15 "github.com/cli/cli/internal/ghinstance" 16 "github.com/cli/cli/internal/ghrepo" 17 "github.com/cli/cli/internal/run" 18 "github.com/cli/cli/pkg/cmdutil" 19 "github.com/cli/cli/pkg/iostreams" 20 "github.com/cli/cli/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 GitIgnoreTemplate string 41 LicenseTemplate string 42 } 43 44 func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { 45 opts := &CreateOptions{ 46 IO: f.IOStreams, 47 HttpClient: f.HttpClient, 48 Config: f.Config, 49 } 50 51 cmd := &cobra.Command{ 52 Use: "create [<name>]", 53 Short: "Create a new repository", 54 Long: heredoc.Docf(` 55 Create a new GitHub repository. 56 57 When the current directory is a local git repository, the new repository will be added 58 as the "origin" git remote. Otherwise, the command will prompt to clone the new 59 repository into a sub-directory. 60 61 To create a repository non-interactively, supply the following: 62 - the name argument; 63 - the %[1]s--confirm%[1]s flag; 64 - one of %[1]s--public%[1]s, %[1]s--private%[1]s, or %[1]s--internal%[1]s. 65 66 To toggle off %[1]s--enable-issues%[1]s or %[1]s--enable-wiki%[1]s, which are enabled 67 by default, use the %[1]s--enable-issues=false%[1]s syntax. 68 `, "`"), 69 Args: cobra.MaximumNArgs(1), 70 Example: heredoc.Doc(` 71 # create a repository under your account using the current directory name 72 $ git init my-project 73 $ cd my-project 74 $ gh repo create 75 76 # create a repository with a specific name 77 $ gh repo create my-project 78 79 # create a repository in an organization 80 $ gh repo create cli/my-project 81 82 # disable issues and wiki 83 $ gh repo create --enable-issues=false --enable-wiki=false 84 `), 85 Annotations: map[string]string{ 86 "help:arguments": heredoc.Doc(` 87 A repository can be supplied as an argument in any of the following formats: 88 - "OWNER/REPO" 89 - by URL, e.g. "https://github.com/OWNER/REPO" 90 `), 91 }, 92 RunE: func(cmd *cobra.Command, args []string) error { 93 if len(args) > 0 { 94 opts.Name = args[0] 95 } 96 97 if len(args) == 0 && (opts.GitIgnoreTemplate != "" || opts.LicenseTemplate != "") { 98 return &cmdutil.FlagError{Err: errors.New(".gitignore and license templates are added only when a specific repository name is passed")} 99 } 100 101 if opts.Template != "" && (opts.GitIgnoreTemplate != "" || opts.LicenseTemplate != "") { 102 return &cmdutil.FlagError{Err: errors.New(".gitignore and license templates are not added when template is provided")} 103 } 104 105 if !opts.IO.CanPrompt() { 106 if opts.Name == "" { 107 return &cmdutil.FlagError{Err: errors.New("name argument required when not running interactively")} 108 } 109 110 if !opts.Internal && !opts.Private && !opts.Public { 111 return &cmdutil.FlagError{Err: errors.New("`--public`, `--private`, or `--internal` required when not running interactively")} 112 } 113 } 114 115 if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || cmd.Flags().Changed("enable-issues") || cmd.Flags().Changed("enable-wiki")) { 116 return &cmdutil.FlagError{Err: errors.New("The `--template` option is not supported with `--homepage`, `--team`, `--enable-issues`, or `--enable-wiki`")} 117 } 118 119 if runF != nil { 120 return runF(opts) 121 } 122 return createRun(opts) 123 }, 124 } 125 126 cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Description of the repository") 127 cmd.Flags().StringVarP(&opts.Homepage, "homepage", "h", "", "Repository home page `URL`") 128 cmd.Flags().StringVarP(&opts.Team, "team", "t", "", "The `name` of the organization team to be granted access") 129 cmd.Flags().StringVarP(&opts.Template, "template", "p", "", "Make the new repository based on a template `repository`") 130 cmd.Flags().BoolVar(&opts.EnableIssues, "enable-issues", true, "Enable issues in the new repository") 131 cmd.Flags().BoolVar(&opts.EnableWiki, "enable-wiki", true, "Enable wiki in the new repository") 132 cmd.Flags().BoolVar(&opts.Public, "public", false, "Make the new repository public") 133 cmd.Flags().BoolVar(&opts.Private, "private", false, "Make the new repository private") 134 cmd.Flags().BoolVar(&opts.Internal, "internal", false, "Make the new repository internal") 135 cmd.Flags().BoolVarP(&opts.ConfirmSubmit, "confirm", "y", false, "Skip the confirmation prompt") 136 cmd.Flags().StringVarP(&opts.GitIgnoreTemplate, "gitignore", "g", "", "Specify a gitignore template for the repository") 137 cmd.Flags().StringVarP(&opts.LicenseTemplate, "license", "l", "", "Specify an Open Source License for the repository") 138 139 _ = cmd.RegisterFlagCompletionFunc("gitignore", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 140 httpClient, err := opts.HttpClient() 141 if err != nil { 142 return nil, cobra.ShellCompDirectiveError 143 } 144 cfg, err := opts.Config() 145 if err != nil { 146 return nil, cobra.ShellCompDirectiveError 147 } 148 hostname, err := cfg.DefaultHost() 149 if err != nil { 150 return nil, cobra.ShellCompDirectiveError 151 } 152 results, err := listGitIgnoreTemplates(httpClient, hostname) 153 if err != nil { 154 return nil, cobra.ShellCompDirectiveError 155 } 156 return results, cobra.ShellCompDirectiveNoFileComp 157 }) 158 159 _ = cmd.RegisterFlagCompletionFunc("license", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 160 httpClient, err := opts.HttpClient() 161 if err != nil { 162 return nil, cobra.ShellCompDirectiveError 163 } 164 cfg, err := opts.Config() 165 if err != nil { 166 return nil, cobra.ShellCompDirectiveError 167 } 168 hostname, err := cfg.DefaultHost() 169 if err != nil { 170 return nil, cobra.ShellCompDirectiveError 171 } 172 licenses, err := listLicenseTemplates(httpClient, hostname) 173 if err != nil { 174 return nil, cobra.ShellCompDirectiveError 175 } 176 var results []string 177 for _, license := range licenses { 178 results = append(results, fmt.Sprintf("%s\t%s", license.Key, license.Name)) 179 } 180 return results, cobra.ShellCompDirectiveNoFileComp 181 }) 182 183 return cmd 184 } 185 186 func createRun(opts *CreateOptions) error { 187 projectDir, projectDirErr := git.ToplevelDir() 188 isNameAnArg := false 189 isDescEmpty := opts.Description == "" 190 isVisibilityPassed := false 191 inLocalRepo := projectDirErr == nil 192 193 if opts.Name != "" { 194 isNameAnArg = true 195 } else { 196 if projectDirErr != nil { 197 return projectDirErr 198 } 199 opts.Name = path.Base(projectDir) 200 } 201 202 enabledFlagCount := 0 203 visibility := "" 204 if opts.Public { 205 enabledFlagCount++ 206 visibility = "PUBLIC" 207 } 208 if opts.Private { 209 enabledFlagCount++ 210 visibility = "PRIVATE" 211 } 212 if opts.Internal { 213 enabledFlagCount++ 214 visibility = "INTERNAL" 215 } 216 217 if enabledFlagCount > 1 { 218 return fmt.Errorf("expected exactly one of `--public`, `--private`, or `--internal` to be true") 219 } else if enabledFlagCount == 1 { 220 isVisibilityPassed = true 221 } 222 223 cfg, err := opts.Config() 224 if err != nil { 225 return err 226 } 227 228 var gitIgnoreTemplate, repoLicenseTemplate string 229 230 gitIgnoreTemplate = opts.GitIgnoreTemplate 231 repoLicenseTemplate = opts.LicenseTemplate 232 233 // Trigger interactive prompt if name is not passed 234 if !isNameAnArg { 235 newName, newDesc, newVisibility, err := interactiveRepoCreate(isDescEmpty, isVisibilityPassed, opts.Name) 236 if err != nil { 237 return err 238 } 239 if newName != "" { 240 opts.Name = newName 241 } 242 if newDesc != "" { 243 opts.Description = newDesc 244 } 245 if newVisibility != "" { 246 visibility = newVisibility 247 } 248 249 } else { 250 // Go for a prompt only if visibility isn't passed 251 if !isVisibilityPassed { 252 newVisibility, err := getVisibility() 253 if err != nil { 254 return nil 255 } 256 visibility = newVisibility 257 } 258 259 httpClient, err := opts.HttpClient() 260 if err != nil { 261 return err 262 } 263 264 host, err := cfg.DefaultHost() 265 if err != nil { 266 return err 267 } 268 269 // GitIgnore and License templates not added when a template repository 270 // is passed, or when the confirm flag is set. 271 if opts.Template == "" && opts.IO.CanPrompt() && !opts.ConfirmSubmit { 272 if gitIgnoreTemplate == "" { 273 gt, err := interactiveGitIgnore(httpClient, host) 274 if err != nil { 275 return err 276 } 277 gitIgnoreTemplate = gt 278 } 279 if repoLicenseTemplate == "" { 280 lt, err := interactiveLicense(httpClient, host) 281 if err != nil { 282 return err 283 } 284 repoLicenseTemplate = lt 285 } 286 } 287 } 288 289 var repoToCreate ghrepo.Interface 290 291 if strings.Contains(opts.Name, "/") { 292 var err error 293 repoToCreate, err = ghrepo.FromFullName(opts.Name) 294 if err != nil { 295 return fmt.Errorf("argument error: %w", err) 296 } 297 } else { 298 host, err := cfg.DefaultHost() 299 if err != nil { 300 return err 301 } 302 repoToCreate = ghrepo.NewWithHost("", opts.Name, host) 303 } 304 305 input := repoCreateInput{ 306 Name: repoToCreate.RepoName(), 307 Visibility: visibility, 308 OwnerLogin: repoToCreate.RepoOwner(), 309 TeamSlug: opts.Team, 310 Description: opts.Description, 311 HomepageURL: opts.Homepage, 312 HasIssuesEnabled: opts.EnableIssues, 313 HasWikiEnabled: opts.EnableWiki, 314 GitIgnoreTemplate: gitIgnoreTemplate, 315 LicenseTemplate: repoLicenseTemplate, 316 } 317 318 httpClient, err := opts.HttpClient() 319 if err != nil { 320 return err 321 } 322 323 var templateRepoMainBranch string 324 if opts.Template != "" { 325 var templateRepo ghrepo.Interface 326 apiClient := api.NewClientFromHTTP(httpClient) 327 328 templateRepoName := opts.Template 329 if !strings.Contains(templateRepoName, "/") { 330 currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default()) 331 if err != nil { 332 return err 333 } 334 templateRepoName = currentUser + "/" + templateRepoName 335 } 336 templateRepo, err = ghrepo.FromFullName(templateRepoName) 337 if err != nil { 338 return fmt.Errorf("argument error: %w", err) 339 } 340 341 repo, err := api.GitHubRepo(apiClient, templateRepo) 342 if err != nil { 343 return err 344 } 345 346 input.TemplateRepositoryID = repo.ID 347 templateRepoMainBranch = repo.DefaultBranchRef.Name 348 } 349 350 createLocalDirectory := opts.ConfirmSubmit 351 if !opts.ConfirmSubmit { 352 opts.ConfirmSubmit, err = confirmSubmission(input.Name, input.OwnerLogin, inLocalRepo) 353 if err != nil { 354 return err 355 } 356 } 357 358 if opts.ConfirmSubmit { 359 repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input) 360 if err != nil { 361 return err 362 } 363 stderr := opts.IO.ErrOut 364 stdout := opts.IO.Out 365 cs := opts.IO.ColorScheme() 366 isTTY := opts.IO.IsStdoutTTY() 367 368 if isTTY { 369 fmt.Fprintf(stderr, "%s Created repository %s on GitHub\n", cs.SuccessIconWithColor(cs.Green), ghrepo.FullName(repo)) 370 } else { 371 fmt.Fprintln(stdout, ghrepo.GenerateRepoURL(repo, "")) 372 } 373 374 protocol, err := cfg.Get(repo.RepoHost(), "git_protocol") 375 if err != nil { 376 return err 377 } 378 379 remoteURL := ghrepo.FormatRemoteURL(repo, protocol) 380 381 if inLocalRepo { 382 _, err = git.AddRemote("origin", remoteURL) 383 if err != nil { 384 return err 385 } 386 if isTTY { 387 fmt.Fprintf(stderr, "%s Added remote %s\n", cs.SuccessIcon(), remoteURL) 388 } 389 } else { 390 if opts.IO.CanPrompt() { 391 if !createLocalDirectory && (gitIgnoreTemplate == "" && repoLicenseTemplate == "") { 392 err := prompt.Confirm(fmt.Sprintf(`Create a local project directory for "%s"?`, ghrepo.FullName(repo)), &createLocalDirectory) 393 if err != nil { 394 return err 395 } 396 } else if !createLocalDirectory && (gitIgnoreTemplate != "" || repoLicenseTemplate != "") { 397 err := prompt.Confirm(fmt.Sprintf(`Clone the remote project directory "%s"?`, ghrepo.FullName(repo)), &createLocalDirectory) 398 if err != nil { 399 return err 400 } 401 } 402 } 403 if createLocalDirectory && (gitIgnoreTemplate == "" && repoLicenseTemplate == "") { 404 path := repo.RepoName() 405 checkoutBranch := "" 406 if opts.Template != "" { 407 // NOTE: we cannot read `defaultBranchRef` from the newly created repository as it will 408 // be null at this time. Instead, we assume that the main branch name of the new 409 // repository will be the same as that of the template repository. 410 checkoutBranch = templateRepoMainBranch 411 } 412 if err := localInit(opts.IO, remoteURL, path, checkoutBranch); err != nil { 413 return err 414 } 415 if isTTY { 416 fmt.Fprintf(stderr, "%s Initialized repository in \"%s\"\n", cs.SuccessIcon(), path) 417 } 418 } else if createLocalDirectory && (gitIgnoreTemplate != "" || repoLicenseTemplate != "") { 419 _, err := git.RunClone(remoteURL, []string{}) 420 if err != nil { 421 return err 422 } 423 } 424 } 425 426 return nil 427 } 428 fmt.Fprintln(opts.IO.Out, "Discarding...") 429 return nil 430 } 431 432 func interactiveGitIgnore(client *http.Client, hostname string) (string, error) { 433 434 var addGitIgnore bool 435 var addGitIgnoreSurvey []*survey.Question 436 437 addGitIgnoreQuestion := &survey.Question{ 438 Name: "addGitIgnore", 439 Prompt: &survey.Confirm{ 440 Message: "Would you like to add a .gitignore?", 441 Default: false, 442 }, 443 } 444 445 addGitIgnoreSurvey = append(addGitIgnoreSurvey, addGitIgnoreQuestion) 446 err := prompt.SurveyAsk(addGitIgnoreSurvey, &addGitIgnore) 447 if err != nil { 448 return "", err 449 } 450 451 var wantedIgnoreTemplate string 452 453 if addGitIgnore { 454 var gitIg []*survey.Question 455 456 gitIgnoretemplates, err := listGitIgnoreTemplates(client, hostname) 457 if err != nil { 458 return "", err 459 } 460 gitIgnoreQuestion := &survey.Question{ 461 Name: "chooseGitIgnore", 462 Prompt: &survey.Select{ 463 Message: "Choose a .gitignore template", 464 Options: gitIgnoretemplates, 465 }, 466 } 467 gitIg = append(gitIg, gitIgnoreQuestion) 468 err = prompt.SurveyAsk(gitIg, &wantedIgnoreTemplate) 469 if err != nil { 470 return "", err 471 } 472 } 473 474 return wantedIgnoreTemplate, nil 475 } 476 477 func interactiveLicense(client *http.Client, hostname string) (string, error) { 478 var addLicense bool 479 var addLicenseSurvey []*survey.Question 480 var wantedLicense string 481 482 addLicenseQuestion := &survey.Question{ 483 Name: "addLicense", 484 Prompt: &survey.Confirm{ 485 Message: "Would you like to add a license?", 486 Default: false, 487 }, 488 } 489 490 addLicenseSurvey = append(addLicenseSurvey, addLicenseQuestion) 491 err := prompt.SurveyAsk(addLicenseSurvey, &addLicense) 492 if err != nil { 493 return "", err 494 } 495 496 licenseKey := map[string]string{} 497 498 if addLicense { 499 licenseTemplates, err := listLicenseTemplates(client, hostname) 500 if err != nil { 501 return "", err 502 } 503 var licenseNames []string 504 for _, l := range licenseTemplates { 505 licenseNames = append(licenseNames, l.Name) 506 licenseKey[l.Name] = l.Key 507 } 508 var licenseQs []*survey.Question 509 510 licenseQuestion := &survey.Question{ 511 Name: "chooseLicense", 512 Prompt: &survey.Select{ 513 Message: "Choose a license", 514 Options: licenseNames, 515 }, 516 } 517 licenseQs = append(licenseQs, licenseQuestion) 518 err = prompt.SurveyAsk(licenseQs, &wantedLicense) 519 if err != nil { 520 return "", err 521 } 522 return licenseKey[wantedLicense], nil 523 } 524 return "", nil 525 } 526 527 func localInit(io *iostreams.IOStreams, remoteURL, path, checkoutBranch string) error { 528 gitInit, err := git.GitCommand("init", path) 529 if err != nil { 530 return err 531 } 532 isTTY := io.IsStdoutTTY() 533 if isTTY { 534 gitInit.Stdout = io.Out 535 } 536 gitInit.Stderr = io.ErrOut 537 err = run.PrepareCmd(gitInit).Run() 538 if err != nil { 539 return err 540 } 541 542 gitRemoteAdd, err := git.GitCommand("-C", path, "remote", "add", "origin", remoteURL) 543 if err != nil { 544 return err 545 } 546 gitRemoteAdd.Stdout = io.Out 547 gitRemoteAdd.Stderr = io.ErrOut 548 err = run.PrepareCmd(gitRemoteAdd).Run() 549 if err != nil { 550 return err 551 } 552 553 if checkoutBranch == "" { 554 return nil 555 } 556 557 gitFetch, err := git.GitCommand("-C", path, "fetch", "origin", fmt.Sprintf("+refs/heads/%[1]s:refs/remotes/origin/%[1]s", checkoutBranch)) 558 if err != nil { 559 return err 560 } 561 gitFetch.Stdout = io.Out 562 gitFetch.Stderr = io.ErrOut 563 err = run.PrepareCmd(gitFetch).Run() 564 if err != nil { 565 return err 566 } 567 568 gitCheckout, err := git.GitCommand("-C", path, "checkout", checkoutBranch) 569 if err != nil { 570 return err 571 } 572 gitCheckout.Stdout = io.Out 573 gitCheckout.Stderr = io.ErrOut 574 return run.PrepareCmd(gitCheckout).Run() 575 } 576 577 func interactiveRepoCreate(isDescEmpty bool, isVisibilityPassed bool, repoName string) (string, string, string, error) { 578 qs := []*survey.Question{} 579 580 repoNameQuestion := &survey.Question{ 581 Name: "repoName", 582 Prompt: &survey.Input{ 583 Message: "Repository name", 584 Default: repoName, 585 }, 586 } 587 qs = append(qs, repoNameQuestion) 588 589 if isDescEmpty { 590 repoDescriptionQuestion := &survey.Question{ 591 Name: "repoDescription", 592 Prompt: &survey.Input{ 593 Message: "Repository description", 594 }, 595 } 596 597 qs = append(qs, repoDescriptionQuestion) 598 } 599 600 if !isVisibilityPassed { 601 repoVisibilityQuestion := &survey.Question{ 602 Name: "repoVisibility", 603 Prompt: &survey.Select{ 604 Message: "Visibility", 605 Options: []string{"Public", "Private", "Internal"}, 606 }, 607 } 608 qs = append(qs, repoVisibilityQuestion) 609 } 610 611 answers := struct { 612 RepoName string 613 RepoDescription string 614 RepoVisibility string 615 }{} 616 617 err := prompt.SurveyAsk(qs, &answers) 618 619 if err != nil { 620 return "", "", "", err 621 } 622 623 return answers.RepoName, answers.RepoDescription, strings.ToUpper(answers.RepoVisibility), nil 624 } 625 626 func confirmSubmission(repoName string, repoOwner string, inLocalRepo bool) (bool, error) { 627 qs := []*survey.Question{} 628 629 promptString := "" 630 if inLocalRepo { 631 promptString = `This will add an "origin" git remote to your local repository. Continue?` 632 } else { 633 targetRepo := repoName 634 if repoOwner != "" { 635 targetRepo = fmt.Sprintf("%s/%s", repoOwner, repoName) 636 } 637 promptString = fmt.Sprintf(`This will create the "%s" repository on GitHub. Continue?`, targetRepo) 638 } 639 640 confirmSubmitQuestion := &survey.Question{ 641 Name: "confirmSubmit", 642 Prompt: &survey.Confirm{ 643 Message: promptString, 644 Default: true, 645 }, 646 } 647 qs = append(qs, confirmSubmitQuestion) 648 649 answer := struct { 650 ConfirmSubmit bool 651 }{} 652 653 err := prompt.SurveyAsk(qs, &answer) 654 if err != nil { 655 return false, err 656 } 657 658 return answer.ConfirmSubmit, nil 659 } 660 661 func getVisibility() (string, error) { 662 qs := []*survey.Question{} 663 664 getVisibilityQuestion := &survey.Question{ 665 Name: "repoVisibility", 666 Prompt: &survey.Select{ 667 Message: "Visibility", 668 Options: []string{"Public", "Private", "Internal"}, 669 }, 670 } 671 qs = append(qs, getVisibilityQuestion) 672 673 answer := struct { 674 RepoVisibility string 675 }{} 676 677 err := prompt.SurveyAsk(qs, &answer) 678 if err != nil { 679 return "", err 680 } 681 682 return strings.ToUpper(answer.RepoVisibility), nil 683 }