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>
This commit is contained in:
a1012112796 2021-07-05 23:29:08 +08:00 committed by GitHub
parent 64122fe105
commit 5bb97a12d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 296 additions and 0 deletions

View file

@ -495,6 +495,43 @@ func TestAPIRepoTransfer(t *testing.T) {
_ = models.DeleteRepository(user, repo.OwnerID, repo.ID) _ = 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) { func TestAPIRepoGetReviewers(t *testing.T) {
defer prepareTestEnv(t)() defer prepareTestEnv(t)()
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)

View file

@ -180,6 +180,36 @@ type EditRepoOption struct {
MirrorInterval *string `json:"mirror_interval,omitempty"` 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 // CreateBranchRepoOption options when creating a branch in a repository
// swagger:model // swagger:model
type CreateBranchRepoOption struct { type CreateBranchRepoOption struct {

View file

@ -722,6 +722,7 @@ func Routes() *web.Route {
m.Combo("").Get(reqAnyRepoReader(), repo.Get). m.Combo("").Get(reqAnyRepoReader(), repo.Get).
Delete(reqToken(), reqOwner(), repo.Delete). Delete(reqToken(), reqOwner(), repo.Delete).
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) 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.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
m.Combo("/notifications"). m.Combo("/notifications").
Get(reqToken(), notify.ListRepoNotifications). Get(reqToken(), notify.ListRepoNotifications).

View file

@ -307,6 +307,115 @@ func Create(ctx *context.APIContext) {
CreateUserRepo(ctx, ctx.User, *opt) 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 // CreateOrgRepoDeprecated create one repository of the organization
func CreateOrgRepoDeprecated(ctx *context.APIContext) { func CreateOrgRepoDeprecated(ctx *context.APIContext) {
// swagger:operation POST /org/{org}/repos organization createOrgRepoDeprecated // swagger:operation POST /org/{org}/repos organization createOrgRepoDeprecated

View file

@ -87,6 +87,8 @@ type swaggerParameterBodies struct {
TransferRepoOption api.TransferRepoOption TransferRepoOption api.TransferRepoOption
// in:body // in:body
CreateForkOption api.CreateForkOption CreateForkOption api.CreateForkOption
// in:body
GenerateRepoOption api.GenerateRepoOption
// in:body // in:body
CreateStatusOption api.CreateStatusOption CreateStatusOption api.CreateStatusOption

View file

@ -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}": { "/repositories/{id}": {
"get": { "get": {
"produces": [ "produces": [
@ -14551,6 +14606,68 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "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": { "GitBlobResponse": {
"description": "GitBlobResponse represents a git blob", "description": "GitBlobResponse represents a git blob",
"type": "object", "type": "object",