diff options
author | David Svantesson <davidsvantesson@gmail.com> | 2019-09-03 17:46:24 +0200 |
---|---|---|
committer | Lunny Xiao <xiaolunwen@gmail.com> | 2019-09-03 23:46:24 +0800 |
commit | 1f951cdebae314a45b5e4ec1d464f1b4a53c8002 (patch) | |
tree | 5c22a1872beaa218599374678491a2fd66323075 /routers/api | |
parent | 99d6863238d8717c2f14bc72ab023a0f683c50cc (diff) | |
download | gitea-1f951cdebae314a45b5e4ec1d464f1b4a53c8002.tar.gz gitea-1f951cdebae314a45b5e4ec1d464f1b4a53c8002.zip |
Add API endpoint for accessing repo topics (#7963)
* Create API endpoints for repo topics.
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Generate swagger
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Add documentation to functions
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Grammar fix
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Fix function comment
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Can't use FindTopics when looking for a single repo topic, as it doesnt use exact match
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Add PUT /repos/{owner}/{repo}/topics and remove GET /repos/{owner}/{repo}/topics
* Ignore if topic is sent twice in same request, refactoring.
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Fix topic dropdown with api changes.
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Style fix
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Update API documentation
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Better way to handle duplicate topics in slice
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Make response element TopicName an array of strings, instead of using an array of TopicName
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Add test cases for API Repo Topics.
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Fix format of tests
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Fix comments
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Fix unit tests after adding some more topics to the test fixture.
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Update models/topic.go
Limit multiple if else if ...
Co-Authored-By: Antoine GIRARD <sapk@users.noreply.github.com>
* Engine as first parameter in function
Co-Authored-By: Antoine GIRARD <sapk@users.noreply.github.com>
* Replace magic numbers with http status code constants.
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Fix variable scope
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Test one read with login and one with token
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Add some more tests
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Apply suggestions from code review
Use empty struct for efficiency
Co-Authored-By: Lauris BH <lauris@nix.lv>
* Add test case to check access for user with write access
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Fix access, repo admin required to change topics
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Correct first test to be without token
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Any repo reader should be able to access topics.
* No need for string pointer
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
Diffstat (limited to 'routers/api')
-rw-r--r-- | routers/api/v1/api.go | 8 | ||||
-rw-r--r-- | routers/api/v1/convert/convert.go | 11 | ||||
-rw-r--r-- | routers/api/v1/repo/repo.go | 42 | ||||
-rw-r--r-- | routers/api/v1/repo/topic.go | 274 | ||||
-rw-r--r-- | routers/api/v1/swagger/options.go | 3 | ||||
-rw-r--r-- | routers/api/v1/swagger/repo.go | 14 |
6 files changed, 310 insertions, 42 deletions
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 2842d78cd3..c57edf6a99 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -771,6 +771,14 @@ func RegisterRoutes(m *macaron.Macaron) { m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile) }, reqRepoWriter(models.UnitTypeCode), reqToken()) }, reqRepoReader(models.UnitTypeCode)) + m.Group("/topics", func() { + m.Combo("").Get(repo.ListTopics). + Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics) + m.Group("/:topic", func() { + m.Combo("").Put(reqToken(), repo.AddTopic). + Delete(reqToken(), repo.DeleteTopic) + }, reqAdmin()) + }, reqAnyRepoReader()) }, repoAssignment()) }) diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go index 90202117cc..40e4ca7ae3 100644 --- a/routers/api/v1/convert/convert.go +++ b/routers/api/v1/convert/convert.go @@ -291,3 +291,14 @@ func ToCommitMeta(repo *models.Repository, tag *git.Tag) *api.CommitMeta { URL: util.URLJoin(repo.APIURL(), "git/commits", tag.ID.String()), } } + +// ToTopicResponse convert from models.Topic to api.TopicResponse +func ToTopicResponse(topic *models.Topic) *api.TopicResponse { + return &api.TopicResponse{ + ID: topic.ID, + Name: topic.Name, + RepoCount: topic.RepoCount, + Created: topic.CreatedUnix.AsTime(), + Updated: topic.UpdatedUnix.AsTime(), + } +} diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index eccff8c387..82bfa58b7a 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -871,45 +871,3 @@ func MirrorSync(ctx *context.APIContext) { go models.MirrorQueue.Add(repo.ID) ctx.Status(200) } - -// TopicSearch search for creating topic -func TopicSearch(ctx *context.Context) { - // swagger:operation GET /topics/search repository topicSearch - // --- - // summary: search topics via keyword - // produces: - // - application/json - // parameters: - // - name: q - // in: query - // description: keywords to search - // required: true - // type: string - // responses: - // "200": - // "$ref": "#/responses/Repository" - if ctx.User == nil { - ctx.JSON(403, map[string]interface{}{ - "message": "Only owners could change the topics.", - }) - return - } - - kw := ctx.Query("q") - - topics, err := models.FindTopics(&models.FindTopicOptions{ - Keyword: kw, - Limit: 10, - }) - if err != nil { - log.Error("SearchTopics failed: %v", err) - ctx.JSON(500, map[string]interface{}{ - "message": "Search topics failed.", - }) - return - } - - ctx.JSON(200, map[string]interface{}{ - "topics": topics, - }) -} diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go new file mode 100644 index 0000000000..6c3ac0020a --- /dev/null +++ b/routers/api/v1/repo/topic.go @@ -0,0 +1,274 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/routers/api/v1/convert" +) + +// ListTopics returns list of current topics for repo +func ListTopics(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/topics repository repoListTopics + // --- + // summary: Get list of topics that a repository has + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/TopicNames" + + topics, err := models.FindTopics(&models.FindTopicOptions{ + RepoID: ctx.Repo.Repository.ID, + }) + if err != nil { + log.Error("ListTopics failed: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "message": "ListTopics failed.", + }) + return + } + + topicNames := make([]string, len(topics)) + for i, topic := range topics { + topicNames[i] = topic.Name + } + ctx.JSON(http.StatusOK, map[string]interface{}{ + "topics": topicNames, + }) +} + +// UpdateTopics updates repo with a new set of topics +func UpdateTopics(ctx *context.APIContext, form api.RepoTopicOptions) { + // swagger:operation PUT /repos/{owner}/{repo}/topics repository repoUpdateTopics + // --- + // summary: Replace list of topics for a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/RepoTopicOptions" + // responses: + // "204": + // "$ref": "#/responses/empty" + + topicNames := form.Topics + validTopics, invalidTopics := models.SanitizeAndValidateTopics(topicNames) + + if len(validTopics) > 25 { + ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{ + "invalidTopics": nil, + "message": "Exceeding maximum number of topics per repo", + }) + return + } + + if len(invalidTopics) > 0 { + ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{ + "invalidTopics": invalidTopics, + "message": "Topic names are invalid", + }) + return + } + + err := models.SaveTopics(ctx.Repo.Repository.ID, validTopics...) + if err != nil { + log.Error("SaveTopics failed: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "message": "Save topics failed.", + }) + return + } + + ctx.Status(http.StatusNoContent) +} + +// AddTopic adds a topic name to a repo +func AddTopic(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/topics/{topic} repository repoAddTopíc + // --- + // summary: Add a topic to a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: topic + // in: path + // description: name of the topic to add + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + + topicName := strings.TrimSpace(strings.ToLower(ctx.Params(":topic"))) + + if !models.ValidateTopic(topicName) { + ctx.Error(http.StatusUnprocessableEntity, "", "Topic name is invalid") + return + } + + // Prevent adding more topics than allowed to repo + topics, err := models.FindTopics(&models.FindTopicOptions{ + RepoID: ctx.Repo.Repository.ID, + }) + if err != nil { + log.Error("AddTopic failed: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "message": "ListTopics failed.", + }) + return + } + if len(topics) >= 25 { + ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{ + "message": "Exceeding maximum allowed topics per repo.", + }) + return + } + + _, err = models.AddTopic(ctx.Repo.Repository.ID, topicName) + if err != nil { + log.Error("AddTopic failed: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "message": "AddTopic failed.", + }) + return + } + + ctx.Status(http.StatusNoContent) +} + +// DeleteTopic removes topic name from repo +func DeleteTopic(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/topics/{topic} repository repoDeleteTopic + // --- + // summary: Delete a topic from a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: topic + // in: path + // description: name of the topic to delete + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + topicName := strings.TrimSpace(strings.ToLower(ctx.Params(":topic"))) + + if !models.ValidateTopic(topicName) { + ctx.Error(http.StatusUnprocessableEntity, "", "Topic name is invalid") + return + } + + topic, err := models.DeleteTopic(ctx.Repo.Repository.ID, topicName) + if err != nil { + log.Error("DeleteTopic failed: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "message": "DeleteTopic failed.", + }) + return + } + + if topic == nil { + ctx.NotFound() + } + + ctx.Status(http.StatusNoContent) +} + +// TopicSearch search for creating topic +func TopicSearch(ctx *context.Context) { + // swagger:operation GET /topics/search repository topicSearch + // --- + // summary: search topics via keyword + // produces: + // - application/json + // parameters: + // - name: q + // in: query + // description: keywords to search + // required: true + // type: string + // responses: + // "200": + // "$ref": "#/responses/TopicListResponse" + if ctx.User == nil { + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "message": "Only owners could change the topics.", + }) + return + } + + kw := ctx.Query("q") + + topics, err := models.FindTopics(&models.FindTopicOptions{ + Keyword: kw, + Limit: 10, + }) + if err != nil { + log.Error("SearchTopics failed: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "message": "Search topics failed.", + }) + return + } + + topicResponses := make([]*api.TopicResponse, len(topics)) + for i, topic := range topics { + topicResponses[i] = convert.ToTopicResponse(topic) + } + ctx.JSON(http.StatusOK, map[string]interface{}{ + "topics": topicResponses, + }) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index c1196eeb71..80e4bf422a 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -117,4 +117,7 @@ type swaggerParameterBodies struct { // in:body DeleteFileOptions api.DeleteFileOptions + + // in:body + RepoTopicOptions api.RepoTopicOptions } diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index 422cc0861c..4ac5c6d2d5 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -246,3 +246,17 @@ type swaggerFileDeleteResponse struct { //in: body Body api.FileDeleteResponse `json:"body"` } + +// TopicListResponse +// swagger:response TopicListResponse +type swaggerTopicListResponse struct { + //in: body + Body []api.TopicResponse `json:"body"` +} + +// TopicNames +// swagger:response TopicNames +type swaggerTopicNames struct { + //in: body + Body api.TopicName `json:"body"` +} |