1package create
2
3import (
4	"bytes"
5	"encoding/json"
6	"fmt"
7	"net/http"
8	"strings"
9
10	"github.com/cli/cli/v2/api"
11)
12
13// repoCreateInput is input parameters for the repoCreate method
14type repoCreateInput struct {
15	Name                 string
16	HomepageURL          string
17	Description          string
18	Visibility           string
19	OwnerLogin           string
20	TeamSlug             string
21	TemplateRepositoryID string
22	HasIssuesEnabled     bool
23	HasWikiEnabled       bool
24	GitIgnoreTemplate    string
25	LicenseTemplate      string
26}
27
28// createRepositoryInputV3 is the payload for the repo create REST API
29type createRepositoryInputV3 struct {
30	Name              string `json:"name"`
31	HomepageURL       string `json:"homepage,omitempty"`
32	Description       string `json:"description,omitempty"`
33	IsPrivate         bool   `json:"private"`
34	Visibility        string `json:"visibility,omitempty"`
35	TeamID            uint64 `json:"team_id,omitempty"`
36	HasIssuesEnabled  bool   `json:"has_issues"`
37	HasWikiEnabled    bool   `json:"has_wiki"`
38	GitIgnoreTemplate string `json:"gitignore_template,omitempty"`
39	LicenseTemplate   string `json:"license_template,omitempty"`
40}
41
42// createRepositoryInput is the payload for the repo create GraphQL mutation
43type createRepositoryInput struct {
44	Name             string `json:"name"`
45	HomepageURL      string `json:"homepageUrl,omitempty"`
46	Description      string `json:"description,omitempty"`
47	Visibility       string `json:"visibility"`
48	OwnerID          string `json:"ownerId,omitempty"`
49	TeamID           string `json:"teamId,omitempty"`
50	HasIssuesEnabled bool   `json:"hasIssuesEnabled"`
51	HasWikiEnabled   bool   `json:"hasWikiEnabled"`
52}
53
54// cloneTemplateRepositoryInput is the payload for creating a repo from a template using GraphQL
55type cloneTemplateRepositoryInput struct {
56	Name         string `json:"name"`
57	Visibility   string `json:"visibility"`
58	Description  string `json:"description,omitempty"`
59	OwnerID      string `json:"ownerId"`
60	RepositoryID string `json:"repositoryId"`
61}
62
63// repoCreate creates a new GitHub repository
64func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*api.Repository, error) {
65	isOrg := false
66	var ownerID string
67	var teamID string
68	var teamIDv3 uint64
69
70	apiClient := api.NewClientFromHTTP(client)
71
72	if input.TeamSlug != "" {
73		team, err := resolveOrganizationTeam(apiClient, hostname, input.OwnerLogin, input.TeamSlug)
74		if err != nil {
75			return nil, err
76		}
77		teamIDv3 = team.ID
78		teamID = team.NodeID
79		ownerID = team.Organization.NodeID
80		isOrg = true
81	} else if input.OwnerLogin != "" {
82		owner, err := resolveOwner(apiClient, hostname, input.OwnerLogin)
83		if err != nil {
84			return nil, err
85		}
86		ownerID = owner.NodeID
87		isOrg = owner.IsOrganization()
88	}
89
90	if input.TemplateRepositoryID != "" {
91		var response struct {
92			CloneTemplateRepository struct {
93				Repository api.Repository
94			}
95		}
96
97		if ownerID == "" {
98			var err error
99			ownerID, err = api.CurrentUserID(apiClient, hostname)
100			if err != nil {
101				return nil, err
102			}
103		}
104
105		variables := map[string]interface{}{
106			"input": cloneTemplateRepositoryInput{
107				Name:         input.Name,
108				Description:  input.Description,
109				Visibility:   strings.ToUpper(input.Visibility),
110				OwnerID:      ownerID,
111				RepositoryID: input.TemplateRepositoryID,
112			},
113		}
114
115		err := apiClient.GraphQL(hostname, `
116		mutation CloneTemplateRepository($input: CloneTemplateRepositoryInput!) {
117			cloneTemplateRepository(input: $input) {
118				repository {
119					id
120					name
121					owner { login }
122					url
123				}
124			}
125		}
126		`, variables, &response)
127		if err != nil {
128			return nil, err
129		}
130
131		return api.InitRepoHostname(&response.CloneTemplateRepository.Repository, hostname), nil
132	}
133
134	if input.GitIgnoreTemplate != "" || input.LicenseTemplate != "" {
135		inputv3 := createRepositoryInputV3{
136			Name:              input.Name,
137			HomepageURL:       input.HomepageURL,
138			Description:       input.Description,
139			IsPrivate:         strings.EqualFold(input.Visibility, "PRIVATE"),
140			TeamID:            teamIDv3,
141			HasIssuesEnabled:  input.HasIssuesEnabled,
142			HasWikiEnabled:    input.HasWikiEnabled,
143			GitIgnoreTemplate: input.GitIgnoreTemplate,
144			LicenseTemplate:   input.LicenseTemplate,
145		}
146
147		path := "user/repos"
148		if isOrg {
149			path = fmt.Sprintf("orgs/%s/repos", input.OwnerLogin)
150			inputv3.Visibility = strings.ToLower(input.Visibility)
151		}
152
153		body := &bytes.Buffer{}
154		enc := json.NewEncoder(body)
155		if err := enc.Encode(inputv3); err != nil {
156			return nil, err
157		}
158
159		repo, err := api.CreateRepoTransformToV4(apiClient, hostname, "POST", path, body)
160		if err != nil {
161			return nil, err
162		}
163		return repo, nil
164	}
165
166	var response struct {
167		CreateRepository struct {
168			Repository api.Repository
169		}
170	}
171
172	variables := map[string]interface{}{
173		"input": createRepositoryInput{
174			Name:             input.Name,
175			Description:      input.Description,
176			HomepageURL:      input.HomepageURL,
177			Visibility:       strings.ToUpper(input.Visibility),
178			OwnerID:          ownerID,
179			TeamID:           teamID,
180			HasIssuesEnabled: input.HasIssuesEnabled,
181			HasWikiEnabled:   input.HasWikiEnabled,
182		},
183	}
184
185	err := apiClient.GraphQL(hostname, `
186	mutation RepositoryCreate($input: CreateRepositoryInput!) {
187		createRepository(input: $input) {
188			repository {
189				id
190				name
191				owner { login }
192				url
193			}
194		}
195	}
196	`, variables, &response)
197	if err != nil {
198		return nil, err
199	}
200
201	return api.InitRepoHostname(&response.CreateRepository.Repository, hostname), nil
202}
203
204type ownerResponse struct {
205	NodeID string `json:"node_id"`
206	Type   string `json:"type"`
207}
208
209func (r *ownerResponse) IsOrganization() bool {
210	return r.Type == "Organization"
211}
212
213func resolveOwner(client *api.Client, hostname, orgName string) (*ownerResponse, error) {
214	var response ownerResponse
215	err := client.REST(hostname, "GET", fmt.Sprintf("users/%s", orgName), nil, &response)
216	return &response, err
217}
218
219type teamResponse struct {
220	ID           uint64 `json:"id"`
221	NodeID       string `json:"node_id"`
222	Organization struct {
223		NodeID string `json:"node_id"`
224	}
225}
226
227func resolveOrganizationTeam(client *api.Client, hostname, orgName, teamSlug string) (*teamResponse, error) {
228	var response teamResponse
229	err := client.REST(hostname, "GET", fmt.Sprintf("orgs/%s/teams/%s", orgName, teamSlug), nil, &response)
230	return &response, err
231}
232
233// listGitIgnoreTemplates uses API v3 here because gitignore template isn't supported by GraphQL yet.
234func listGitIgnoreTemplates(httpClient *http.Client, hostname string) ([]string, error) {
235	var gitIgnoreTemplates []string
236	client := api.NewClientFromHTTP(httpClient)
237	err := client.REST(hostname, "GET", "gitignore/templates", nil, &gitIgnoreTemplates)
238	if err != nil {
239		return []string{}, err
240	}
241	return gitIgnoreTemplates, nil
242}
243
244// listLicenseTemplates uses API v3 here because license template isn't supported by GraphQL yet.
245func listLicenseTemplates(httpClient *http.Client, hostname string) ([]api.License, error) {
246	var licenseTemplates []api.License
247	client := api.NewClientFromHTTP(httpClient)
248	err := client.REST(hostname, "GET", "licenses", nil, &licenseTemplates)
249	if err != nil {
250		return nil, err
251	}
252	return licenseTemplates, nil
253}
254