Skip to content

Commit fd31007

Browse files
author
Nate Smith
authored
Merge pull request cli#1590 from colinshum/colinshum/template-repo
[Feature] Create repositories from a template repo
2 parents bb65ca0 + 99372f0 commit fd31007

File tree

5 files changed

+205
-9
lines changed

5 files changed

+205
-9
lines changed

api/queries_user.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,14 @@ func CurrentLoginName(client *Client, hostname string) (string, error) {
1414
err := gql.QueryNamed(context.Background(), "UserCurrent", &query, nil)
1515
return query.Viewer.Login, err
1616
}
17+
18+
func CurrentUserID(client *Client, hostname string) (string, error) {
19+
var query struct {
20+
Viewer struct {
21+
ID string
22+
}
23+
}
24+
gql := graphQLClient(client.http, hostname)
25+
err := gql.QueryNamed(context.Background(), "UserCurrent", &query, nil)
26+
return query.Viewer.ID, err
27+
}

pkg/cmd/repo/create/create.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package create
22

33
import (
4+
"errors"
45
"fmt"
56
"net/http"
67
"path"
78
"strings"
89

910
"github.com/AlecAivazis/survey/v2"
1011
"github.com/MakeNowJust/heredoc"
12+
"github.com/cli/cli/api"
1113
"github.com/cli/cli/git"
1214
"github.com/cli/cli/internal/config"
15+
"github.com/cli/cli/internal/ghinstance"
1316
"github.com/cli/cli/internal/ghrepo"
1417
"github.com/cli/cli/internal/run"
1518
"github.com/cli/cli/pkg/cmdutil"
@@ -28,6 +31,7 @@ type CreateOptions struct {
2831
Description string
2932
Homepage string
3033
Team string
34+
Template string
3135
EnableIssues bool
3236
EnableWiki bool
3337
Public bool
@@ -73,13 +77,18 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
7377
return runF(opts)
7478
}
7579

80+
if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || !opts.EnableIssues || !opts.EnableWiki) {
81+
return &cmdutil.FlagError{Err: errors.New(`The '--template' option is not supported with '--homepage, --team, --enable-issues or --enable-wiki'`)}
82+
}
83+
7684
return createRun(opts)
7785
},
7886
}
7987

8088
cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Description of repository")
8189
cmd.Flags().StringVarP(&opts.Homepage, "homepage", "h", "", "Repository home page URL")
8290
cmd.Flags().StringVarP(&opts.Team, "team", "t", "", "The name of the organization team to be granted access")
91+
cmd.Flags().StringVarP(&opts.Template, "template", "p", "", "Make the new repository based on a template repository")
8392
cmd.Flags().BoolVar(&opts.EnableIssues, "enable-issues", true, "Enable issues in the new repository")
8493
cmd.Flags().BoolVar(&opts.EnableWiki, "enable-wiki", true, "Enable wiki in the new repository")
8594
cmd.Flags().BoolVar(&opts.Public, "public", false, "Make the new repository public")
@@ -164,6 +173,37 @@ func createRun(opts *CreateOptions) error {
164173
}
165174
}
166175

176+
// Find template repo ID
177+
if opts.Template != "" {
178+
httpClient, err := opts.HttpClient()
179+
if err != nil {
180+
return err
181+
}
182+
183+
var toClone ghrepo.Interface
184+
apiClient := api.NewClientFromHTTP(httpClient)
185+
186+
cloneURL := opts.Template
187+
if !strings.Contains(cloneURL, "/") {
188+
currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default())
189+
if err != nil {
190+
return err
191+
}
192+
cloneURL = currentUser + "/" + cloneURL
193+
}
194+
toClone, err = ghrepo.FromFullName(cloneURL)
195+
if err != nil {
196+
return fmt.Errorf("argument error: %w", err)
197+
}
198+
199+
repo, err := api.GitHubRepo(apiClient, toClone)
200+
if err != nil {
201+
return err
202+
}
203+
204+
opts.Template = repo.ID
205+
}
206+
167207
input := repoCreateInput{
168208
Name: repoToCreate.RepoName(),
169209
Visibility: visibility,
@@ -189,7 +229,7 @@ func createRun(opts *CreateOptions) error {
189229
}
190230

191231
if opts.ConfirmSubmit {
192-
repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input)
232+
repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input, opts.Template)
193233
if err != nil {
194234
return err
195235
}

