Merge pull request #1421 from cli/migrate-repo-create

isolate repo create command
This commit is contained in:
Nate Smith 2020-07-27 10:29:08 -05:00 committed by GitHub
commit d84c202a6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 640 additions and 552 deletions

View file

@ -2,32 +2,10 @@ package api
import (
"context"
"fmt"
"github.com/shurcooL/githubv4"
)
// using API v3 here because the equivalent in GraphQL needs `read:org` scope
func resolveOrganization(client *Client, orgName string) (string, error) {
var response struct {
NodeID string `json:"node_id"`
}
err := client.REST("GET", fmt.Sprintf("users/%s", orgName), nil, &response)
return response.NodeID, err
}
// using API v3 here because the equivalent in GraphQL needs `read:org` scope
func resolveOrganizationTeam(client *Client, orgName, teamSlug string) (string, string, error) {
var response struct {
NodeID string `json:"node_id"`
Organization struct {
NodeID string `json:"node_id"`
}
}
err := client.REST("GET", fmt.Sprintf("orgs/%s/teams/%s", orgName, teamSlug), nil, &response)
return response.Organization.NodeID, response.NodeID, err
}
// OrganizationProjects fetches all open projects for an organization
func OrganizationProjects(client *Client, owner string) ([]RepoProject, error) {
var query struct {

View file

@ -109,7 +109,7 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
return nil, err
}
return initRepoHostname(&result.Repository, repo.RepoHost()), nil
return InitRepoHostname(&result.Repository, repo.RepoHost()), nil
}
func RepoDefaultBranch(client *Client, repo ghrepo.Interface) (string, error) {
@ -250,7 +250,7 @@ func RepoNetwork(client *Client, repos []ghrepo.Interface) (RepoNetworkResult, e
if err := decoder.Decode(&repo); err != nil {
return result, err
}
result.Repositories = append(result.Repositories, initRepoHostname(&repo, hostname))
result.Repositories = append(result.Repositories, InitRepoHostname(&repo, hostname))
} else {
return result, fmt.Errorf("unknown GraphQL result key %q", name)
}
@ -258,7 +258,7 @@ func RepoNetwork(client *Client, repos []ghrepo.Interface) (RepoNetworkResult, e
return result, nil
}
func initRepoHostname(repo *Repository, hostname string) *Repository {
func InitRepoHostname(repo *Repository, hostname string) *Repository {
repo.hostname = hostname
if repo.Parent != nil {
repo.Parent.hostname = hostname
@ -338,72 +338,11 @@ func RepoFindFork(client *Client, repo ghrepo.Interface) (*Repository, error) {
// `affiliations` condition, to guard against versions of GitHub with a
// faulty `affiliations` implementation
if len(forks) > 0 && forks[0].ViewerCanPush() {
return initRepoHostname(&forks[0], repo.RepoHost()), nil
return InitRepoHostname(&forks[0], repo.RepoHost()), nil
}
return nil, &NotFoundError{errors.New("no fork found")}
}
// RepoCreateInput represents input parameters for RepoCreate
type RepoCreateInput struct {
Name string `json:"name"`
Visibility string `json:"visibility"`
HomepageURL string `json:"homepageUrl,omitempty"`
Description string `json:"description,omitempty"`
OwnerID string `json:"ownerId,omitempty"`
TeamID string `json:"teamId,omitempty"`
HasIssuesEnabled bool `json:"hasIssuesEnabled"`
HasWikiEnabled bool `json:"hasWikiEnabled"`
}
// RepoCreate creates a new GitHub repository
func RepoCreate(client *Client, input RepoCreateInput) (*Repository, error) {
var response struct {
CreateRepository struct {
Repository Repository
}
}
if input.TeamID != "" {
orgID, teamID, err := resolveOrganizationTeam(client, input.OwnerID, input.TeamID)
if err != nil {
return nil, err
}
input.TeamID = teamID
input.OwnerID = orgID
} else if input.OwnerID != "" {
orgID, err := resolveOrganization(client, input.OwnerID)
if err != nil {
return nil, err
}
input.OwnerID = orgID
}
variables := map[string]interface{}{
"input": input,
}
err := client.GraphQL(`
mutation RepositoryCreate($input: CreateRepositoryInput!) {
createRepository(input: $input) {
repository {
id
name
owner { login }
url
}
}
}
`, variables, &response)
if err != nil {
return nil, err
}
// FIXME: support Enterprise hosts
return initRepoHostname(&response.CreateRepository.Repository, "github.com"), nil
}
type RepoMetadataResult struct {
AssignableUsers []RepoAssignee
Labels []RepoLabel

View file

@ -1,51 +1,12 @@
package api
import (
"bytes"
"encoding/json"
"io/ioutil"
"testing"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/httpmock"
)
func Test_RepoCreate(t *testing.T) {
http := &httpmock.Registry{}
client := NewClient(ReplaceTripper(http))
http.StubResponse(200, bytes.NewBufferString(`{}`))
input := RepoCreateInput{
Description: "roasted chesnuts",
HomepageURL: "http://example.com",
}
_, err := RepoCreate(client, input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(http.Requests) != 1 {
t.Fatalf("expected 1 HTTP request, seen %d", len(http.Requests))
}
var reqBody struct {
Query string
Variables struct {
Input map[string]interface{}
}
}
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
_ = json.Unmarshal(bodyBytes, &reqBody)
if description := reqBody.Variables.Input["description"].(string); description != "roasted chesnuts" {
t.Errorf("expected description to be %q, got %q", "roasted chesnuts", description)
}
if homepage := reqBody.Variables.Input["homepageUrl"].(string); homepage != "http://example.com" {
t.Errorf("expected homepageUrl to be %q, got %q", "http://example.com", homepage)
}
}
func Test_RepoMetadata(t *testing.T) {
http := &httpmock.Registry{}
client := NewClient(ReplaceTripper(http))

View file

@ -4,30 +4,20 @@ import (
"errors"
"fmt"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/api"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/internal/run"
"github.com/cli/cli/pkg/prompt"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
)
func init() {
repoCmd.AddCommand(repoCreateCmd)
repoCreateCmd.Flags().StringP("description", "d", "", "Description of repository")
repoCreateCmd.Flags().StringP("homepage", "h", "", "Repository home page URL")
repoCreateCmd.Flags().StringP("team", "t", "", "The name of the organization team to be granted access")
repoCreateCmd.Flags().Bool("enable-issues", true, "Enable issues in the new repository")
repoCreateCmd.Flags().Bool("enable-wiki", true, "Enable wiki in the new repository")
repoCreateCmd.Flags().Bool("public", false, "Make the new repository public (default: private)")
repoCmd.AddCommand(repoForkCmd)
repoForkCmd.Flags().String("clone", "prompt", "Clone fork: {true|false|prompt}")
repoForkCmd.Flags().String("remote", "prompt", "Add remote for fork: {true|false|prompt}")
@ -55,26 +45,6 @@ A repository can be supplied as an argument in any of the following formats:
- by URL, e.g. "https://github.com/OWNER/REPO"`},
}
var repoCreateCmd = &cobra.Command{
Use: "create [<name>]",
Short: "Create a new repository",
Long: `Create a new GitHub repository.`,
Example: heredoc.Doc(`
# create a repository under your account using the current directory name
$ gh repo create
# create a repository with a specific name
$ gh repo create my-project
# create a repository in an organization
$ gh repo create cli/my-project
`),
Annotations: map[string]string{"help:arguments": `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"`},
RunE: repoCreate,
}
var repoForkCmd = &cobra.Command{
Use: "fork [<repository>]",
Short: "Create a fork of a repository",
@ -105,141 +75,6 @@ var repoCreditsCmd = &cobra.Command{
Hidden: true,
}
func repoCreate(cmd *cobra.Command, args []string) error {
projectDir, projectDirErr := git.ToplevelDir()
orgName := ""
teamSlug, err := cmd.Flags().GetString("team")
if err != nil {
return err
}
var name string
if len(args) > 0 {
name = args[0]
if strings.Contains(name, "/") {
newRepo, err := ghrepo.FromFullName(name)
if err != nil {
return fmt.Errorf("argument error: %w", err)
}
orgName = newRepo.RepoOwner()
name = newRepo.RepoName()
}
} else {
if projectDirErr != nil {
return projectDirErr
}
name = path.Base(projectDir)
}
isPublic, err := cmd.Flags().GetBool("public")
if err != nil {
return err
}
hasIssuesEnabled, err := cmd.Flags().GetBool("enable-issues")
if err != nil {
return err
}
hasWikiEnabled, err := cmd.Flags().GetBool("enable-wiki")
if err != nil {
return err
}
description, err := cmd.Flags().GetString("description")
if err != nil {
return err
}
homepage, err := cmd.Flags().GetString("homepage")
if err != nil {
return err
}
// TODO: move this into constant within `api`
visibility := "PRIVATE"
if isPublic {
visibility = "PUBLIC"
}
input := api.RepoCreateInput{
Name: name,
Visibility: visibility,
OwnerID: orgName,
TeamID: teamSlug,
Description: description,
HomepageURL: homepage,
HasIssuesEnabled: hasIssuesEnabled,
HasWikiEnabled: hasWikiEnabled,
}
ctx := contextForCommand(cmd)
client, err := apiClientForContext(ctx)
if err != nil {
return err
}
repo, err := api.RepoCreate(client, input)
if err != nil {
return err
}
out := cmd.OutOrStdout()
greenCheck := utils.Green("✓")
isTTY := false
if outFile, isFile := out.(*os.File); isFile {
isTTY = utils.IsTerminal(outFile)
if isTTY {
// FIXME: duplicates colorableOut
out = utils.NewColorable(outFile)
}
}
if isTTY {
fmt.Fprintf(out, "%s Created repository %s on GitHub\n", greenCheck, ghrepo.FullName(repo))
} else {
fmt.Fprintln(out, repo.URL)
}
remoteURL := formatRemoteURL(cmd, repo)
if projectDirErr == nil {
_, err = git.AddRemote("origin", remoteURL)
if err != nil {
return err
}
if isTTY {
fmt.Fprintf(out, "%s Added remote %s\n", greenCheck, remoteURL)
}
} else if isTTY {
doSetup := false
err := Confirm(fmt.Sprintf("Create a local project directory for %s?", ghrepo.FullName(repo)), &doSetup)
if err != nil {
return err
}
if doSetup {
path := repo.Name
gitInit := git.GitCommand("init", path)
gitInit.Stdout = os.Stdout
gitInit.Stderr = os.Stderr
err = run.PrepareCmd(gitInit).Run()
if err != nil {
return err
}
gitRemoteAdd := git.GitCommand("-C", path, "remote", "add", "origin", remoteURL)
gitRemoteAdd.Stdout = os.Stdout
gitRemoteAdd.Stderr = os.Stderr
err = run.PrepareCmd(gitRemoteAdd).Run()
if err != nil {
return err
}
fmt.Fprintf(out, "%s Initialized repository in './%s/'\n", greenCheck, path)
}
}
return nil
}
var Since = func(t time.Time) time.Duration {
return time.Since(t)
}
@ -371,7 +206,7 @@ func repoFork(cmd *cobra.Command, args []string) error {
remoteDesired := remotePref == "true"
if remotePref == "prompt" {
err = Confirm("Would you like to add a remote for the fork?", &remoteDesired)
err = prompt.Confirm("Would you like to add a remote for the fork?", &remoteDesired)
if err != nil {
return fmt.Errorf("failed to prompt: %w", err)
}
@ -409,7 +244,7 @@ func repoFork(cmd *cobra.Command, args []string) error {
} else {
cloneDesired := clonePref == "true"
if clonePref == "prompt" {
err = Confirm("Would you like to clone the fork?", &cloneDesired)
err = prompt.Confirm("Would you like to clone the fork?", &cloneDesired)
if err != nil {
return fmt.Errorf("failed to prompt: %w", err)
}
@ -447,14 +282,6 @@ func repoFork(cmd *cobra.Command, args []string) error {
return nil
}
var Confirm = func(prompt string, result *bool) error {
p := &survey.Confirm{
Message: prompt,
Default: true,
}
return survey.AskOne(p, result)
}
func repoCredits(cmd *cobra.Command, args []string) error {
return credits(cmd, args)
}

View file

@ -1,8 +1,6 @@
package command
import (
"encoding/json"
"io/ioutil"
"os/exec"
"regexp"
"strings"
@ -13,7 +11,7 @@ import (
"github.com/cli/cli/context"
"github.com/cli/cli/internal/run"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/pkg/prompt"
"github.com/cli/cli/test"
"github.com/cli/cli/utils"
"github.com/stretchr/testify/assert"
@ -296,12 +294,7 @@ func TestRepoFork_outside_survey_yes(t *testing.T) {
cs.Stub("") // git clone
cs.Stub("") // git remote add
oldConfirm := Confirm
Confirm = func(_ string, result *bool) error {
*result = true
return nil
}
defer func() { Confirm = oldConfirm }()
defer prompt.StubConfirm(true)()
output, err := RunCommand("repo fork OWNER/REPO")
if err != nil {
@ -331,12 +324,7 @@ func TestRepoFork_outside_survey_no(t *testing.T) {
return &test.OutputStub{}
})()
oldConfirm := Confirm
Confirm = func(_ string, result *bool) error {
*result = false
return nil
}
defer func() { Confirm = oldConfirm }()
defer prompt.StubConfirm(false)()
output, err := RunCommand("repo fork OWNER/REPO")
if err != nil {
@ -369,12 +357,7 @@ func TestRepoFork_in_parent_survey_yes(t *testing.T) {
return &test.OutputStub{}
})()
oldConfirm := Confirm
Confirm = func(_ string, result *bool) error {
*result = true
return nil
}
defer func() { Confirm = oldConfirm }()
defer prompt.StubConfirm(true)()
output, err := RunCommand("repo fork")
if err != nil {
@ -413,12 +396,7 @@ func TestRepoFork_in_parent_survey_no(t *testing.T) {
return &test.OutputStub{}
})()
oldConfirm := Confirm
Confirm = func(_ string, result *bool) error {
*result = false
return nil
}
defer func() { Confirm = oldConfirm }()
defer prompt.StubConfirm(false)()
output, err := RunCommand("repo fork")
if err != nil {
@ -435,210 +413,3 @@ func TestRepoFork_in_parent_survey_no(t *testing.T) {
return
}
}
func TestRepoCreate(t *testing.T) {
ctx := context.NewBlank()
ctx.SetBranch("master")
initContext = func() context.Context {
return ctx
}
http := initFakeHTTP()
http.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.StringResponse(`
{ "data": { "createRepository": {
"repository": {
"id": "REPOID",
"url": "https://github.com/OWNER/REPO",
"name": "REPO",
"owner": {
"login": "OWNER"
}
}
} } }`))
var seenCmd *exec.Cmd
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
output, err := RunCommand("repo create REPO")
if err != nil {
t.Errorf("error running command `repo create`: %v", err)
}
eq(t, output.String(), "https://github.com/OWNER/REPO\n")
eq(t, output.Stderr(), "")
if seenCmd == nil {
t.Fatal("expected a command to run")
}
eq(t, strings.Join(seenCmd.Args, " "), "git remote add -f origin https://github.com/OWNER/REPO.git")
var reqBody struct {
Query string
Variables struct {
Input map[string]interface{}
}
}
if len(http.Requests) != 1 {
t.Fatalf("expected 1 HTTP request, got %d", len(http.Requests))
}
bodyBytes, _ := ioutil.ReadAll(http.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) {
ctx := context.NewBlank()
ctx.SetBranch("master")
initContext = func() context.Context {
return ctx
}
http := initFakeHTTP()
http.Register(
httpmock.REST("GET", "users/ORG"),
httpmock.StringResponse(`
{ "node_id": "ORGID"
}`))
http.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.StringResponse(`
{ "data": { "createRepository": {
"repository": {
"id": "REPOID",
"url": "https://github.com/ORG/REPO",
"name": "REPO",
"owner": {
"login": "ORG"
}
}
} } }`))
var seenCmd *exec.Cmd
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
output, err := RunCommand("repo create ORG/REPO")
if err != nil {
t.Errorf("error running command `repo create`: %v", err)
}
eq(t, output.String(), "https://github.com/ORG/REPO\n")
eq(t, output.Stderr(), "")
if seenCmd == nil {
t.Fatal("expected a command to run")
}
eq(t, strings.Join(seenCmd.Args, " "), "git remote add -f origin https://github.com/ORG/REPO.git")
var reqBody struct {
Query string
Variables struct {
Input map[string]interface{}
}
}
if len(http.Requests) != 2 {
t.Fatalf("expected 2 HTTP requests, got %d", len(http.Requests))
}
eq(t, http.Requests[0].URL.Path, "/users/ORG")
bodyBytes, _ := ioutil.ReadAll(http.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) {
ctx := context.NewBlank()
ctx.SetBranch("master")
initContext = func() context.Context {
return ctx
}
http := initFakeHTTP()
http.Register(
httpmock.REST("GET", "orgs/ORG/teams/monkeys"),
httpmock.StringResponse(`
{ "node_id": "TEAMID",
"organization": { "node_id": "ORGID" }
}`))
http.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.StringResponse(`
{ "data": { "createRepository": {
"repository": {
"id": "REPOID",
"url": "https://github.com/ORG/REPO",
"name": "REPO",
"owner": {
"login": "ORG"
}
}
} } }`))
var seenCmd *exec.Cmd
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
output, err := RunCommand("repo create ORG/REPO --team monkeys")
if err != nil {
t.Errorf("error running command `repo create`: %v", err)
}
eq(t, output.String(), "https://github.com/ORG/REPO\n")
eq(t, output.Stderr(), "")
if seenCmd == nil {
t.Fatal("expected a command to run")
}
eq(t, strings.Join(seenCmd.Args, " "), "git remote add -f origin https://github.com/ORG/REPO.git")
var reqBody struct {
Query string
Variables struct {
Input map[string]interface{}
}
}
if len(http.Requests) != 2 {
t.Fatalf("expected 2 HTTP requests, got %d", len(http.Requests))
}
eq(t, http.Requests[0].URL.Path, "/orgs/ORG/teams/monkeys")
bodyBytes, _ := ioutil.ReadAll(http.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)
}
}

View file

@ -22,6 +22,7 @@ import (
apiCmd "github.com/cli/cli/pkg/cmd/api"
gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create"
repoCloneCmd "github.com/cli/cli/pkg/cmd/repo/clone"
repoCreateCmd "github.com/cli/cli/pkg/cmd/repo/create"
repoViewCmd "github.com/cli/cli/pkg/cmd/repo/view"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
@ -148,6 +149,7 @@ func init() {
RootCmd.AddCommand(repoCmd)
repoCmd.AddCommand(repoViewCmd.NewCmdView(&repoResolvingCmdFactory, nil))
repoCmd.AddCommand(repoCloneCmd.NewCmdClone(cmdFactory, nil))
repoCmd.AddCommand(repoCreateCmd.NewCmdCreate(cmdFactory, nil))
}
// RootCmd is the entry point of command-line execution

View file

@ -1,7 +1,6 @@
package clone
import (
"bytes"
"net/http"
"strings"
"testing"
@ -15,20 +14,7 @@ import (
"github.com/stretchr/testify/assert"
)
// TODO copypasta from command package
type cmdOut struct {
outBuf, errBuf *bytes.Buffer
}
func (c cmdOut) String() string {
return c.outBuf.String()
}
func (c cmdOut) Stderr() string {
return c.errBuf.String()
}
func runCloneCommand(httpClient *http.Client, cli string) (*cmdOut, error) {
func runCloneCommand(httpClient *http.Client, cli string) (*test.CmdOut, error) {
io, stdin, stdout, stderr := iostreams.Test()
fac := &cmdutil.Factory{
IOStreams: io,
@ -59,7 +45,7 @@ func runCloneCommand(httpClient *http.Client, cli string) (*cmdOut, error) {
return nil, err
}
return &cmdOut{stdout, stderr}, nil
return &test.CmdOut{OutBuf: stdout, ErrBuf: stderr}, nil
}
func Test_RepoClone(t *testing.T) {

View file

@ -0,0 +1,193 @@
package create
import (
"fmt"
"net/http"
"path"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/internal/run"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/prompt"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
)
type CreateOptions struct {
HttpClient func() (*http.Client, error)
Config func() (config.Config, error)
IO *iostreams.IOStreams
Name string
Description string
Homepage string
Team string
EnableIssues bool
EnableWiki bool
Public bool
}
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
opts := &CreateOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
Config: f.Config,
}
cmd := &cobra.Command{
Use: "create [<name>]",
Short: "Create a new repository",
Long: `Create a new GitHub repository.`,
Args: cobra.MaximumNArgs(1),
Example: heredoc.Doc(`
# create a repository under your account using the current directory name
$ gh repo create
# create a repository with a specific name
$ gh repo create my-project
# create a repository in an organization
$ gh repo create cli/my-project
`),
Annotations: map[string]string{
"help:arguments": heredoc.Doc(
`A repository can be supplied as an argument in any of the following formats:
- <OWNER/REPO>
- by URL, e.g. "https://github.com/OWNER/REPO"`),
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
opts.Name = args[0]
}
if runF != nil {
return runF(opts)
}
return createRun(opts)
},
}
cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Description of repository")
cmd.Flags().StringVarP(&opts.Homepage, "homepage", "h", "", "Repository home page URL")
cmd.Flags().StringVarP(&opts.Team, "team", "t", "", "The name of the organization team to be granted access")
cmd.Flags().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 (default: private)")
return cmd
}
func createRun(opts *CreateOptions) error {
projectDir, projectDirErr := git.ToplevelDir()
orgName := ""
name := opts.Name
if name != "" {
if strings.Contains(name, "/") {
newRepo, err := ghrepo.FromFullName(name)
if err != nil {
return fmt.Errorf("argument error: %w", err)
}
orgName = newRepo.RepoOwner()
name = newRepo.RepoName()
}
} else {
if projectDirErr != nil {
return projectDirErr
}
name = path.Base(projectDir)
}
visibility := "PRIVATE"
if opts.Public {
visibility = "PUBLIC"
}
input := repoCreateInput{
Name: name,
Visibility: visibility,
OwnerID: orgName,
TeamID: opts.Team,
Description: opts.Description,
HomepageURL: opts.Homepage,
HasIssuesEnabled: opts.EnableIssues,
HasWikiEnabled: opts.EnableWiki,
}
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
repo, err := repoCreate(httpClient, input)
if err != nil {
return err
}
stderr := opts.IO.ErrOut
stdout := opts.IO.Out
greenCheck := utils.Green("✓")
isTTY := opts.IO.IsStdoutTTY()
if isTTY {
fmt.Fprintf(stderr, "%s Created repository %s on GitHub\n", greenCheck, ghrepo.FullName(repo))
} else {
fmt.Fprintln(stdout, repo.URL)
}
// TODO This is overly wordy and I'd like to streamline this.
cfg, err := opts.Config()
if err != nil {
return err
}
protocol, err := cfg.Get("", "git_protocol")
if err != nil {
return err
}
remoteURL := ghrepo.FormatRemoteURL(repo, protocol)
if projectDirErr == nil {
_, err = git.AddRemote("origin", remoteURL)
if err != nil {
return err
}
if isTTY {
fmt.Fprintf(stderr, "%s Added remote %s\n", greenCheck, remoteURL)
}
} else if isTTY {
doSetup := false
err := prompt.Confirm(fmt.Sprintf("Create a local project directory for %s?", ghrepo.FullName(repo)), &doSetup)
if err != nil {
return err
}
if doSetup {
path := repo.Name
gitInit := git.GitCommand("init", path)
gitInit.Stdout = stdout
gitInit.Stderr = stderr
err = run.PrepareCmd(gitInit).Run()
if err != nil {
return err
}
gitRemoteAdd := git.GitCommand("-C", path, "remote", "add", "origin", remoteURL)
gitRemoteAdd.Stdout = stdout
gitRemoteAdd.Stderr = stderr
err = run.PrepareCmd(gitRemoteAdd).Run()
if err != nil {
return err
}
fmt.Fprintf(stderr, "%s Initialized repository in './%s/'\n", greenCheck, path)
}
}
return nil
}

View file

@ -0,0 +1,256 @@
package create
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"os/exec"
"strings"
"testing"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/run"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/test"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func runCommand(httpClient *http.Client, cli string) (*test.CmdOut, error) {
io, _, stdout, stderr := iostreams.Test()
fac := &cmdutil.Factory{
IOStreams: io,
HttpClient: func() (*http.Client, error) {
return httpClient, nil
},
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}
var seenCmd *exec.Cmd
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
output, err := runCommand(httpClient, "REPO")
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())
if seenCmd == nil {
t.Fatal("expected a command to run")
}
assert.Equal(t, "git remote add -f origin https://github.com/OWNER/REPO.git", strings.Join(seenCmd.Args, " "))
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_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"
}
}
} } }`))
httpClient := &http.Client{Transport: reg}
var seenCmd *exec.Cmd
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
output, err := runCommand(httpClient, "ORG/REPO")
if err != nil {
t.Errorf("error running command `repo create`: %v", err)
}
assert.Equal(t, "https://github.com/ORG/REPO\n", output.String())
assert.Equal(t, "", output.Stderr())
if seenCmd == nil {
t.Fatal("expected a command to run")
}
assert.Equal(t, "git remote add -f origin https://github.com/ORG/REPO.git", strings.Join(seenCmd.Args, " "))
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}
var seenCmd *exec.Cmd
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
output, err := runCommand(httpClient, "ORG/REPO --team monkeys")
if err != nil {
t.Errorf("error running command `repo create`: %v", err)
}
assert.Equal(t, "https://github.com/ORG/REPO\n", output.String())
assert.Equal(t, "", output.Stderr())
if seenCmd == nil {
t.Fatal("expected a command to run")
}
assert.Equal(t, "git remote add -f origin https://github.com/ORG/REPO.git", strings.Join(seenCmd.Args, " "))
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)
}
}

View file

@ -0,0 +1,92 @@
package create
import (
"fmt"
"net/http"
"github.com/cli/cli/api"
)
// repoCreateInput represents input parameters for repoCreate
type repoCreateInput struct {
Name string `json:"name"`
Visibility string `json:"visibility"`
HomepageURL string `json:"homepageUrl,omitempty"`
Description string `json:"description,omitempty"`
OwnerID string `json:"ownerId,omitempty"`
TeamID string `json:"teamId,omitempty"`
HasIssuesEnabled bool `json:"hasIssuesEnabled"`
HasWikiEnabled bool `json:"hasWikiEnabled"`
}
// repoCreate creates a new GitHub repository
func repoCreate(client *http.Client, input repoCreateInput) (*api.Repository, error) {
apiClient := api.NewClientFromHTTP(client)
var response struct {
CreateRepository struct {
Repository api.Repository
}
}
if input.TeamID != "" {
orgID, teamID, err := resolveOrganizationTeam(apiClient, input.OwnerID, input.TeamID)
if err != nil {
return nil, err
}
input.TeamID = teamID
input.OwnerID = orgID
} else if input.OwnerID != "" {
orgID, err := resolveOrganization(apiClient, input.OwnerID)
if err != nil {
return nil, err
}
input.OwnerID = orgID
}
variables := map[string]interface{}{
"input": input,
}
err := apiClient.GraphQL(`
mutation RepositoryCreate($input: CreateRepositoryInput!) {
createRepository(input: $input) {
repository {
id
name
owner { login }
url
}
}
}
`, variables, &response)
if err != nil {
return nil, err
}
// FIXME: support Enterprise hosts
return api.InitRepoHostname(&response.CreateRepository.Repository, "github.com"), nil
}
// using API v3 here because the equivalent in GraphQL needs `read:org` scope
func resolveOrganization(client *api.Client, orgName string) (string, error) {
var response struct {
NodeID string `json:"node_id"`
}
err := client.REST("GET", fmt.Sprintf("users/%s", orgName), nil, &response)
return response.NodeID, err
}
// using API v3 here because the equivalent in GraphQL needs `read:org` scope
func resolveOrganizationTeam(client *api.Client, orgName, teamSlug string) (string, string, error) {
var response struct {
NodeID string `json:"node_id"`
Organization struct {
NodeID string `json:"node_id"`
}
}
err := client.REST("GET", fmt.Sprintf("orgs/%s/teams/%s", orgName, teamSlug), nil, &response)
return response.Organization.NodeID, response.NodeID, err
}

View file

@ -0,0 +1,48 @@
package create
import (
"bytes"
"encoding/json"
"io/ioutil"
"testing"
"github.com/cli/cli/api"
"github.com/cli/cli/pkg/httpmock"
)
func Test_RepoCreate(t *testing.T) {
reg := &httpmock.Registry{}
httpClient := api.NewHTTPClient(api.ReplaceTripper(reg))
reg.StubResponse(200, bytes.NewBufferString(`{}`))
input := repoCreateInput{
Description: "roasted chesnuts",
HomepageURL: "http://example.com",
}
_, err := repoCreate(httpClient, input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(reg.Requests) != 1 {
t.Fatalf("expected 1 HTTP request, seen %d", len(reg.Requests))
}
var reqBody struct {
Query string
Variables struct {
Input map[string]interface{}
}
}
bodyBytes, _ := ioutil.ReadAll(reg.Requests[0].Body)
_ = json.Unmarshal(bodyBytes, &reqBody)
if description := reqBody.Variables.Input["description"].(string); description != "roasted chesnuts" {
t.Errorf("expected description to be %q, got %q", "roasted chesnuts", description)
}
if homepage := reqBody.Variables.Input["homepageUrl"].(string); homepage != "http://example.com" {
t.Errorf("expected homepageUrl to be %q, got %q", "http://example.com", homepage)
}
}

22
pkg/prompt/prompt.go Normal file
View file

@ -0,0 +1,22 @@
package prompt
import "github.com/AlecAivazis/survey/v2"
func StubConfirm(result bool) func() {
orig := Confirm
Confirm = func(_ string, r *bool) error {
*r = result
return nil
}
return func() {
Confirm = orig
}
}
var Confirm = func(prompt string, result *bool) error {
p := &survey.Confirm{
Message: prompt,
Default: true,
}
return survey.AskOne(p, result)
}

View file

@ -11,6 +11,19 @@ import (
"github.com/cli/cli/internal/run"
)
// TODO copypasta from command package
type CmdOut struct {
OutBuf, ErrBuf *bytes.Buffer
}
func (c CmdOut) String() string {
return c.OutBuf.String()
}
func (c CmdOut) Stderr() string {
return c.ErrBuf.String()
}
// OutputStub implements a simple utils.Runnable
type OutputStub struct {
Out []byte