diff --git a/api/queries_org.go b/api/queries_org.go new file mode 100644 index 000000000..21d1f528c --- /dev/null +++ b/api/queries_org.go @@ -0,0 +1,24 @@ +package api + +import "fmt" + +// 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 +} diff --git a/api/queries_repo.go b/api/queries_repo.go index 4793f7595..ce371f656 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -225,6 +225,9 @@ type RepoCreateInput struct { Homepage string `json:"homepage,omitempty"` Description string `json:"description,omitempty"` + OwnerID string `json:"ownerId,omitempty"` + TeamID string `json:"teamId,omitempty"` + HasIssuesEnabled bool `json:"hasIssuesEnabled"` HasWikiEnabled bool `json:"hasWikiEnabled"` } @@ -237,6 +240,21 @@ func RepoCreate(client *Client, input RepoCreateInput) (*Repository, error) { } } + 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, } diff --git a/command/repo.go b/command/repo.go index 82198ca49..4464b898f 100644 --- a/command/repo.go +++ b/command/repo.go @@ -20,6 +20,7 @@ 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") @@ -49,8 +50,10 @@ To pass 'git clone' options, separate them with '--'.`, var repoCreateCmd = &cobra.Command{ Use: "create []", Short: "Create a new repository", - Long: `Create a new GitHub repository.`, - RunE: repoCreate, + Long: `Create a new GitHub repository. + +Use the "ORG/NAME" syntax to create a repository within your organization.`, + RunE: repoCreate, } var repoViewCmd = &cobra.Command{ @@ -82,9 +85,20 @@ func repoClone(cmd *cobra.Command, args []string) error { 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 := ghrepo.FromFullName(name) + orgName = newRepo.RepoOwner() + name = newRepo.RepoName() + } } else { if projectDirErr != nil { return projectDirErr @@ -122,6 +136,8 @@ func repoCreate(cmd *cobra.Command, args []string) error { input := api.RepoCreateInput{ Name: name, Visibility: visibility, + OwnerID: orgName, + TeamID: teamSlug, Description: description, Homepage: homepage, HasIssuesEnabled: hasIssuesEnabled, diff --git a/command/repo_test.go b/command/repo_test.go index 82cae32c7..d55af31c3 100644 --- a/command/repo_test.go +++ b/command/repo_test.go @@ -108,6 +108,10 @@ func TestRepoCreate(t *testing.T) { } } + 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" { @@ -116,6 +120,138 @@ func TestRepoCreate(t *testing.T) { 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.StubResponse(200, bytes.NewBufferString(` + { "node_id": "ORGID" + } + `)) + http.StubResponse(200, bytes.NewBufferString(` + { "data": { "createRepository": { + "repository": { + "id": "REPOID", + "url": "https://github.com/ORG/REPO" + } + } } } + `)) + + var seenCmd *exec.Cmd + restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable { + seenCmd = cmd + return &outputStub{} + }) + defer restoreCmd() + + output, err := RunCommand(repoCreateCmd, "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 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.StubResponse(200, bytes.NewBufferString(` + { "node_id": "TEAMID", + "organization": { "node_id": "ORGID" } + } + `)) + http.StubResponse(200, bytes.NewBufferString(` + { "data": { "createRepository": { + "repository": { + "id": "REPOID", + "url": "https://github.com/ORG/REPO" + } + } } } + `)) + + var seenCmd *exec.Cmd + restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable { + seenCmd = cmd + return &outputStub{} + }) + defer restoreCmd() + + output, err := RunCommand(repoCreateCmd, "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 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) + } } func TestRepoView(t *testing.T) {