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:
		
							parent
							
								
									64122fe105
								
							
						
					
					
						commit
						5bb97a12d7
					
				
					 6 changed files with 296 additions and 0 deletions
				
			
		|  | @ -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) | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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). | ||||||
|  |  | ||||||
|  | @ -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
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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", | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue