Skip to content

Commit 58978ad

Browse files
committed
Creating a repo from a template repo via API
fix go-gitea#15934 ref: https://docs.github.com/en/rest/reference/repos#create-a-repository-using-a-template Signed-off-by: a1012112796 <[email protected]>
1 parent b4d1059 commit 58978ad

File tree

6 files changed

+291
-0
lines changed

6 files changed

+291
-0
lines changed

integrations/api_repo_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,3 +494,37 @@ func TestAPIRepoTransfer(t *testing.T) {
494494
repo = models.AssertExistsAndLoadBean(t, &models.Repository{ID: repo.ID}).(*models.Repository)
495495
_ = models.DeleteRepository(user, repo.OwnerID, repo.ID)
496496
}
497+
498+
func TestAPIGenerateRepo(t *testing.T) {
499+
defer prepareTestEnv(t)()
500+
501+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
502+
session := loginUser(t, user.Name)
503+
token := getTokenForLoggedInUser(t, session)
504+
505+
templateRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 44}).(*models.Repository)
506+
507+
// user
508+
repo := new(models.Repository)
509+
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate?token=%s", templateRepo.OwnerName, templateRepo.Name, token), &api.GenerateRepoOption{
510+
Owner: user.Name,
511+
Name: "new-repo",
512+
Description: "test generate repo",
513+
Private: false,
514+
GitContent: true,
515+
})
516+
resp := session.MakeRequest(t, req, http.StatusCreated)
517+
DecodeJSON(t, resp, repo)
518+
519+
assert.Equal(t, "new-repo", repo.Name)
520+
521+
// org
522+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate?token=%s", templateRepo.OwnerName, templateRepo.Name, token), &api.GenerateRepoOption{
523+
Owner: "user3",
524+
Name: "new-repo",
525+
Description: "test generate repo",
526+
Private: false,
527+
GitContent: true,
528+
})
529+
session.MakeRequest(t, req, http.StatusCreated)
530+
}

modules/structs/repo.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,36 @@ type EditRepoOption struct {
180180
MirrorInterval *string `json:"mirror_interval,omitempty"`
181181
}
182182

