Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating a repo from a template repo via API #15958

Merged
merged 7 commits into from
Jul 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions integrations/api_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions modules/structs/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
109 changes: 109 additions & 0 deletions routers/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions routers/api/v1/swagger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
117 changes: 117 additions & 0 deletions templates/swagger/v1_json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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",
Expand Down