From 5bb97a12d79309e799e9826badf3527f03815dd2 Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Mon, 5 Jul 2021 23:29:08 +0800 Subject: [PATCH] Creating a repo from a template repo via API (#15958) * Creating a repo from a template repo via API fix #15934 ref: https://docs.github.com/en/rest/reference/repos#create-a-repository-using-a-template Signed-off-by: a1012112796 <1012112796@qq.com> --- integrations/api_repo_test.go | 37 ++++++++++ modules/structs/repo.go | 30 ++++++++ routers/api/v1/api.go | 1 + routers/api/v1/repo/repo.go | 109 ++++++++++++++++++++++++++++ routers/api/v1/swagger/options.go | 2 + templates/swagger/v1_json.tmpl | 117 ++++++++++++++++++++++++++++++ 6 files changed, 296 insertions(+) diff --git a/integrations/api_repo_test.go b/integrations/api_repo_test.go index 7052e74b01..3948489f56 100644 --- a/integrations/api_repo_test.go +++ b/integrations/api_repo_test.go @@ -495,6 +495,43 @@ func TestAPIRepoTransfer(t *testing.T) { _ = models.DeleteRepository(user, repo.OwnerID, repo.ID) } +func TestAPIGenerateRepo(t *testing.T) { + defer prepareTestEnv(t)() + + user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User) + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session) + + templateRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 44}).(*models.Repository) + + // user + repo := new(api.Repository) + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate?token=%s", templateRepo.OwnerName, templateRepo.Name, token), &api.GenerateRepoOption{ + Owner: user.Name, + Name: "new-repo", + Description: "test generate repo", + Private: false, + GitContent: true, + }) + resp := session.MakeRequest(t, req, http.StatusCreated) + DecodeJSON(t, resp, repo) + + assert.Equal(t, "new-repo", repo.Name) + + // org + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate?token=%s", templateRepo.OwnerName, templateRepo.Name, token), &api.GenerateRepoOption{ + Owner: "user3", + Name: "new-repo", + Description: "test generate repo", + Private: false, + GitContent: true, + }) + resp = session.MakeRequest(t, req, http.StatusCreated) + DecodeJSON(t, resp, repo) + + assert.Equal(t, "new-repo", repo.Name) +} + func TestAPIRepoGetReviewers(t *testing.T) { defer prepareTestEnv(t)() user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 4fdc1e54cb..cef864c020 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -180,6 +180,36 @@ type EditRepoOption struct { MirrorInterval *string `json:"mirror_interval,omitempty"` } +// GenerateRepoOption options when creating repository using a template +// swagger:model +type GenerateRepoOption struct { + // The organization or person who will own the new repository + // + // required: true + Owner string `json:"owner"` + // Name of the repository to create + // + // required: true + // unique: true + Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(100)"` + // Description of the repository to create + Description string `json:"description" binding:"MaxSize(255)"` + // Whether the repository is private + Private bool `json:"private"` + // include git content of default branch in template repo + GitContent bool `json:"git_content"` + // include topics in template repo + Topics bool `json:"topics"` + // include git hooks in template repo + GitHooks bool `json:"git_hooks"` + // include webhooks in template repo + Webhooks bool `json:"webhooks"` + // include avatar of the template repo + Avatar bool `json:"avatar"` + // include labels in template repo + Labels bool `json:"labels"` +} + // CreateBranchRepoOption options when creating a branch in a repository // swagger:model type CreateBranchRepoOption struct { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b6913ea1bc..b4f14bf2d1 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -722,6 +722,7 @@ func Routes() *web.Route { m.Combo("").Get(reqAnyRepoReader(), repo.Get). Delete(reqToken(), reqOwner(), repo.Delete). Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) + m.Post("/generate", reqToken(), reqRepoReader(models.UnitTypeCode), bind(api.GenerateRepoOption{}), repo.Generate) m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer) m.Combo("/notifications"). Get(reqToken(), notify.ListRepoNotifications). diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 35d3490510..5d397191a6 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -307,6 +307,115 @@ func Create(ctx *context.APIContext) { CreateUserRepo(ctx, ctx.User, *opt) } +// Generate Create a repository using a template +func Generate(ctx *context.APIContext) { + // swagger:operation POST /repos/{template_owner}/{template_repo}/generate repository generateRepo + // --- + // summary: Create a repository using a template + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: template_owner + // in: path + // description: name of the template repository owner + // type: string + // required: true + // - name: template_repo + // in: path + // description: name of the template repository + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/GenerateRepoOption" + // responses: + // "201": + // "$ref": "#/responses/Repository" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "409": + // description: The repository with the same name already exists. + // "422": + // "$ref": "#/responses/validationError" + form := web.GetForm(ctx).(*api.GenerateRepoOption) + + if !ctx.Repo.Repository.IsTemplate { + ctx.Error(http.StatusUnprocessableEntity, "", "this is not a template repo") + return + } + + if ctx.User.IsOrganization() { + ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization") + return + } + + opts := models.GenerateRepoOptions{ + Name: form.Name, + Description: form.Description, + Private: form.Private, + GitContent: form.GitContent, + Topics: form.Topics, + GitHooks: form.GitHooks, + Webhooks: form.Webhooks, + Avatar: form.Avatar, + IssueLabels: form.Labels, + } + + if !opts.IsValid() { + ctx.Error(http.StatusUnprocessableEntity, "", "must select at least one template item") + return + } + + ctxUser := ctx.User + var err error + if form.Owner != ctxUser.Name { + ctxUser, err = models.GetOrgByName(form.Owner) + if err != nil { + if models.IsErrOrgNotExist(err) { + ctx.JSON(http.StatusNotFound, map[string]interface{}{ + "error": "request owner `" + form.Name + "` is not exist", + }) + return + } + + ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) + return + } + + if !ctx.User.IsAdmin { + canCreate, err := ctxUser.CanCreateOrgRepo(ctx.User.ID) + if err != nil { + ctx.ServerError("CanCreateOrgRepo", err) + return + } else if !canCreate { + ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.") + return + } + } + } + + repo, err := repo_service.GenerateRepository(ctx.User, ctxUser, ctx.Repo.Repository, opts) + if err != nil { + if models.IsErrRepoAlreadyExist(err) { + ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") + } else if models.IsErrNameReserved(err) || + models.IsErrNamePatternNotAllowed(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateRepository", err) + } + return + } + log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) + + ctx.JSON(http.StatusCreated, convert.ToRepo(repo, models.AccessModeOwner)) +} + // CreateOrgRepoDeprecated create one repository of the organization func CreateOrgRepoDeprecated(ctx *context.APIContext) { // swagger:operation POST /org/{org}/repos organization createOrgRepoDeprecated diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index b5f34e86a3..0ae96a9203 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -87,6 +87,8 @@ type swaggerParameterBodies struct { TransferRepoOption api.TransferRepoOption // in:body CreateForkOption api.CreateForkOption + // in:body + GenerateRepoOption api.GenerateRepoOption // in:body CreateStatusOption api.CreateStatusOption diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index a2e449228e..669e3552cc 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -9777,6 +9777,61 @@ } } }, + "/repos/{template_owner}/{template_repo}/generate": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Create a repository using a template", + "operationId": "generateRepo", + "parameters": [ + { + "type": "string", + "description": "name of the template repository owner", + "name": "template_owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the template repository", + "name": "template_repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/GenerateRepoOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Repository" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "409": { + "description": "The repository with the same name already exists." + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/repositories/{id}": { "get": { "produces": [ @@ -14551,6 +14606,68 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "GenerateRepoOption": { + "description": "GenerateRepoOption options when creating repository using a template", + "type": "object", + "required": [ + "owner", + "name" + ], + "properties": { + "avatar": { + "description": "include avatar of the template repo", + "type": "boolean", + "x-go-name": "Avatar" + }, + "description": { + "description": "Description of the repository to create", + "type": "string", + "x-go-name": "Description" + }, + "git_content": { + "description": "include git content of default branch in template repo", + "type": "boolean", + "x-go-name": "GitContent" + }, + "git_hooks": { + "description": "include git hooks in template repo", + "type": "boolean", + "x-go-name": "GitHooks" + }, + "labels": { + "description": "include labels in template repo", + "type": "boolean", + "x-go-name": "Labels" + }, + "name": { + "description": "Name of the repository to create", + "type": "string", + "uniqueItems": true, + "x-go-name": "Name" + }, + "owner": { + "description": "The organization or person who will own the new repository", + "type": "string", + "x-go-name": "Owner" + }, + "private": { + "description": "Whether the repository is private", + "type": "boolean", + "x-go-name": "Private" + }, + "topics": { + "description": "include topics in template repo", + "type": "boolean", + "x-go-name": "Topics" + }, + "webhooks": { + "description": "include webhooks in template repo", + "type": "boolean", + "x-go-name": "Webhooks" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "GitBlobResponse": { "description": "GitBlobResponse represents a git blob", "type": "object",