Merge pull request #1590 from colinshum/colinshum/template-repo

[Feature] Create repositories from a template repo
This commit is contained in:
Nate Smith 2020-08-28 14:09:12 -05:00 committed by GitHub
commit fd31007075
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 206 additions and 10 deletions

View file

@ -14,3 +14,14 @@ func CurrentLoginName(client *Client, hostname string) (string, error) {
err := gql.QueryNamed(context.Background(), "UserCurrent", &query, nil)
return query.Viewer.Login, err
}
func CurrentUserID(client *Client, hostname string) (string, error) {
var query struct {
Viewer struct {
ID string
}
}
gql := graphQLClient(client.http, hostname)
err := gql.QueryNamed(context.Background(), "UserCurrent", &query, nil)
return query.Viewer.ID, err
}

View file

@ -1,6 +1,7 @@
package create
import (
"errors"
"fmt"
"net/http"
"path"
@ -8,8 +9,10 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/api"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/internal/run"
"github.com/cli/cli/pkg/cmdutil"
@ -28,6 +31,7 @@ type CreateOptions struct {
Description string
Homepage string
Team string
Template string
EnableIssues bool
EnableWiki bool
Public bool
@ -73,6 +77,10 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
return runF(opts)
}
if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || !opts.EnableIssues || !opts.EnableWiki) {
return &cmdutil.FlagError{Err: errors.New(`The '--template' option is not supported with '--homepage, --team, --enable-issues or --enable-wiki'`)}
}
return createRun(opts)
},
}
@ -80,6 +88,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
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().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")
@ -164,6 +173,37 @@ func createRun(opts *CreateOptions) error {
}
}
// Find template repo ID
if opts.Template != "" {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
var toClone ghrepo.Interface
apiClient := api.NewClientFromHTTP(httpClient)
cloneURL := opts.Template
if !strings.Contains(cloneURL, "/") {
currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default())
if err != nil {
return err
}
cloneURL = currentUser + "/" + cloneURL
}
toClone, err = ghrepo.FromFullName(cloneURL)
if err != nil {
return fmt.Errorf("argument error: %w", err)
}
repo, err := api.GitHubRepo(apiClient, toClone)
if err != nil {
return err
}
opts.Template = repo.ID
}
input := repoCreateInput{
Name: repoToCreate.RepoName(),
Visibility: visibility,
@ -189,7 +229,7 @@ func createRun(opts *CreateOptions) error {
}
if opts.ConfirmSubmit {
repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input)
repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input, opts.Template)
if err != nil {
return err
}

View file

@ -303,3 +303,94 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
t.Errorf("expected %q, got %q", "TEAMID", teamID)
}
}
func TestRepoCreate_template(t *testing.T) {
reg := &httpmock.Registry{}
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.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": {
"repository": {
"id": "REPOID",
"description": "DESCRIPTION"
} } }`))
reg.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data":{"viewer":{"ID":"OWNERID"}}}`))
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()
as, surveyTearDown := prompt.InitAskStubber()
defer surveyTearDown()
as.Stub([]*prompt.QuestionStub{
{
Name: "repoVisibility",
Value: "PRIVATE",
},
})
as.Stub([]*prompt.QuestionStub{
{
Name: "confirmSubmit",
Value: true,
},
})
output, err := runCommand(httpClient, "REPO --template='OWNER/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) != 3 {
t.Fatalf("expected 3 HTTP requests, got %d", len(reg.Requests))
}
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)
}
}

View file

@ -21,15 +21,18 @@ type repoCreateInput struct {
HasWikiEnabled bool `json:"hasWikiEnabled"`
}
// repoCreate creates a new GitHub repository
func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*api.Repository, error) {
apiClient := api.NewClientFromHTTP(client)
type repoTemplateInput struct {
Name string `json:"name"`
Visibility string `json:"visibility"`
OwnerID string `json:"ownerId,omitempty"`
var response struct {
CreateRepository struct {
Repository api.Repository
}
}
RepositoryID string `json:"repositoryId,omitempty"`
Description string `json:"description,omitempty"`
}
// repoCreate creates a new GitHub repository
func repoCreate(client *http.Client, hostname string, input repoCreateInput, templateRepositoryID string) (*api.Repository, error) {
apiClient := api.NewClientFromHTTP(client)
if input.TeamID != "" {
orgID, teamID, err := resolveOrganizationTeam(apiClient, hostname, input.OwnerID, input.TeamID)
@ -46,6 +49,57 @@ func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*a
input.OwnerID = orgID
}
if templateRepositoryID != "" {
var response struct {
CloneTemplateRepository struct {
Repository api.Repository
}
}
if input.OwnerID == "" {
var err error
input.OwnerID, err = api.CurrentUserID(apiClient, hostname)
if err != nil {
return nil, err
}
}
templateInput := repoTemplateInput{
Name: input.Name,
Visibility: input.Visibility,
OwnerID: input.OwnerID,
RepositoryID: templateRepositoryID,
}
variables := map[string]interface{}{
"input": templateInput,
}
err := apiClient.GraphQL(hostname, `
mutation CloneTemplateRepository($input: CloneTemplateRepositoryInput!) {
cloneTemplateRepository(input: $input) {
repository {
id
name
owner { login }
url
}
}
}
`, variables, &response)
if err != nil {
return nil, err
}
return api.InitRepoHostname(&response.CloneTemplateRepository.Repository, hostname), nil
}
var response struct {
CreateRepository struct {
Repository api.Repository
}
}
variables := map[string]interface{}{
"input": input,
}

View file

@ -21,7 +21,7 @@ func Test_RepoCreate(t *testing.T) {
HomepageURL: "http://example.com",
}
_, err := repoCreate(httpClient, "github.com", input)
_, err := repoCreate(httpClient, "github.com", input, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}