From 0a0a358168ca39705f8fa510b7cc4524225a4cf1 Mon Sep 17 00:00:00 2001 From: Meijke Date: Wed, 17 Nov 2021 11:16:37 -0800 Subject: [PATCH] rewrite `gh repo create` (#4578) Co-authored-by: Parth Patel --- git/git.go | 10 + pkg/cmd/repo/create/create.go | 839 ++++++++++++--------- pkg/cmd/repo/create/create_test.go | 1089 +++++++++------------------- 3 files changed, 856 insertions(+), 1082 deletions(-) diff --git a/git/git.go b/git/git.go index 97dafa1c7..bdd14933b 100644 --- a/git/git.go +++ b/git/git.go @@ -366,6 +366,16 @@ func ToplevelDir() (string, error) { } +// ToplevelDirFromPath returns the top-level given path of the current repository +func GetDirFromPath(p string) (string, error) { + showCmd, err := GitCommand("-C", p, "rev-parse", "--git-dir") + if err != nil { + return "", err + } + output, err := run.PrepareCmd(showCmd).Output() + return firstLine(output), err +} + func PathFromRepoRoot() string { showCmd, err := GitCommand("rev-parse", "--show-prefix") if err != nil { diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index dc1e0bf8f..d480efc79 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -1,9 +1,11 @@ package create import ( + "errors" "fmt" "net/http" - "path" + "os/exec" + "path/filepath" "strings" "github.com/AlecAivazis/survey/v2" @@ -29,14 +31,23 @@ type CreateOptions struct { Homepage string Team string Template string - EnableIssues bool - EnableWiki bool Public bool Private bool Internal bool - ConfirmSubmit bool + Visibility string + Push bool + Clone bool + Source string + Remote string GitIgnoreTemplate string LicenseTemplate string + DisableIssues bool + DisableWiki bool + Interactive bool + + ConfirmSubmit bool + EnableIssues bool + EnableWiki bool } func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { @@ -52,66 +63,84 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co Long: heredoc.Docf(` Create a new GitHub repository. - When the current directory is a local git repository, the new repository will be added - as the "origin" git remote. Otherwise, the command will prompt to clone the new - repository into a sub-directory. + To create a repository interactively, use %[1]sgh repo create%[1]s with no arguments. - To create a repository non-interactively, supply the following: - - the name argument; - - the %[1]s--confirm%[1]s flag; - - one of %[1]s--public%[1]s, %[1]s--private%[1]s, or %[1]s--internal%[1]s. + To create a remote repository non-interactively, supply the repository name and one of %[1]s--public%[1]s, %[1]s--private%[1]s, or %[1]s--internal%[1]s. + Pass %[1]s--clone%[1]s to clone the new repository locally. - To toggle off %[1]s--enable-issues%[1]s or %[1]s--enable-wiki%[1]s, which are enabled - by default, use the %[1]s--enable-issues=false%[1]s syntax. + To create a remote repository from an existing local repository, specify the source directory with %[1]s--source%[1]s. + By default, the remote repository name will be the name of the source directory. + Pass %[1]s--push%[1]s to push any local commits to the new repository. `, "`"), - Args: cobra.MaximumNArgs(1), Example: heredoc.Doc(` - # create a repository under your account using the current directory name - $ git init my-project - $ cd my-project - $ gh repo create + # create a repository interactively + gh repo create - # create a repository with a specific name - $ gh repo create my-project + # create a new remote repository and clone it locally + gh repo create my-project --public --clone - # create a repository in an organization - $ gh repo create cli/my-project - - # disable issues and wiki - $ gh repo create --enable-issues=false --enable-wiki=false - `), - Annotations: map[string]string{ - "help:arguments": heredoc.Doc(` - A repository can be supplied as an argument in any of the following formats: - - "OWNER/REPO" - - by URL, e.g. "https://github.com/OWNER/REPO" - `), - }, + # create a remote repository from the current directory + gh repo create my-project --private --source=. --remote=upstream + `), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { opts.Name = args[0] } - if len(args) == 0 && (opts.GitIgnoreTemplate != "" || opts.LicenseTemplate != "") { - return cmdutil.FlagErrorf(".gitignore and license templates are added only when a specific repository name is passed") + if len(args) == 0 && cmd.Flags().NFlag() == 0 { + if !opts.IO.CanPrompt() { + return cmdutil.FlagErrorf("at least one argument required in non-interactive mode") + } + opts.Interactive = true + } else { + // exactly one visibility flag required + if !opts.Public && !opts.Private && !opts.Internal { + return cmdutil.FlagErrorf("`--public`, `--private`, or `--internal` required when not running interactively") + } + err := cmdutil.MutuallyExclusive( + "expected exactly one of `--public`, `--private`, or `--internal`", + opts.Public, opts.Private, opts.Internal) + if err != nil { + return err + } + + if opts.Public { + opts.Visibility = "PUBLIC" + } else if opts.Private { + opts.Visibility = "PRIVATE" + } else { + opts.Visibility = "INTERNAL" + } + } + + if opts.Source == "" { + if opts.Remote != "" { + return cmdutil.FlagErrorf("the `--remote` option can only be used with `--source`") + } + if opts.Push { + return cmdutil.FlagErrorf("the `--push` option can only be used with `--source`") + } + if opts.Name == "" && !opts.Interactive { + return cmdutil.FlagErrorf("name argument required to create new remote repository") + } + + } else if opts.Clone || opts.GitIgnoreTemplate != "" || opts.LicenseTemplate != "" || opts.Template != "" { + return cmdutil.FlagErrorf("the `--source` option is not supported with `--clone`, `--template`, `--license`, or `--gitignore`") } if opts.Template != "" && (opts.GitIgnoreTemplate != "" || opts.LicenseTemplate != "") { return cmdutil.FlagErrorf(".gitignore and license templates are not added when template is provided") } - if !opts.IO.CanPrompt() { - if opts.Name == "" { - return cmdutil.FlagErrorf("name argument required when not running interactively") - } - - if !opts.Internal && !opts.Private && !opts.Public { - return cmdutil.FlagErrorf("`--public`, `--private`, or `--internal` required when not running interactively") - } + if cmd.Flags().Changed("enable-issues") { + opts.DisableIssues = !opts.EnableIssues } - - if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || cmd.Flags().Changed("enable-issues") || cmd.Flags().Changed("enable-wiki")) { - return cmdutil.FlagErrorf("The `--template` option is not supported with `--homepage`, `--team`, `--enable-issues`, or `--enable-wiki`") + if cmd.Flags().Changed("enable-wiki") { + opts.DisableWiki = !opts.EnableWiki + } + if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || opts.DisableIssues || opts.DisableWiki) { + return cmdutil.FlagErrorf("the `--template` option is not supported with `--homepage`, `--team`, `--disable-issues`, or `--disable-wiki`") } if runF != nil { @@ -125,14 +154,26 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co 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, "Skip the confirmation prompt") cmd.Flags().StringVarP(&opts.GitIgnoreTemplate, "gitignore", "g", "", "Specify a gitignore template for the repository") cmd.Flags().StringVarP(&opts.LicenseTemplate, "license", "l", "", "Specify an Open Source License for the repository") + cmd.Flags().StringVarP(&opts.Source, "source", "s", "", "Specify path to local repository to use as source") + cmd.Flags().StringVarP(&opts.Remote, "remote", "r", "", "Specify remote name for the new repository") + cmd.Flags().BoolVar(&opts.Push, "push", false, "Push local commits to the new repository") + cmd.Flags().BoolVarP(&opts.Clone, "clone", "c", false, "Clone the new repository to the current directory") + cmd.Flags().BoolVar(&opts.DisableIssues, "disable-issues", false, "Disable issues in the new repository") + cmd.Flags().BoolVar(&opts.DisableWiki, "disable-wiki", false, "Disable wiki in the new repository") + + // deprecated flags + cmd.Flags().BoolVarP(&opts.ConfirmSubmit, "confirm", "y", false, "Skip the confirmation prompt") + 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().MarkDeprecated("confirm", "Pass any argument to skip confirmation prompt") + _ = cmd.Flags().MarkDeprecated("enable-issues", "Disable issues with `--disable-issues`") + _ = cmd.Flags().MarkDeprecated("enable-wiki", "Disable wiki with `--disable-wiki`") _ = cmd.RegisterFlagCompletionFunc("gitignore", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { httpClient, err := opts.HttpClient() @@ -182,109 +223,65 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co } func createRun(opts *CreateOptions) error { - projectDir, projectDirErr := git.ToplevelDir() - isNameAnArg := false - isDescEmpty := opts.Description == "" - isVisibilityPassed := false - inLocalRepo := projectDirErr == nil + var fromScratch bool + modeOptions := []string{ + "Create a new repository on GitHub from scratch", + "Push an existing local repository to GitHub"} - if opts.Name != "" { - isNameAnArg = true - } else { - if projectDirErr != nil { - return projectDirErr + if opts.Interactive { + var createMode string + createModePrompt := &survey.Select{ + Message: "What would you like to do?", + Options: modeOptions, } - opts.Name = path.Base(projectDir) + err := prompt.SurveyAskOne(createModePrompt, &createMode) + if err != nil { + return err + } + fromScratch = createMode == modeOptions[0] + } else { + fromScratch = opts.Source == "" } - enabledFlagCount := 0 - visibility := "" - if opts.Public { - enabledFlagCount++ - visibility = "PUBLIC" + if fromScratch { + return createFromScratch(opts) + } else { + return createFromLocal(opts) } - 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 +} + +// create new repo on remote host +func createFromScratch(opts *CreateOptions) error { + httpClient, err := opts.HttpClient() + if err != nil { + return err } + var repoToCreate ghrepo.Interface cfg, err := opts.Config() if err != nil { return err } - var gitIgnoreTemplate, repoLicenseTemplate string - - gitIgnoreTemplate = opts.GitIgnoreTemplate - repoLicenseTemplate = opts.LicenseTemplate - - // 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 err - } - visibility = newVisibility - } - - httpClient, err := opts.HttpClient() - if err != nil { - return err - } - - host, err := cfg.DefaultHost() - if err != nil { - return err - } - - // GitIgnore and License templates not added when a template repository - // is passed, or when the confirm flag is set. - if opts.Template == "" && opts.IO.CanPrompt() && !opts.ConfirmSubmit { - if gitIgnoreTemplate == "" { - gt, err := interactiveGitIgnore(httpClient, host) - if err != nil { - return err - } - gitIgnoreTemplate = gt - } - if repoLicenseTemplate == "" { - lt, err := interactiveLicense(httpClient, host) - if err != nil { - return err - } - repoLicenseTemplate = lt - } - } + host, err := cfg.DefaultHost() + if err != nil { + return err } - var repoToCreate ghrepo.Interface + if opts.Interactive { + opts.Name, opts.Description, opts.Visibility, err = interactiveRepoInfo("") + if err != nil { + return err + } + opts.GitIgnoreTemplate, err = interactiveGitIgnore(httpClient, host) + if err != nil { + return err + } + opts.LicenseTemplate, err = interactiveLicense(httpClient, host) + if err != nil { + return err + } + } if strings.Contains(opts.Name, "/") { var err error @@ -293,29 +290,20 @@ func createRun(opts *CreateOptions) error { return fmt.Errorf("argument error: %w", err) } } else { - host, err := cfg.DefaultHost() - if err != nil { - return err - } repoToCreate = ghrepo.NewWithHost("", opts.Name, host) } input := repoCreateInput{ Name: repoToCreate.RepoName(), - Visibility: visibility, + Visibility: opts.Visibility, OwnerLogin: repoToCreate.RepoOwner(), TeamSlug: opts.Team, Description: opts.Description, HomepageURL: opts.Homepage, - HasIssuesEnabled: opts.EnableIssues, - HasWikiEnabled: opts.EnableWiki, - GitIgnoreTemplate: gitIgnoreTemplate, - LicenseTemplate: repoLicenseTemplate, - } - - httpClient, err := opts.HttpClient() - if err != nil { - return err + HasIssuesEnabled: !opts.DisableIssues, + HasWikiEnabled: !opts.DisableWiki, + GitIgnoreTemplate: opts.GitIgnoreTemplate, + LicenseTemplate: opts.LicenseTemplate, } var templateRepoMainBranch string @@ -325,10 +313,6 @@ func createRun(opts *CreateOptions) error { templateRepoName := opts.Template if !strings.Contains(templateRepoName, "/") { - host, err := cfg.DefaultHost() - if err != nil { - return err - } currentUser, err := api.CurrentLoginName(apiClient, host) if err != nil { return err @@ -349,30 +333,42 @@ func createRun(opts *CreateOptions) error { templateRepoMainBranch = repo.DefaultBranchRef.Name } - createLocalDirectory := opts.ConfirmSubmit - if !opts.ConfirmSubmit { - opts.ConfirmSubmit, err = confirmSubmission(input.Name, input.OwnerLogin, inLocalRepo) + repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input) + if err != nil { + return err + } + + if opts.Interactive { + doCreate, err := confirmSubmission(opts.Name, repoToCreate.RepoOwner(), opts.Visibility) + if err != nil { + return err + } + if !doCreate { + return cmdutil.CancelError + } + } + + cs := opts.IO.ColorScheme() + isTTY := opts.IO.IsStdoutTTY() + if isTTY { + fmt.Fprintf(opts.IO.Out, + "%s Created repository %s on GitHub\n", + cs.SuccessIconWithColor(cs.Green), + ghrepo.FullName(repo)) + } + + if opts.Interactive { + cloneQuestion := &survey.Confirm{ + Message: "Clone the new repository locally?", + Default: true, + } + err = prompt.SurveyAskOne(cloneQuestion, &opts.Clone) 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 - cs := opts.IO.ColorScheme() - isTTY := opts.IO.IsStdoutTTY() - - if isTTY { - fmt.Fprintf(stderr, "%s Created repository %s on GitHub\n", cs.SuccessIconWithColor(cs.Green), ghrepo.FullName(repo)) - } else { - fmt.Fprintln(stdout, ghrepo.GenerateRepoURL(repo, "")) - } - + if opts.Clone { protocol, err := cfg.Get(repo.RepoHost(), "git_protocol") if err != nil { return err @@ -380,59 +376,312 @@ func createRun(opts *CreateOptions) error { remoteURL := ghrepo.FormatRemoteURL(repo, protocol) - if inLocalRepo { - _, err = git.AddRemote("origin", remoteURL) - if err != nil { + if opts.LicenseTemplate == "" && opts.GitIgnoreTemplate == "" { + // cloning empty repository or template + checkoutBranch := "" + if opts.Template != "" { + // use the template's default branch + checkoutBranch = templateRepoMainBranch + } + if err := localInit(opts.IO, remoteURL, repo.RepoName(), checkoutBranch); err != nil { return err } - if isTTY { - fmt.Fprintf(stderr, "%s Added remote %s\n", cs.SuccessIcon(), remoteURL) - } - } else { - if opts.IO.CanPrompt() { - if !createLocalDirectory && (gitIgnoreTemplate == "" && repoLicenseTemplate == "") { - err := prompt.Confirm(fmt.Sprintf(`Create a local project directory for "%s"?`, ghrepo.FullName(repo)), &createLocalDirectory) - if err != nil { - return err - } - } else if !createLocalDirectory && (gitIgnoreTemplate != "" || repoLicenseTemplate != "") { - err := prompt.Confirm(fmt.Sprintf(`Clone the remote project directory "%s"?`, ghrepo.FullName(repo)), &createLocalDirectory) - if err != nil { - return err - } - } - } - if createLocalDirectory && (gitIgnoreTemplate == "" && repoLicenseTemplate == "") { - path := repo.RepoName() - checkoutBranch := "" - if opts.Template != "" { - // NOTE: we cannot read `defaultBranchRef` from the newly created repository as it will - // be null at this time. Instead, we assume that the main branch name of the new - // repository will be the same as that of the template repository. - checkoutBranch = templateRepoMainBranch - } - if err := localInit(opts.IO, remoteURL, path, checkoutBranch); err != nil { - return err - } - if isTTY { - fmt.Fprintf(stderr, "%s Initialized repository in \"%s\"\n", cs.SuccessIcon(), path) - } - } else if createLocalDirectory && (gitIgnoreTemplate != "" || repoLicenseTemplate != "") { - _, err := git.RunClone(remoteURL, []string{}) - if err != nil { - return err - } - } + } else if _, err := git.RunClone(remoteURL, []string{}); err != nil { + return err } - - return nil } - fmt.Fprintln(opts.IO.Out, "Discarding...") + return nil } -func interactiveGitIgnore(client *http.Client, hostname string) (string, error) { +// create repo on remote host from existing local repo +func createFromLocal(opts *CreateOptions) error { + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + cs := opts.IO.ColorScheme() + isTTY := opts.IO.IsStdoutTTY() + stdout := opts.IO.Out + + cfg, err := opts.Config() + if err != nil { + return err + } + host, err := cfg.DefaultHost() + if err != nil { + return err + } + + if opts.Interactive { + opts.Source, err = interactiveSource() + if err != nil { + return err + } + } + + repoPath := opts.Source + + var baseRemote string + if opts.Remote == "" { + baseRemote = "origin" + } else { + baseRemote = opts.Remote + } + + absPath, err := filepath.Abs(repoPath) + if err != nil { + return err + } + + isRepo, err := isLocalRepo(repoPath) + if err != nil { + return err + } + if !isRepo { + if repoPath == "." { + return fmt.Errorf("current directory is not a git repository. Run `git init` to initalize it") + } + return fmt.Errorf("%s is not a git repository. Run `git -C %s init` to initialize it", absPath, repoPath) + } + + committed, err := hasCommits(repoPath) + if err != nil { + return err + } + if opts.Push { + // fail immediately if trying to push with no commits + if !committed { + return fmt.Errorf("`--push` enabled but no commits found in %s", absPath) + } + } + + if opts.Interactive { + opts.Name, opts.Description, opts.Visibility, err = interactiveRepoInfo(filepath.Base(absPath)) + if err != nil { + return err + } + } + + var repoToCreate ghrepo.Interface + + // repo name will be currdir name or specified + if opts.Name == "" { + repoToCreate = ghrepo.NewWithHost("", filepath.Base(absPath), host) + } else 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.NewWithHost("", opts.Name, host) + } + + input := repoCreateInput{ + Name: repoToCreate.RepoName(), + Visibility: opts.Visibility, + OwnerLogin: repoToCreate.RepoOwner(), + TeamSlug: opts.Team, + Description: opts.Description, + HomepageURL: opts.Homepage, + HasIssuesEnabled: !opts.DisableIssues, + HasWikiEnabled: !opts.DisableWiki, + GitIgnoreTemplate: opts.GitIgnoreTemplate, + LicenseTemplate: opts.LicenseTemplate, + } + + repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input) + if err != nil { + return err + } + + if isTTY { + fmt.Fprintf(stdout, + "%s Created repository %s on GitHub\n", + cs.SuccessIconWithColor(cs.Green), + ghrepo.FullName(repo)) + } + + protocol, err := cfg.Get(repo.RepoHost(), "git_protocol") + if err != nil { + return err + } + + remoteURL := ghrepo.FormatRemoteURL(repo, protocol) + + if opts.Interactive { + var addRemote bool + remoteQuesiton := &survey.Confirm{ + Message: `Add a remote?`, + Default: true, + } + err = prompt.SurveyAskOne(remoteQuesiton, &addRemote) + if err != nil { + return err + } + + if !addRemote { + return nil + } + + pushQuestion := &survey.Input{ + Message: "What should the new remote be called?", + Default: "origin", + } + err = prompt.SurveyAskOne(pushQuestion, &baseRemote) + if err != nil { + return err + } + } + + if err := sourceInit(opts.IO, remoteURL, baseRemote, repoPath); err != nil { + return err + } + + // don't prompt for push if there's no commits + if opts.Interactive && committed { + pushQuestion := &survey.Confirm{ + Message: fmt.Sprintf(`Would you like to push commits from the current branch to the %q?`, baseRemote), + Default: true, + } + err = prompt.SurveyAskOne(pushQuestion, &opts.Push) + if err != nil { + return err + } + } + + if opts.Push { + repoPush, err := git.GitCommand("-C", repoPath, "push", "-u", baseRemote, "HEAD") + if err != nil { + return err + } + err = run.PrepareCmd(repoPush).Run() + if err != nil { + return err + } + + if isTTY { + fmt.Fprintf(stdout, "%s Pushed commits to %s\n", cs.SuccessIcon(), remoteURL) + } + } + return nil +} + +func sourceInit(io *iostreams.IOStreams, remoteURL, baseRemote, repoPath string) error { + cs := io.ColorScheme() + isTTY := io.IsStdoutTTY() + stdout := io.Out + + remoteAdd, err := git.GitCommand("-C", repoPath, "remote", "add", baseRemote, remoteURL) + if err != nil { + return err + } + + err = run.PrepareCmd(remoteAdd).Run() + if err != nil { + return fmt.Errorf("%s Unable to add remote %q", cs.FailureIcon(), baseRemote) + } + if isTTY { + fmt.Fprintf(stdout, "%s Added remote %s\n", cs.SuccessIcon(), remoteURL) + } + return nil +} + +// check if local repository has commited changes +func hasCommits(repoPath string) (bool, error) { + hasCommitsCmd, err := git.GitCommand("-C", repoPath, "rev-parse", "HEAD") + if err != nil { + return false, err + } + prepareCmd := run.PrepareCmd(hasCommitsCmd) + err = prepareCmd.Run() + if err == nil { + return true, nil + } + + var execError *exec.ExitError + if errors.As(err, &execError) { + exitCode := int(execError.ExitCode()) + if exitCode == 128 { + return false, nil + } + return false, err + } + return false, nil +} + +// check if path is the top level directory of a git repo +func isLocalRepo(repoPath string) (bool, error) { + projectDir, projectDirErr := git.GetDirFromPath(repoPath) + if projectDirErr != nil { + var execError *exec.ExitError + if errors.As(projectDirErr, &execError) { + if exitCode := int(execError.ExitCode()); exitCode == 128 { + return false, nil + } + return false, projectDirErr + } + } + if projectDir != ".git" { + return false, nil + } + return true, nil +} + +// clone the checkout branch to specified path +func localInit(io *iostreams.IOStreams, remoteURL, path, checkoutBranch string) error { + gitInit, err := git.GitCommand("init", path) + if err != nil { + return err + } + isTTY := io.IsStdoutTTY() + if isTTY { + gitInit.Stdout = io.Out + } + gitInit.Stderr = io.ErrOut + err = run.PrepareCmd(gitInit).Run() + if err != nil { + return err + } + + gitRemoteAdd, err := git.GitCommand("-C", path, "remote", "add", "origin", remoteURL) + if err != nil { + return err + } + gitRemoteAdd.Stdout = io.Out + gitRemoteAdd.Stderr = io.ErrOut + err = run.PrepareCmd(gitRemoteAdd).Run() + if err != nil { + return err + } + + if checkoutBranch == "" { + return nil + } + + gitFetch, err := git.GitCommand("-C", path, "fetch", "origin", fmt.Sprintf("+refs/heads/%[1]s:refs/remotes/origin/%[1]s", checkoutBranch)) + if err != nil { + return err + } + gitFetch.Stdout = io.Out + gitFetch.Stderr = io.ErrOut + err = run.PrepareCmd(gitFetch).Run() + if err != nil { + return err + } + + gitCheckout, err := git.GitCommand("-C", path, "checkout", checkoutBranch) + if err != nil { + return err + } + gitCheckout.Stdout = io.Out + gitCheckout.Stderr = io.ErrOut + return run.PrepareCmd(gitCheckout).Run() +} + +func interactiveGitIgnore(client *http.Client, hostname string) (string, error) { var addGitIgnore bool var addGitIgnoreSurvey []*survey.Question @@ -471,6 +720,7 @@ func interactiveGitIgnore(client *http.Client, hostname string) (string, error) if err != nil { return "", err } + } return wantedIgnoreTemplate, nil @@ -526,118 +776,63 @@ func interactiveLicense(client *http.Client, hostname string) (string, error) { return "", nil } -func localInit(io *iostreams.IOStreams, remoteURL, path, checkoutBranch string) error { - gitInit, err := git.GitCommand("init", path) - if err != nil { - return err - } - isTTY := io.IsStdoutTTY() - if isTTY { - gitInit.Stdout = io.Out - } - gitInit.Stderr = io.ErrOut - err = run.PrepareCmd(gitInit).Run() - if err != nil { - return err - } - - gitRemoteAdd, err := git.GitCommand("-C", path, "remote", "add", "origin", remoteURL) - if err != nil { - return err - } - gitRemoteAdd.Stdout = io.Out - gitRemoteAdd.Stderr = io.ErrOut - err = run.PrepareCmd(gitRemoteAdd).Run() - if err != nil { - return err - } - - if checkoutBranch == "" { - return nil - } - - gitFetch, err := git.GitCommand("-C", path, "fetch", "origin", fmt.Sprintf("+refs/heads/%[1]s:refs/remotes/origin/%[1]s", checkoutBranch)) - if err != nil { - return err - } - gitFetch.Stdout = io.Out - gitFetch.Stderr = io.ErrOut - err = run.PrepareCmd(gitFetch).Run() - if err != nil { - return err - } - - gitCheckout, err := git.GitCommand("-C", path, "checkout", checkoutBranch) - if err != nil { - return err - } - gitCheckout.Stdout = io.Out - gitCheckout.Stderr = io.ErrOut - return run.PrepareCmd(gitCheckout).Run() -} - -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", +// name, description, and visibility +func interactiveRepoInfo(defaultName string) (string, string, string, error) { + qs := []*survey.Question{ + { + Name: "repoName", Prompt: &survey.Input{ - Message: "Repository description", + Message: "Repository Name: ", + Default: defaultName, }, - } - - qs = append(qs, repoDescriptionQuestion) - } - - if !isVisibilityPassed { - repoVisibilityQuestion := &survey.Question{ + }, + { + Name: "repoDescription", + Prompt: &survey.Input{Message: "Description: "}, + }, + { Name: "repoVisibility", Prompt: &survey.Select{ Message: "Visibility", Options: []string{"Public", "Private", "Internal"}, }, - } - qs = append(qs, repoVisibilityQuestion) - } + }} - answers := struct { + answer := struct { RepoName string RepoDescription string RepoVisibility string }{} - err := prompt.SurveyAsk(qs, &answers) - + err := prompt.SurveyAsk(qs, &answer) if err != nil { return "", "", "", err } - return answers.RepoName, answers.RepoDescription, strings.ToUpper(answers.RepoVisibility), nil + return answer.RepoName, answer.RepoDescription, strings.ToUpper(answer.RepoVisibility), nil } -func confirmSubmission(repoName string, repoOwner string, inLocalRepo bool) (bool, error) { +func interactiveSource() (string, error) { + var sourcePath string + sourcePrompt := &survey.Input{ + Message: "Path to local repository: ", + Default: "."} + + err := prompt.SurveyAskOne(sourcePrompt, &sourcePath) + if err != nil { + return "", err + } + return sourcePath, nil +} + +func confirmSubmission(repoName, repoOwner, visibility string) (bool, error) { qs := []*survey.Question{} - promptString := "" - if inLocalRepo { - promptString = `This will add an "origin" git remote to your local repository. Continue?` - } else { - targetRepo := repoName - if repoOwner != "" { - targetRepo = fmt.Sprintf("%s/%s", repoOwner, repoName) - } - promptString = fmt.Sprintf(`This will create the "%s" repository on GitHub. Continue?`, targetRepo) + targetRepo := repoName + if repoOwner != "" { + targetRepo = fmt.Sprintf("%s/%s", repoOwner, repoName) } + promptString := fmt.Sprintf(`This will create "%s" as a %s repository on GitHub. Continue?`, targetRepo, strings.ToLower(visibility)) confirmSubmitQuestion := &survey.Question{ Name: "confirmSubmit", @@ -659,27 +854,3 @@ func confirmSubmission(repoName string, repoOwner string, inLocalRepo bool) (boo 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 b9b10f43d..e9d6f8773 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -2,774 +2,367 @@ package create import ( "bytes" - "encoding/json" - "io/ioutil" "net/http" "testing" - "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/run" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/prompt" - "github.com/cli/cli/v2/test" "github.com/google/shlex" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func runCommand(httpClient *http.Client, cli string, isTTY bool) (*test.CmdOut, error) { - io, _, stdout, stderr := iostreams.Test() - io.SetStdoutTTY(isTTY) - io.SetStdinTTY(isTTY) - fac := &cmdutil.Factory{ - IOStreams: io, - HttpClient: func() (*http.Client, error) { - return httpClient, nil +func TestNewCmdCreate(t *testing.T) { + tests := []struct { + name string + tty bool + cli string + wantsErr bool + errMsg string + wantsOpts CreateOptions + }{ + { + name: "no args tty", + tty: true, + cli: "", + wantsOpts: CreateOptions{Interactive: true}, }, - Config: func() (config.Config, error) { + { + name: "no args no-tty", + tty: false, + cli: "", + wantsErr: true, + errMsg: "at least one argument required in non-interactive mode", + }, + { + name: "new repo from remote", + cli: "NEWREPO --public --clone", + wantsOpts: CreateOptions{ + Name: "NEWREPO", + Public: true, + Clone: true}, + }, + { + name: "no visibility", + tty: true, + cli: "NEWREPO", + wantsErr: true, + errMsg: "`--public`, `--private`, or `--internal` required when not running interactively", + }, + { + name: "multiple visibility", + tty: true, + cli: "NEWREPO --public --private", + wantsErr: true, + errMsg: "expected exactly one of `--public`, `--private`, or `--internal`", + }, + { + name: "new remote from local", + cli: "--source=/path/to/repo --private", + wantsOpts: CreateOptions{ + Private: true, + Source: "/path/to/repo"}, + }, + { + name: "new remote from local with remote", + cli: "--source=/path/to/repo --public --remote upstream", + wantsOpts: CreateOptions{ + Public: true, + Source: "/path/to/repo", + Remote: "upstream", + }, + }, + { + name: "new remote from local with push", + cli: "--source=/path/to/repo --push --public", + wantsOpts: CreateOptions{ + Public: true, + Source: "/path/to/repo", + Push: true, + }, + }, + { + name: "new remote from local without visibility", + cli: "--source=/path/to/repo --push", + wantsOpts: CreateOptions{ + Source: "/path/to/repo", + Push: true, + }, + wantsErr: true, + errMsg: "`--public`, `--private`, or `--internal` required when not running interactively", + }, + { + name: "source with template", + cli: "--source=/path/to/repo --private --template mytemplate", + wantsErr: true, + errMsg: "the `--source` option is not supported with `--clone`, `--template`, `--license`, or `--gitignore`", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, _, _ := iostreams.Test() + io.SetStdinTTY(tt.tty) + io.SetStdoutTTY(tt.tty) + + f := &cmdutil.Factory{ + IOStreams: io, + } + + var opts *CreateOptions + cmd := NewCmdCreate(f, func(o *CreateOptions) error { + opts = o + return nil + }) + + // TODO STUPID HACK + // cobra aggressively adds help to all commands. since we're not running through the root command + // (which manages help when running for real) and since create has a '-h' flag (for homepage), + // cobra blows up when it tried to add a help flag and -h is already in use. This hack adds a + // dummy help flag with a random shorthand to get around this. + cmd.Flags().BoolP("help", "x", false, "") + + args, err := shlex.Split(tt.cli) + require.NoError(t, err) + cmd.SetArgs(args) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err = cmd.ExecuteC() + if tt.wantsErr { + assert.Error(t, err) + assert.Equal(t, tt.errMsg, err.Error()) + return + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.wantsOpts.Interactive, opts.Interactive) + assert.Equal(t, tt.wantsOpts.Source, opts.Source) + assert.Equal(t, tt.wantsOpts.Name, opts.Name) + assert.Equal(t, tt.wantsOpts.Public, opts.Public) + assert.Equal(t, tt.wantsOpts.Internal, opts.Internal) + assert.Equal(t, tt.wantsOpts.Private, opts.Private) + assert.Equal(t, tt.wantsOpts.Clone, opts.Clone) + }) + } +} + +func Test_createRun(t *testing.T) { + tests := []struct { + name string + tty bool + opts *CreateOptions + httpStubs func(*httpmock.Registry) + askStubs func(*prompt.AskStubber) + execStubs func(*run.CommandStubber) + wantStdout string + wantErr bool + errMsg string + }{ + { + name: "interactive create from scratch with gitignore and license", + opts: &CreateOptions{Interactive: true}, + tty: true, + wantStdout: "✓ Created repository OWNER/REPO on GitHub\n", + askStubs: func(as *prompt.AskStubber) { + as.StubOne("Create a new repository on GitHub from scratch") + as.Stub([]*prompt.QuestionStub{ + {Name: "repoName", Value: "REPO"}, + {Name: "repoDescription", Value: "my new repo"}, + {Name: "repoVisibility", Value: "PRIVATE"}, + }) + as.Stub([]*prompt.QuestionStub{ + {Name: "addGitIgnore", Value: true}}) + as.Stub([]*prompt.QuestionStub{ + {Name: "chooseGitIgnore", Value: "Go"}}) + as.Stub([]*prompt.QuestionStub{ + {Name: "addLicense", Value: true}}) + as.Stub([]*prompt.QuestionStub{ + {Name: "chooseLicense", Value: "GNU Lesser General Public License v3.0"}}) + as.Stub([]*prompt.QuestionStub{ + {Name: "confirmSubmit", Value: true}}) + as.StubOne(true) //clone locally? + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "gitignore/templates"), + httpmock.StringResponse(`["Actionscript","Android","AppceleratorTitanium","Autotools","Bancha","C","C++","Go"]`)) + reg.Register( + httpmock.REST("GET", "licenses"), + httpmock.StringResponse(`[{"key": "mit","name": "MIT License"},{"key": "lgpl-3.0","name": "GNU Lesser General Public License v3.0"}]`)) + reg.Register( + httpmock.REST("POST", "user/repos"), + httpmock.StringResponse(`{"name":"REPO", "owner":{"login": "OWNER"}, "html_url":"https://github.com/OWNER/REPO"}`)) + + }, + execStubs: func(cs *run.CommandStubber) { + cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "") + }, + }, + { + name: "interactive with existing repository public", + opts: &CreateOptions{Interactive: true}, + tty: true, + askStubs: func(as *prompt.AskStubber) { + as.StubOne("Push an existing local repository to GitHub") + as.StubOne(".") + as.Stub([]*prompt.QuestionStub{ + {Name: "repoName", Value: "REPO"}, + {Name: "repoDescription", Value: "my new repo"}, + {Name: "repoVisibility", Value: "PRIVATE"}, + }) + as.StubOne(false) + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`mutation RepositoryCreate\b`), + httpmock.StringResponse(` + { + "data": { + "createRepository": { + "repository": { + "id": "REPOID", + "name": "REPO", + "owner": {"login":"OWNER"}, + "url": "https://github.com/OWNER/REPO" + } + } + } + }`)) + }, + execStubs: func(cs *run.CommandStubber) { + cs.Register(`git -C . rev-parse --git-dir`, 0, ".git") + cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") + }, + wantStdout: "✓ Created repository OWNER/REPO on GitHub\n", + }, + { + name: "interactive with existing repository public add remote", + opts: &CreateOptions{Interactive: true}, + tty: true, + askStubs: func(as *prompt.AskStubber) { + as.StubOne("Push an existing local repository to GitHub") + as.StubOne(".") + as.Stub([]*prompt.QuestionStub{ + {Name: "repoName", Value: "REPO"}, + {Name: "repoDescription", Value: "my new repo"}, + {Name: "repoVisibility", Value: "PRIVATE"}, + }) + as.StubOne(true) //ask for adding a remote + as.StubOne("origin") //ask for remote name + as.StubOne(false) //ask to push to remote + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`mutation RepositoryCreate\b`), + httpmock.StringResponse(` + { + "data": { + "createRepository": { + "repository": { + "id": "REPOID", + "name": "REPO", + "owner": {"login":"OWNER"}, + "url": "https://github.com/OWNER/REPO" + } + } + } + }`)) + }, + execStubs: func(cs *run.CommandStubber) { + cs.Register(`git -C . rev-parse --git-dir`, 0, ".git") + cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") + cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") + }, + wantStdout: "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n", + }, + { + name: "interactive with existing repository public, add remote, and push", + opts: &CreateOptions{Interactive: true}, + tty: true, + askStubs: func(as *prompt.AskStubber) { + as.StubOne("Push an existing local repository to GitHub") + as.StubOne(".") + as.Stub([]*prompt.QuestionStub{ + {Name: "repoName", Value: "REPO"}, + {Name: "repoDescription", Value: "my new repo"}, + {Name: "repoVisibility", Value: "PRIVATE"}, + }) + as.StubOne(true) //ask for adding a remote + as.StubOne("origin") //ask for remote name + as.StubOne(true) //ask to push to remote + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`mutation RepositoryCreate\b`), + httpmock.StringResponse(` + { + "data": { + "createRepository": { + "repository": { + "id": "REPOID", + "name": "REPO", + "owner": {"login":"OWNER"}, + "url": "https://github.com/OWNER/REPO" + } + } + } + }`)) + }, + execStubs: func(cs *run.CommandStubber) { + cs.Register(`git -C . rev-parse --git-dir`, 0, ".git") + cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") + cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") + cs.Register(`git -C . push -u origin HEAD`, 0, "") + }, + wantStdout: "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n", + }, + } + for _, tt := range tests { + q, teardown := prompt.InitAskStubber() + defer teardown() + if tt.askStubs != nil { + tt.askStubs(q) + } + + reg := &httpmock.Registry{} + if tt.httpStubs != nil { + tt.httpStubs(reg) + } + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + tt.opts.Config = func() (config.Config, error) { return config.NewBlankConfig(), nil - }, - } - - cmd := NewCmdCreate(fac, nil) - - // TODO STUPID HACK - // cobra aggressively adds help to all commands. since we're not running through the root command - // (which manages help when running for real) and since create has a '-h' flag (for homepage), - // cobra blows up when it tried to add a help flag and -h is already in use. This hack adds a - // dummy help flag with a random shorthand to get around this. - cmd.Flags().BoolP("help", "x", false, "") - - argv, err := shlex.Split(cli) - cmd.SetArgs(argv) - - cmd.SetIn(&bytes.Buffer{}) - cmd.SetOut(&bytes.Buffer{}) - cmd.SetErr(&bytes.Buffer{}) - - if err != nil { - panic(err) - } - - _, err = cmd.ExecuteC() - - if err != nil { - return nil, err - } - - return &test.CmdOut{ - OutBuf: stdout, - ErrBuf: stderr}, nil -} - -func TestRepoCreate(t *testing.T) { - reg := &httpmock.Registry{} - reg.Register( - httpmock.GraphQL(`mutation RepositoryCreate\b`), - httpmock.StringResponse(` - { "data": { "createRepository": { - "repository": { - "id": "REPOID", - "url": "https://github.com/OWNER/REPO", - "name": "REPO", - "owner": { - "login": "OWNER" - } - } - } } }`)) - - httpClient := &http.Client{Transport: reg} - - cs, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - cs.Register(`git remote add -f origin https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git rev-parse --show-toplevel`, 0, "") - - as, surveyTearDown := prompt.InitAskStubber() - defer surveyTearDown() - - as.Stub([]*prompt.QuestionStub{ - { - Name: "repoVisibility", - Value: "PRIVATE", - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "addGitIgnore", - Value: false, - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "addLicense", - Value: false, - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "confirmSubmit", - Value: true, - }, - }) - - output, err := runCommand(httpClient, "REPO", true) - if err != nil { - t.Errorf("error running command `repo create`: %v", err) - } - - assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n", output.Stderr()) - - var reqBody struct { - Query string - Variables struct { - Input map[string]interface{} } - } - if len(reg.Requests) != 1 { - t.Fatalf("expected 1 HTTP request, got %d", len(reg.Requests)) - } - - bodyBytes, _ := ioutil.ReadAll(reg.Requests[0].Body) - _ = json.Unmarshal(bodyBytes, &reqBody) - if repoName := reqBody.Variables.Input["name"].(string); repoName != "REPO" { - t.Errorf("expected %q, got %q", "REPO", repoName) - } - if repoVisibility := reqBody.Variables.Input["visibility"].(string); repoVisibility != "PRIVATE" { - t.Errorf("expected %q, got %q", "PRIVATE", repoVisibility) - } - if _, ownerSet := reqBody.Variables.Input["ownerId"]; ownerSet { - t.Error("expected ownerId not to be set") - } -} - -func TestRepoCreate_outsideGitWorkDir(t *testing.T) { - reg := &httpmock.Registry{} - reg.Register( - httpmock.GraphQL(`mutation RepositoryCreate\b`), - httpmock.StringResponse(` - { "data": { "createRepository": { - "repository": { - "id": "REPOID", - "url": "https://github.com/OWNER/REPO", - "name": "REPO", - "owner": { - "login": "OWNER" - } - } - } } }`)) - - httpClient := &http.Client{Transport: reg} - - cs, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - cs.Register(`git rev-parse --show-toplevel`, 1, "") - cs.Register(`git init REPO`, 0, "") - cs.Register(`git -C REPO remote add origin https://github\.com/OWNER/REPO\.git`, 0, "") - - output, err := runCommand(httpClient, "REPO --private --confirm", false) - if err != nil { - t.Errorf("error running command `repo create`: %v", err) - } - - assert.Equal(t, "https://github.com/OWNER/REPO\n", output.String()) - assert.Equal(t, "", output.Stderr()) - - var reqBody struct { - Query string - Variables struct { - Input map[string]interface{} + cs, restoreRun := run.Stub() + defer restoreRun(t) + if tt.execStubs != nil { + tt.execStubs(cs) } - } - if len(reg.Requests) != 1 { - t.Fatalf("expected 1 HTTP request, got %d", len(reg.Requests)) - } + io, _, stdout, _ := iostreams.Test() + io.SetStdinTTY(tt.tty) + io.SetStdoutTTY(tt.tty) + tt.opts.IO = io - bodyBytes, _ := ioutil.ReadAll(reg.Requests[0].Body) - _ = json.Unmarshal(bodyBytes, &reqBody) - if repoName := reqBody.Variables.Input["name"].(string); repoName != "REPO" { - t.Errorf("expected %q, got %q", "REPO", repoName) - } - if repoVisibility := reqBody.Variables.Input["visibility"].(string); repoVisibility != "PRIVATE" { - t.Errorf("expected %q, got %q", "PRIVATE", repoVisibility) - } - if _, ownerSet := reqBody.Variables.Input["ownerId"]; ownerSet { - t.Error("expected ownerId not to be set") - } -} - -func TestRepoCreate_org(t *testing.T) { - reg := &httpmock.Registry{} - reg.Register( - httpmock.REST("GET", "users/ORG"), - httpmock.StringResponse(` - { "node_id": "ORGID" - }`)) - reg.Register( - httpmock.GraphQL(`mutation RepositoryCreate\b`), - httpmock.StringResponse(` - { "data": { "createRepository": { - "repository": { - "id": "REPOID", - "url": "https://github.com/ORG/REPO", - "name": "REPO", - "owner": { - "login": "ORG" - } + t.Run(tt.name, func(t *testing.T) { + defer reg.Verify(t) + err := createRun(tt.opts) + if tt.wantErr { + assert.Error(t, err) + assert.Equal(t, tt.errMsg, err.Error()) + return } - } } }`)) - httpClient := &http.Client{Transport: reg} - - cs, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - cs.Register(`git remote add -f origin https://github\.com/ORG/REPO\.git`, 0, "") - cs.Register(`git rev-parse --show-toplevel`, 0, "") - - as, surveyTearDown := prompt.InitAskStubber() - defer surveyTearDown() - - as.Stub([]*prompt.QuestionStub{ - { - Name: "repoVisibility", - Value: "PRIVATE", - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "addGitIgnore", - Value: false, - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "addLicense", - Value: false, - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "confirmSubmit", - Value: true, - }, - }) - - output, err := runCommand(httpClient, "ORG/REPO", true) - if err != nil { - t.Errorf("error running command `repo create`: %v", err) - } - - assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created repository ORG/REPO on GitHub\n✓ Added remote https://github.com/ORG/REPO.git\n", output.Stderr()) - - var reqBody struct { - Query string - Variables struct { - Input map[string]interface{} - } - } - - if len(reg.Requests) != 2 { - t.Fatalf("expected 2 HTTP requests, got %d", len(reg.Requests)) - } - - assert.Equal(t, "/users/ORG", reg.Requests[0].URL.Path) - - bodyBytes, _ := ioutil.ReadAll(reg.Requests[1].Body) - _ = json.Unmarshal(bodyBytes, &reqBody) - if orgID := reqBody.Variables.Input["ownerId"].(string); orgID != "ORGID" { - t.Errorf("expected %q, got %q", "ORGID", orgID) - } - if _, teamSet := reqBody.Variables.Input["teamId"]; teamSet { - t.Error("expected teamId not to be set") - } -} - -func TestRepoCreate_orgWithTeam(t *testing.T) { - reg := &httpmock.Registry{} - reg.Register( - httpmock.REST("GET", "orgs/ORG/teams/monkeys"), - httpmock.StringResponse(` - { "node_id": "TEAMID", - "organization": { "node_id": "ORGID" } - }`)) - reg.Register( - httpmock.GraphQL(`mutation RepositoryCreate\b`), - httpmock.StringResponse(` - { "data": { "createRepository": { - "repository": { - "id": "REPOID", - "url": "https://github.com/ORG/REPO", - "name": "REPO", - "owner": { - "login": "ORG" - } - } - } } }`)) - httpClient := &http.Client{Transport: reg} - - cs, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - cs.Register(`git remote add -f origin https://github\.com/ORG/REPO\.git`, 0, "") - cs.Register(`git rev-parse --show-toplevel`, 0, "") - - as, surveyTearDown := prompt.InitAskStubber() - defer surveyTearDown() - - as.Stub([]*prompt.QuestionStub{ - { - Name: "repoVisibility", - Value: "PRIVATE", - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "addGitIgnore", - Value: false, - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "addLicense", - Value: false, - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "confirmSubmit", - Value: true, - }, - }) - - output, err := runCommand(httpClient, "ORG/REPO --team monkeys", true) - if err != nil { - t.Errorf("error running command `repo create`: %v", err) - } - - assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created repository ORG/REPO on GitHub\n✓ Added remote https://github.com/ORG/REPO.git\n", output.Stderr()) - - var reqBody struct { - Query string - Variables struct { - Input map[string]interface{} - } - } - - if len(reg.Requests) != 2 { - t.Fatalf("expected 2 HTTP requests, got %d", len(reg.Requests)) - } - - assert.Equal(t, "/orgs/ORG/teams/monkeys", reg.Requests[0].URL.Path) - - bodyBytes, _ := ioutil.ReadAll(reg.Requests[1].Body) - _ = json.Unmarshal(bodyBytes, &reqBody) - if orgID := reqBody.Variables.Input["ownerId"].(string); orgID != "ORGID" { - t.Errorf("expected %q, got %q", "ORGID", orgID) - } - if teamID := reqBody.Variables.Input["teamId"].(string); teamID != "TEAMID" { - t.Errorf("expected %q, got %q", "TEAMID", teamID) - } -} - -func TestRepoCreate_template(t *testing.T) { - reg := &httpmock.Registry{} - defer reg.Verify(t) - reg.Register( - httpmock.GraphQL(`mutation CloneTemplateRepository\b`), - httpmock.StringResponse(` - { "data": { "cloneTemplateRepository": { - "repository": { - "id": "REPOID", - "name": "REPO", - "owner": { - "login": "OWNER" - }, - "url": "https://github.com/OWNER/REPO" - } - } } }`)) - - reg.StubRepoInfoResponse("OWNER", "REPO", "main") - - reg.Register( - httpmock.GraphQL(`query UserCurrent\b`), - httpmock.StringResponse(`{"data":{"viewer":{"ID":"OWNERID"}}}`)) - - httpClient := &http.Client{Transport: reg} - - cs, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - cs.Register(`git rev-parse --show-toplevel`, 1, "") - cs.Register(`git init REPO`, 0, "") - cs.Register(`git -C REPO remote add`, 0, "") - cs.Register(`git -C REPO fetch origin \+refs/heads/main:refs/remotes/origin/main`, 0, "") - cs.Register(`git -C REPO checkout main`, 0, "") - - _, surveyTearDown := prompt.InitAskStubber() - defer surveyTearDown() - - output, err := runCommand(httpClient, "REPO -y --private --template='OWNER/REPO'", true) - if err != nil { - t.Errorf("error running command `repo create`: %v", err) - return - } - - assert.Equal(t, "", output.String()) - assert.Equal(t, heredoc.Doc(` - ✓ Created repository OWNER/REPO on GitHub - ✓ Initialized repository in "REPO" - `), output.Stderr()) - - var reqBody struct { - Query string - Variables struct { - Input map[string]interface{} - } - } - - bodyBytes, _ := ioutil.ReadAll(reg.Requests[2].Body) - _ = json.Unmarshal(bodyBytes, &reqBody) - if repoName := reqBody.Variables.Input["name"].(string); repoName != "REPO" { - t.Errorf("expected %q, got %q", "REPO", repoName) - } - if repoVisibility := reqBody.Variables.Input["visibility"].(string); repoVisibility != "PRIVATE" { - t.Errorf("expected %q, got %q", "PRIVATE", repoVisibility) - } - if ownerId := reqBody.Variables.Input["ownerId"].(string); ownerId != "OWNERID" { - t.Errorf("expected %q, got %q", "OWNERID", ownerId) - } -} - -func TestRepoCreate_withoutNameArg(t *testing.T) { - reg := &httpmock.Registry{} - reg.Register( - httpmock.REST("GET", "users/OWNER"), - httpmock.StringResponse(`{ "node_id": "OWNERID" }`)) - reg.Register( - httpmock.GraphQL(`mutation RepositoryCreate\b`), - httpmock.StringResponse(` - { "data": { "createRepository": { - "repository": { - "id": "REPOID", - "url": "https://github.com/OWNER/REPO", - "name": "REPO", - "owner": { - "login": "OWNER" - } - } - } } }`)) - httpClient := &http.Client{Transport: reg} - - cs, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - cs.Register(`git remote add -f origin https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git rev-parse --show-toplevel`, 0, "") - - as, surveyTearDown := prompt.InitAskStubber() - defer surveyTearDown() - - as.Stub([]*prompt.QuestionStub{ - { - Name: "repoName", - Value: "OWNER/REPO", - }, - { - Name: "repoDescription", - Value: "DESCRIPTION", - }, - { - Name: "repoVisibility", - Value: "PRIVATE", - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "confirmSubmit", - Value: true, - }, - }) - - output, err := runCommand(httpClient, "", true) - if err != nil { - t.Errorf("error running command `repo create`: %v", err) - } - - assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n", output.Stderr()) - - var reqBody struct { - Query string - Variables struct { - Input map[string]interface{} - } - } - - if len(reg.Requests) != 2 { - t.Fatalf("expected 2 HTTP request, got %d", len(reg.Requests)) - } - - bodyBytes, _ := ioutil.ReadAll(reg.Requests[1].Body) - _ = json.Unmarshal(bodyBytes, &reqBody) - if repoName := reqBody.Variables.Input["name"].(string); repoName != "REPO" { - t.Errorf("expected %q, got %q", "REPO", repoName) - } - if repoVisibility := reqBody.Variables.Input["visibility"].(string); repoVisibility != "PRIVATE" { - t.Errorf("expected %q, got %q", "PRIVATE", repoVisibility) - } - if ownerId := reqBody.Variables.Input["ownerId"].(string); ownerId != "OWNERID" { - t.Errorf("expected %q, got %q", "OWNERID", ownerId) - } -} - -func TestRepoCreate_WithGitIgnore(t *testing.T) { - cs, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - cs.Register(`git remote add -f origin https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git rev-parse --show-toplevel`, 0, "") - - as, surveyTearDown := prompt.InitAskStubber() - defer surveyTearDown() - - as.Stub([]*prompt.QuestionStub{ - { - Name: "repoVisibility", - Value: "PRIVATE", - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "addGitIgnore", - Value: true, - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "chooseGitIgnore", - Value: "Go", - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "addLicense", - Value: false, - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "confirmSubmit", - Value: true, - }, - }) - - reg := &httpmock.Registry{} - reg.Register( - httpmock.REST("GET", "users/OWNER"), - httpmock.StringResponse(`{ "node_id": "OWNERID" }`)) - reg.Register( - httpmock.REST("GET", "gitignore/templates"), - httpmock.StringResponse(`["Actionscript","Android","AppceleratorTitanium","Autotools","Bancha","C","C++","Go"]`)) - reg.Register( - httpmock.REST("POST", "user/repos"), - httpmock.StringResponse(`{"name":"REPO", "owner":{"login": "OWNER"}, "html_url":"https://github.com/OWNER/REPO"}`)) - httpClient := &http.Client{Transport: reg} - - output, err := runCommand(httpClient, "OWNER/REPO", true) - if err != nil { - t.Errorf("error running command `repo create`: %v", err) - } - - assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n", output.Stderr()) - - if len(reg.Requests) != 3 { - t.Fatalf("expected 3 HTTP request, got %d", len(reg.Requests)) - } - - reqBody := make(map[string]interface{}) - dec := json.NewDecoder(reg.Requests[2].Body) - assert.NoError(t, dec.Decode(&reqBody)) - - if gitignore := reqBody["gitignore_template"]; gitignore != "Go" { - t.Errorf("expected %q, got %q", "Go", gitignore) - } - if license := reqBody["license_template"]; license != nil { - t.Errorf("expected %v, got %v", nil, license) - } -} - -func TestRepoCreate_WithBothGitIgnoreLicense(t *testing.T) { - cs, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - cs.Register(`git remote add -f origin https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git rev-parse --show-toplevel`, 0, "") - - as, surveyTearDown := prompt.InitAskStubber() - defer surveyTearDown() - - as.Stub([]*prompt.QuestionStub{ - { - Name: "repoVisibility", - Value: "PRIVATE", - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "addGitIgnore", - Value: true, - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "chooseGitIgnore", - Value: "Go", - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "addLicense", - Value: true, - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "chooseLicense", - Value: "GNU Lesser General Public License v3.0", - }, - }) - - as.Stub([]*prompt.QuestionStub{ - { - Name: "confirmSubmit", - Value: true, - }, - }) - - reg := &httpmock.Registry{} - reg.Register( - httpmock.REST("GET", "users/OWNER"), - httpmock.StringResponse(`{ "node_id": "OWNERID" }`)) - reg.Register( - httpmock.REST("GET", "gitignore/templates"), - httpmock.StringResponse(`["Actionscript","Android","AppceleratorTitanium","Autotools","Bancha","C","C++","Go"]`)) - reg.Register( - httpmock.REST("GET", "licenses"), - httpmock.StringResponse(`[{"key": "mit","name": "MIT License"},{"key": "lgpl-3.0","name": "GNU Lesser General Public License v3.0"}]`)) - reg.Register( - httpmock.REST("POST", "user/repos"), - httpmock.StringResponse(`{"name":"REPO", "owner":{"login": "OWNER"}, "html_url":"https://github.com/OWNER/REPO"}`)) - httpClient := &http.Client{Transport: reg} - - output, err := runCommand(httpClient, "OWNER/REPO", true) - if err != nil { - t.Errorf("error running command `repo create`: %v", err) - } - - assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n", output.Stderr()) - - if len(reg.Requests) != 4 { - t.Fatalf("expected 4 HTTP request, got %d", len(reg.Requests)) - } - - reqBody := make(map[string]interface{}) - dec := json.NewDecoder(reg.Requests[3].Body) - assert.NoError(t, dec.Decode(&reqBody)) - - if gitignore := reqBody["gitignore_template"]; gitignore != "Go" { - t.Errorf("expected %q, got %q", "Go", gitignore) - } - if license := reqBody["license_template"]; license != "lgpl-3.0" { - t.Errorf("expected %q, got %q", "lgpl-3.0", license) - } -} - -func TestRepoCreate_WithConfirmFlag(t *testing.T) { - cs, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - cs.Register(`git remote add -f origin https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git rev-parse --show-toplevel`, 0, "") - - reg := &httpmock.Registry{} - - reg.Register( - httpmock.GraphQL(`mutation RepositoryCreate\b`), - httpmock.StringResponse(` - { "data": { "createRepository": { - "repository": { - "id": "REPOID", - "url": "https://github.com/OWNER/REPO", - "name": "REPO", - "owner": { - "login": "OWNER" - } - } - } } }`), - ) - - reg.Register( - httpmock.REST("GET", "users/OWNER"), - httpmock.StringResponse(`{ "node_id": "OWNERID" }`), - ) - - httpClient := &http.Client{Transport: reg} - - in := "OWNER/REPO --confirm --private" - output, err := runCommand(httpClient, in, true) - if err != nil { - t.Errorf("error running command `repo create %v`: %v", in, err) - } - - assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n", output.Stderr()) - - var reqBody struct { - Query string - Variables struct { - Input map[string]interface{} - } - } - - if len(reg.Requests) != 2 { - t.Fatalf("expected 2 HTTP request, got %d", len(reg.Requests)) - } - - bodyBytes, _ := ioutil.ReadAll(reg.Requests[1].Body) - _ = json.Unmarshal(bodyBytes, &reqBody) - if repoName := reqBody.Variables.Input["name"].(string); repoName != "REPO" { - t.Errorf("expected %q, got %q", "REPO", repoName) - } - if repoVisibility := reqBody.Variables.Input["visibility"].(string); repoVisibility != "PRIVATE" { - t.Errorf("expected %q, got %q", "PRIVATE", repoVisibility) + assert.NoError(t, err) + assert.Equal(t, tt.wantStdout, stdout.String()) + }) } }