183+
// GenerateRepoOption options when creating repository using a template
184+
// swagger:model
185+
type GenerateRepoOption struct {
186+
// The organization or person who will own the new repository
187+
//
188+
// required: true
189+
Owner string `json:"owner"`
190+
// Name of the repository to create
191+
//
192+
// required: true
193+
// unique: true
194+
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(100)"`
195+
// Description of the repository to create
196+
Description string `json:"description" binding:"MaxSize(255)"`
197+
// Whether the repository is private
198+
Private bool `json:"private"`
199+
// include git content of default branch in template repo
200+
GitContent bool `json:"git_content"`
201+
// include topics in template repo
202+
Topics bool `json:"topics"`
203+
// include git hooks in template repo
204+
GitHooks bool `json:"git_hooks"`
205+
// include webhooks in template repo
206+
Webhooks bool `json:"webhooks"`
207+
// include avatar of the template repo
208+
Avatar bool `json:"avatar"`
209+
// include labels in template repo
210+
Labels bool `json:"labels"`
211+
}
212+
183213
// CreateBranchRepoOption options when creating a branch in a repository
184214
// swagger:model
185215
type CreateBranchRepoOption struct {

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ func Routes() *web.Route {
713713
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
714714
Delete(reqToken(), reqOwner(), repo.Delete).
715715
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
716+
m.Post("/generate", reqToken(), bind(api.GenerateRepoOption{}), repo.Generate)
716717
m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
717718
m.Combo("/notifications").
718719
Get(reqToken(), notify.ListRepoNotifications).

routers/api/v1/repo/repo.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,113 @@ func Create(ctx *context.APIContext) {
307307
CreateUserRepo(ctx, ctx.User, *opt)
308308
}
309309

310+
// Generate Create a repository using a template
311+
func Generate(ctx *context.APIContext) {
312+
// swagger:operation POST /repos/{template_owner}/{template_repo}/generate repository generateRepo
313+
// ---
314+
// summary: Create a repository using a template
315+
// consumes:
316+
// - application/json
317+
// produces:
318+
// - application/json
319+
// parameters:
320+
// - name: template_owner
321+
// in: path
322+
// description: name of the template repository owner
323+
// type: string
324+
// required: true
325+
// - name: template_repo
326+
// in: path
327+
// description: name of the template repository
328+
// type: string
329+
// required: true
330+
// - name: body
331+
// in: body
332+
// schema:
333+
// "$ref": "#/definitions/GenerateRepoOption"
334+
// responses:
335+
// "201":
336+
// "$ref": "#/responses/Repository"
337+
// "403":
338+
// "$ref": "#/responses/forbidden"
339+
// "404":
340+
// "$ref": "#/responses/notFound"
341+
// "409":
342+
// description: The repository with the same name already exists.
343+
// "422":
344+
// "$ref": "#/responses/validationError"
345+
form := web.GetForm(ctx).(*api.GenerateRepoOption)
346+
347+
if !ctx.Repo.Repository.IsTemplate {
348+
ctx.Error(http.StatusUnprocessableEntity, "", "this is not a template repo")
349+
return
350+
}
351+
352+
if ctx.User.IsOrganization() {
353+
ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization")
354+
return
355+
}
356+
357+
opts := models.GenerateRepoOptions{
358+
Name: form.Name,
359+
Description: form.Description,
360+
Private: form.Private,
361+
GitContent: form.GitContent,
362+
Topics: form.Topics,
363+
GitHooks: form.GitHooks,
364+
Webhooks: form.Webhooks,
365+
Avatar: form.Avatar,
366+
IssueLabels: form.Labels,
367+
}
368+
369+
if !opts.IsValid() {
370+
ctx.Error(http.StatusUnprocessableEntity, "", "must select at least one template item")
371+
return
372+
}
373+
374+
ctxUser := ctx.User
375+
var err error
376+
if form.Owner != ctxUser.Name {
377+
ctxUser, err = models.GetOrgByName(form.Owner)
378+
if err != nil {
379+
if models.IsErrOrgNotExist(err) {
380+
ctx.NotFound()
381+
return
382+
}
383+
384+
ctx.Error(http.StatusInternalServerError, "GetOrgByName", err)
385+
return
386+
}
387+
388+
if !ctx.User.IsAdmin {
389+
canCreate, err := ctxUser.CanCreateOrgRepo(ctx.User.ID)
390+
if err != nil {
391+
ctx.ServerError("CanCreateOrgRepo", err)
392+
return
393+
} else if !canCreate {
394+
ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.")
395+
return
396+
}
397+
}
398+
}
399+
400+
repo, err := repo_service.GenerateRepository(ctx.User, ctxUser, ctx.Repo.Repository, opts)
401+
if err != nil {
402+
if models.IsErrRepoAlreadyExist(err) {
403+
ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
404+
} else if models.IsErrNameReserved(err) ||
405+
models.IsErrNamePatternNotAllowed(err) {
406+
ctx.Error(http.StatusUnprocessableEntity, "", err)
407+
} else {
408+
ctx.Error(http.StatusInternalServerError, "CreateRepository", err)
409+
}
410+
return
411+
}
412+
log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
413+
414+
ctx.JSON(http.StatusCreated, convert.ToRepo(repo, models.AccessModeOwner))
415+
}
416+
310417
// CreateOrgRepoDeprecated create one repository of the organization
311418
func CreateOrgRepoDeprecated(ctx *context.APIContext) {
312419
// swagger:operation POST /org/{org}/repos organization createOrgRepoDeprecated

routers/api/v1/swagger/options.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ type swaggerParameterBodies struct {
8787
TransferRepoOption api.TransferRepoOption
8888
// in:body
8989
CreateForkOption api.CreateForkOption
90+
// in:body
91+
GenerateRepoOption api.GenerateRepoOption
9092

9193
// in:body
9294
CreateStatusOption api.CreateStatusOption

templates/swagger/v1_json.tmpl

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9488,6 +9488,61 @@
94889488
}
94899489
}
94909490
},
9491+
"/repos/{template_owner}/{template_repo}/generate": {
9492+
"post": {
9493+
"consumes": [
9494+
"application/json"
9495+
],
9496+
"produces": [
9497+
"application/json"
9498+
],
9499+
"tags": [
9500+
"repository"
9501+
],
9502+
"summary": "Create a repository using a template",
9503+
"operationId": "generateRepo",
9504+
"parameters": [
9505+
{
9506+
"type": "string",
9507+
"description": "name of the template repository owner",
9508+
"name": "template_owner",
9509+
"in": "path",
9510+
"required": true
9511+
},
9512+
{
9513+
"type": "string",
9514+
"description": "name of the template repository",
9515+
"name": "template_repo",
9516+
"in": "path",
9517+
"required": true
9518+
},
9519+
{
9520+
"name": "body",
9521+
"in": "body",
9522+
"schema": {
9523+
"$ref": "#/definitions/GenerateRepoOption"
9524+
}
9525+
}
9526+
],
9527+
"responses": {
9528+
"201": {
9529+
"$ref": "#/responses/Repository"
9530+
},
9531+
"403": {
9532+
"$ref": "#/responses/forbidden"
9533+
},
9534+
"404": {
9535+
"$ref": "#/responses/notFound"
9536+
},
9537+
"409": {
9538+
"description": "The repository with the same name already exists."
9539+
},
9540+
"422": {
9541+
"$ref": "#/responses/validationError"
9542+
}
9543+
}
9544+
}
9545+
},
94919546
"/repositories/{id}": {
94929547
"get": {
94939548
"produces": [
@@ -14184,6 +14239,68 @@
1418414239
},
1418514240
"x-go-package": "code.gitea.io/gitea/modules/structs"
1418614241
},
14242+
"GenerateRepoOption": {
14243+
"description": "GenerateRepoOption options when creating repository using a template",
14244+
"type": "object",
14245+
"required": [
14246+
"owner",
14247+
"name"
14248+
],
14249+
"properties": {
14250+
"avatar": {
14251+
"description": "include avatar of the template repo",
14252+
"type": "boolean",
14253+
"x-go-name": "Avatar"
14254+
},
14255+
"description": {
14256+
"description": "Description of the repository to create",
14257+
"type": "string",
14258+
"x-go-name": "Description"
14259+
},
14260+
"git_content": {
14261+
"description": "include git content of default branch in template repo",
14262+
"type": "boolean",
14263+
"x-go-name": "GitContent"
14264+
},
14265+
"git_hooks": {
14266+
"description": "include git hooks in template repo",
14267+
"type": "boolean",
14268+
"x-go-name": "GitHooks"
14269+
},
14270+
"labels": {
14271+
"description": "include labels in template repo",
14272+
"type": "boolean",
14273+
"x-go-name": "Labels"
14274+
},
14275+
"name": {
14276+
"description": "Name of the repository to create",
14277+
"type": "string",
14278+
"uniqueItems": true,
14279+
"x-go-name": "Name"
14280+
},
14281+
"owner": {
14282+
"description": "The organization or person who will own the new repository",
14283+
"type": "string",
14284+
"x-go-name": "Owner"
14285+
},
14286+
"private": {
14287+
"description": "Whether the repository is private",
14288+
"type": "boolean",
14289+
"x-go-name": "Private"
14290+
},
14291+
"topics": {
14292+
"description": "include topics in template repo",
14293+
"type": "boolean",
14294+
"x-go-name": "Topics"
14295+
},
14296+
"webhooks": {
14297+
"description": "include webhooks in template repo",
14298+
"type": "boolean",
14299+
"x-go-name": "Webhooks"
14300+
}
14301+
},
14302+
"x-go-package": "code.gitea.io/gitea/modules/structs"
14303+
},
1418714304
"GitBlobResponse": {
1418814305
"description": "GitBlobResponse represents a git blob",
1418914306
"type": "object",

0 commit comments

Comments
 (0)