summaryrefslogtreecommitdiffstats
path: root/routers/api
diff options
context:
space:
mode:
authorDavid Svantesson <davidsvantesson@gmail.com>2019-09-03 17:46:24 +0200
committerLunny Xiao <xiaolunwen@gmail.com>2019-09-03 23:46:24 +0800
commit1f951cdebae314a45b5e4ec1d464f1b4a53c8002 (patch)
tree5c22a1872beaa218599374678491a2fd66323075 /routers/api
parent99d6863238d8717c2f14bc72ab023a0f683c50cc (diff)
downloadgitea-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.go8
-rw-r--r--routers/api/v1/convert/convert.go11
-rw-r--r--routers/api/v1/repo/repo.go42
-rw-r--r--routers/api/v1/repo/topic.go274
-rw-r--r--routers/api/v1/swagger/options.go3
-rw-r--r--routers/api/v1/swagger/repo.go14
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"`
+}