pkg/cmd/repo/create/create_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,94 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
303303
t.Errorf("expected %q, got %q", "TEAMID", teamID)
304304
}
305305
}
306+
307+
func TestRepoCreate_template(t *testing.T) {
308+
reg := &httpmock.Registry{}
309+
reg.Register(
310+
httpmock.GraphQL(`mutation CloneTemplateRepository\b`),
311+
httpmock.StringResponse(`
312+
{ "data": { "cloneTemplateRepository": {
313+
"repository": {
314+
"id": "REPOID",
315+
"name": "REPO",
316+
"owner": {
317+
"login": "OWNER"
318+
},
319+
"url": "https://github.com/OWNER/REPO"
320+
}
321+
} } }`))
322+
323+
reg.Register(
324+
httpmock.GraphQL(`query RepositoryInfo\b`),
325+
httpmock.StringResponse(`
326+
{ "data": {
327+
"repository": {
328+
"id": "REPOID",
329+
"description": "DESCRIPTION"
330+
} } }`))
331+
332+
reg.Register(
333+
httpmock.GraphQL(`query UserCurrent\b`),
334+
httpmock.StringResponse(`{"data":{"viewer":{"ID":"OWNERID"}}}`))
335+
336+
httpClient := &http.Client{Transport: reg}
337+
338+
var seenCmd *exec.Cmd
339+
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
340+
seenCmd = cmd
341+
return &test.OutputStub{}
342+
})
343+
defer restoreCmd()
344+
345+
as, surveyTearDown := prompt.InitAskStubber()
346+
defer surveyTearDown()
347+
348+
as.Stub([]*prompt.QuestionStub{
349+
{
350+
Name: "repoVisibility",
351+
Value: "PRIVATE",
352+
},
353+
})
354+
as.Stub([]*prompt.QuestionStub{
355+
{
356+
Name: "confirmSubmit",
357+
Value: true,
358+
},
359+
})
360+
361+
output, err := runCommand(httpClient, "REPO --template='OWNER/REPO'")
362+
if err != nil {
363+
t.Errorf("error running command `repo create`: %v", err)
364+
}
365+
366+
assert.Equal(t, "https://github.com/OWNER/REPO\n", output.String())
367+
assert.Equal(t, "", output.Stderr())
368+
369+
if seenCmd == nil {
370+
t.Fatal("expected a command to run")
371+
}
372+
assert.Equal(t, "git remote add -f origin https://github.com/OWNER/REPO.git", strings.Join(seenCmd.Args, " "))
373+
374+
var reqBody struct {
375+
Query string
376+
Variables struct {
377+
Input map[string]interface{}
378+
}
379+
}
380+
381+
if len(reg.Requests) != 3 {
382+
t.Fatalf("expected 3 HTTP requests, got %d", len(reg.Requests))
383+
}
384+
385+
bodyBytes, _ := ioutil.ReadAll(reg.Requests[2].Body)
386+
_ = json.Unmarshal(bodyBytes, &reqBody)
387+
if repoName := reqBody.Variables.Input["name"].(string); repoName != "REPO" {
388+
t.Errorf("expected %q, got %q", "REPO", repoName)
389+
}
390+
if repoVisibility := reqBody.Variables.Input["visibility"].(string); repoVisibility != "PRIVATE" {
391+
t.Errorf("expected %q, got %q", "PRIVATE", repoVisibility)
392+
}
393+
if ownerId := reqBody.Variables.Input["ownerId"].(string); ownerId != "OWNERID" {
394+
t.Errorf("expected %q, got %q", "OWNERID", ownerId)
395+
}
396+
}

pkg/cmd/repo/create/http.go

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,19 @@ type repoCreateInput struct {
2121
HasWikiEnabled bool `json:"hasWikiEnabled"`
2222
}
2323

24+
type repoTemplateInput struct {
25+
Name string `json:"name"`
26+
Visibility string `json:"visibility"`
27+
OwnerID string `json:"ownerId,omitempty"`
28+
29+
RepositoryID string `json:"repositoryId,omitempty"`
30+
Description string `json:"description,omitempty"`
31+
}
32+
2433
// repoCreate creates a new GitHub repository
25-
func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*api.Repository, error) {
34+
func repoCreate(client *http.Client, hostname string, input repoCreateInput, templateRepositoryID string) (*api.Repository, error) {
2635
apiClient := api.NewClientFromHTTP(client)
2736

28-
var response struct {
29-
CreateRepository struct {
30-
Repository api.Repository
31-
}
32-
}
33-
3437
if input.TeamID != "" {
3538
orgID, teamID, err := resolveOrganizationTeam(apiClient, hostname, input.OwnerID, input.TeamID)
3639
if err != nil {
@@ -46,6 +49,57 @@ func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*a
4649
input.OwnerID = orgID
4750
}
4851

52+
if templateRepositoryID != "" {
53+
var response struct {
54+
CloneTemplateRepository struct {
55+
Repository api.Repository
56+
}
57+
}
58+
59+
if input.OwnerID == "" {
60+
var err error
61+
input.OwnerID, err = api.CurrentUserID(apiClient, hostname)
62+
if err != nil {
63+
return nil, err
64+
}
65+
}
66+
67+
templateInput := repoTemplateInput{
68+
Name: input.Name,
69+
Visibility: input.Visibility,
70+
OwnerID: input.OwnerID,
71+
RepositoryID: templateRepositoryID,
72+
}
73+
74+
variables := map[string]interface{}{
75+
"input": templateInput,
76+
}
77+
78+
err := apiClient.GraphQL(hostname, `
79+
mutation CloneTemplateRepository($input: CloneTemplateRepositoryInput!) {
80+
cloneTemplateRepository(input: $input) {
81+
repository {
82+
id
83+
name
84+
owner { login }
85+
url
86+
}
87+
}
88+
}
89+
`, variables, &response)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
return api.InitRepoHostname(&response.CloneTemplateRepository.Repository, hostname), nil
95+
}
96+
97+
var response struct {
98+
CreateRepository struct {
99+
Repository api.Repository
100+
}
101+
}
102+
49103
variables := map[string]interface{}{
50104
"input": input,
51105
}

pkg/cmd/repo/create/http_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func Test_RepoCreate(t *testing.T) {
2121
HomepageURL: "http://example.com",
2222
}
2323

24-
_, err := repoCreate(httpClient, "github.com", input)
24+
_, err := repoCreate(httpClient, "github.com", input, "")
2525
if err != nil {
2626
t.Fatalf("unexpected error: %v", err)
2727
}

0 commit comments

Comments
 (0)