diff --git a/api/queries_user.go b/api/queries_user.go index 0a9b68bd2..4ce7a3c46 100644 --- a/api/queries_user.go +++ b/api/queries_user.go @@ -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 +} diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 7628184e5..fc53b460e 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -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 } diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go index 25f5519ab..8eb1a2423 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -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) + } +} diff --git a/pkg/cmd/repo/create/http.go b/pkg/cmd/repo/create/http.go index 3a46c0d9a..70df0dfb7 100644 --- a/pkg/cmd/repo/create/http.go +++ b/pkg/cmd/repo/create/http.go @@ -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, } diff --git a/pkg/cmd/repo/create/http_test.go b/pkg/cmd/repo/create/http_test.go index 4b764572c..c8e2e7a2a 100644 --- a/pkg/cmd/repo/create/http_test.go +++ b/pkg/cmd/repo/create/http_test.go @@ -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) }