Merge pull request #1590 from colinshum/colinshum/template-repo
[Feature] Create repositories from a template repo
This commit is contained in:
commit
fd31007075
5 changed files with 206 additions and 10 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue