mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	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:
		| @@ -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) | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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). | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 a1012112796
					a1012112796