From 58f62a4a155cb8ea3e6ac686e0fdd6764b5613b9 Mon Sep 17 00:00:00 2001 From: Colin Shum Date: Thu, 27 Aug 2020 13:00:34 -0400 Subject: [PATCH 1/5] [Feature] Create repository from template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mislav Marohnić --- api/queries_user.go | 11 ++++++ pkg/cmd/repo/create/create.go | 38 ++++++++++++++++++++ pkg/cmd/repo/create/http.go | 67 +++++++++++++++++++++++++++++++---- 3 files changed, 110 insertions(+), 6 deletions(-) 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..28d8384cc 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -8,8 +8,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 +30,7 @@ type CreateOptions struct { Description string Homepage string Team string + Template string EnableIssues bool EnableWiki bool Public bool @@ -80,6 +83,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,11 +168,45 @@ func createRun(opts *CreateOptions) error { } } + // find template ID + + if opts.Template != "" { + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + + var toView ghrepo.Interface + apiClient := api.NewClientFromHTTP(httpClient) + + // var err errors + viewURL := opts.Template + if !strings.Contains(viewURL, "/") { + currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default()) + if err != nil { + return err + } + viewURL = currentUser + "/" + viewURL + } + toView, err = ghrepo.FromFullName(viewURL) + if err != nil { + return fmt.Errorf("argument error: %w", err) + } + + repo, err := api.GitHubRepo(apiClient, toView) + if err != nil { + return err + } + + opts.Template = repo.ID + } + input := repoCreateInput{ Name: repoToCreate.RepoName(), Visibility: visibility, OwnerID: repoToCreate.RepoOwner(), TeamID: opts.Team, + RepositoryID: opts.Template, Description: opts.Description, HomepageURL: opts.Homepage, HasIssuesEnabled: opts.EnableIssues, diff --git a/pkg/cmd/repo/create/http.go b/pkg/cmd/repo/create/http.go index 3a46c0d9a..f3623c6e2 100644 --- a/pkg/cmd/repo/create/http.go +++ b/pkg/cmd/repo/create/http.go @@ -14,6 +14,8 @@ type repoCreateInput struct { HomepageURL string `json:"homepageUrl,omitempty"` Description string `json:"description,omitempty"` + RepositoryID string `json:"repositoryId,omitempty"` + OwnerID string `json:"ownerId,omitempty"` TeamID string `json:"teamId,omitempty"` @@ -21,16 +23,18 @@ type repoCreateInput struct { HasWikiEnabled bool `json:"hasWikiEnabled"` } +type repoTemplateInput struct { + Name string `json:"name"` + Visibility string `json:"visibility"` + OwnerID string `json:"ownerId,omitempty"` + + RepositoryID string `json:"repositoryId,omitempty"` +} + // repoCreate creates a new GitHub repository func repoCreate(client *http.Client, hostname string, 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, hostname, input.OwnerID, input.TeamID) if err != nil { @@ -46,6 +50,57 @@ func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*a input.OwnerID = orgID } + if input.RepositoryID != "" { + 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: input.RepositoryID, + } + + 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, } From 32e5d053282017cbbfc8620078f046eada27bf9a Mon Sep 17 00:00:00 2001 From: Colin Shum Date: Thu, 27 Aug 2020 14:58:40 -0400 Subject: [PATCH 2/5] [Fix] Warn user when --template is passed with incompatible flags --- pkg/cmd/repo/create/create.go | 5 +++++ pkg/cmd/repo/create/http.go | 1 + 2 files changed, 6 insertions(+) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 28d8384cc..57264c879 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" @@ -76,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) }, } diff --git a/pkg/cmd/repo/create/http.go b/pkg/cmd/repo/create/http.go index f3623c6e2..e8d3f29b1 100644 --- a/pkg/cmd/repo/create/http.go +++ b/pkg/cmd/repo/create/http.go @@ -29,6 +29,7 @@ type repoTemplateInput struct { OwnerID string `json:"ownerId,omitempty"` RepositoryID string `json:"repositoryId,omitempty"` + Description string `json:"description,omitempty"` } // repoCreate creates a new GitHub repository From 263e3a6a356b8e062f133e7d6dd5ad9cae7766f1 Mon Sep 17 00:00:00 2001 From: Colin Shum Date: Thu, 27 Aug 2020 17:57:55 -0400 Subject: [PATCH 3/5] Update create_test.go --- pkg/cmd/repo/create/create_test.go | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go index 25f5519ab..f3de15c5a 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 request, 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) + } +} From 2886dd913f1621646b960fd61d0f1672bba9b64d Mon Sep 17 00:00:00 2001 From: Colin Shum Date: Thu, 27 Aug 2020 19:13:06 -0400 Subject: [PATCH 4/5] [Refactor] toView -> toClone --- pkg/cmd/repo/create/create.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 57264c879..2ac91eb55 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -181,24 +181,23 @@ func createRun(opts *CreateOptions) error { return err } - var toView ghrepo.Interface + var toClone ghrepo.Interface apiClient := api.NewClientFromHTTP(httpClient) - // var err errors - viewURL := opts.Template - if !strings.Contains(viewURL, "/") { + cloneURL := opts.Template + if !strings.Contains(cloneURL, "/") { currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default()) if err != nil { return err } - viewURL = currentUser + "/" + viewURL + cloneURL = currentUser + "/" + cloneURL } - toView, err = ghrepo.FromFullName(viewURL) + toClone, err = ghrepo.FromFullName(cloneURL) if err != nil { return fmt.Errorf("argument error: %w", err) } - repo, err := api.GitHubRepo(apiClient, toView) + repo, err := api.GitHubRepo(apiClient, toClone) if err != nil { return err } From 99372f0dbc47cb1c67628004407909ec607d8141 Mon Sep 17 00:00:00 2001 From: Colin Shum Date: Thu, 27 Aug 2020 19:13:17 -0400 Subject: [PATCH 5/5] [Refactor] Add variadic argument to repoCreate to support templates --- pkg/cmd/repo/create/create.go | 8 +++----- pkg/cmd/repo/create/create_test.go | 2 +- pkg/cmd/repo/create/http.go | 8 +++----- pkg/cmd/repo/create/http_test.go | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 2ac91eb55..fc53b460e 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -78,7 +78,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co } 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 &cmdutil.FlagError{Err: errors.New(`The '--template' option is not supported with '--homepage, --team, --enable-issues or --enable-wiki'`)} } return createRun(opts) @@ -173,8 +173,7 @@ func createRun(opts *CreateOptions) error { } } - // find template ID - + // Find template repo ID if opts.Template != "" { httpClient, err := opts.HttpClient() if err != nil { @@ -210,7 +209,6 @@ func createRun(opts *CreateOptions) error { Visibility: visibility, OwnerID: repoToCreate.RepoOwner(), TeamID: opts.Team, - RepositoryID: opts.Template, Description: opts.Description, HomepageURL: opts.Homepage, HasIssuesEnabled: opts.EnableIssues, @@ -231,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 f3de15c5a..8eb1a2423 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -379,7 +379,7 @@ func TestRepoCreate_template(t *testing.T) { } if len(reg.Requests) != 3 { - t.Fatalf("expected 3 HTTP request, got %d", len(reg.Requests)) + t.Fatalf("expected 3 HTTP requests, got %d", len(reg.Requests)) } bodyBytes, _ := ioutil.ReadAll(reg.Requests[2].Body) diff --git a/pkg/cmd/repo/create/http.go b/pkg/cmd/repo/create/http.go index e8d3f29b1..70df0dfb7 100644 --- a/pkg/cmd/repo/create/http.go +++ b/pkg/cmd/repo/create/http.go @@ -14,8 +14,6 @@ type repoCreateInput struct { HomepageURL string `json:"homepageUrl,omitempty"` Description string `json:"description,omitempty"` - RepositoryID string `json:"repositoryId,omitempty"` - OwnerID string `json:"ownerId,omitempty"` TeamID string `json:"teamId,omitempty"` @@ -33,7 +31,7 @@ type repoTemplateInput struct { } // repoCreate creates a new GitHub repository -func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*api.Repository, error) { +func repoCreate(client *http.Client, hostname string, input repoCreateInput, templateRepositoryID string) (*api.Repository, error) { apiClient := api.NewClientFromHTTP(client) if input.TeamID != "" { @@ -51,7 +49,7 @@ func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*a input.OwnerID = orgID } - if input.RepositoryID != "" { + if templateRepositoryID != "" { var response struct { CloneTemplateRepository struct { Repository api.Repository @@ -70,7 +68,7 @@ func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*a Name: input.Name, Visibility: input.Visibility, OwnerID: input.OwnerID, - RepositoryID: input.RepositoryID, + RepositoryID: templateRepositoryID, } variables := map[string]interface{}{ 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) }