code.gitea.io/gitea@v1.21.7/routers/api/v1/repo/topic.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repo 5 6 import ( 7 "net/http" 8 "strings" 9 10 repo_model "code.gitea.io/gitea/models/repo" 11 "code.gitea.io/gitea/modules/context" 12 "code.gitea.io/gitea/modules/log" 13 api "code.gitea.io/gitea/modules/structs" 14 "code.gitea.io/gitea/modules/web" 15 "code.gitea.io/gitea/routers/api/v1/utils" 16 "code.gitea.io/gitea/services/convert" 17 ) 18 19 // ListTopics returns list of current topics for repo 20 func ListTopics(ctx *context.APIContext) { 21 // swagger:operation GET /repos/{owner}/{repo}/topics repository repoListTopics 22 // --- 23 // summary: Get list of topics that a repository has 24 // produces: 25 // - application/json 26 // parameters: 27 // - name: owner 28 // in: path 29 // description: owner of the repo 30 // type: string 31 // required: true 32 // - name: repo 33 // in: path 34 // description: name of the repo 35 // type: string 36 // required: true 37 // - name: page 38 // in: query 39 // description: page number of results to return (1-based) 40 // type: integer 41 // - name: limit 42 // in: query 43 // description: page size of results 44 // type: integer 45 // responses: 46 // "200": 47 // "$ref": "#/responses/TopicNames" 48 // "404": 49 // "$ref": "#/responses/notFound" 50 51 opts := &repo_model.FindTopicOptions{ 52 ListOptions: utils.GetListOptions(ctx), 53 RepoID: ctx.Repo.Repository.ID, 54 } 55 56 topics, total, err := repo_model.FindTopics(ctx, opts) 57 if err != nil { 58 ctx.InternalServerError(err) 59 return 60 } 61 62 topicNames := make([]string, len(topics)) 63 for i, topic := range topics { 64 topicNames[i] = topic.Name 65 } 66 67 ctx.SetTotalCountHeader(total) 68 ctx.JSON(http.StatusOK, map[string]any{ 69 "topics": topicNames, 70 }) 71 } 72 73 // UpdateTopics updates repo with a new set of topics 74 func UpdateTopics(ctx *context.APIContext) { 75 // swagger:operation PUT /repos/{owner}/{repo}/topics repository repoUpdateTopics 76 // --- 77 // summary: Replace list of topics for a repository 78 // produces: 79 // - application/json 80 // parameters: 81 // - name: owner 82 // in: path 83 // description: owner of the repo 84 // type: string 85 // required: true 86 // - name: repo 87 // in: path 88 // description: name of the repo 89 // type: string 90 // required: true 91 // - name: body 92 // in: body 93 // schema: 94 // "$ref": "#/definitions/RepoTopicOptions" 95 // responses: 96 // "204": 97 // "$ref": "#/responses/empty" 98 // "404": 99 // "$ref": "#/responses/notFound" 100 // "422": 101 // "$ref": "#/responses/invalidTopicsError" 102 103 form := web.GetForm(ctx).(*api.RepoTopicOptions) 104 topicNames := form.Topics 105 validTopics, invalidTopics := repo_model.SanitizeAndValidateTopics(topicNames) 106 107 if len(validTopics) > 25 { 108 ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ 109 "invalidTopics": nil, 110 "message": "Exceeding maximum number of topics per repo", 111 }) 112 return 113 } 114 115 if len(invalidTopics) > 0 { 116 ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ 117 "invalidTopics": invalidTopics, 118 "message": "Topic names are invalid", 119 }) 120 return 121 } 122 123 err := repo_model.SaveTopics(ctx, ctx.Repo.Repository.ID, validTopics...) 124 if err != nil { 125 log.Error("SaveTopics failed: %v", err) 126 ctx.InternalServerError(err) 127 return 128 } 129 130 ctx.Status(http.StatusNoContent) 131 } 132 133 // AddTopic adds a topic name to a repo 134 func AddTopic(ctx *context.APIContext) { 135 // swagger:operation PUT /repos/{owner}/{repo}/topics/{topic} repository repoAddTopic 136 // --- 137 // summary: Add a topic to a repository 138 // produces: 139 // - application/json 140 // parameters: 141 // - name: owner 142 // in: path 143 // description: owner of the repo 144 // type: string 145 // required: true 146 // - name: repo 147 // in: path 148 // description: name of the repo 149 // type: string 150 // required: true 151 // - name: topic 152 // in: path 153 // description: name of the topic to add 154 // type: string 155 // required: true 156 // responses: 157 // "204": 158 // "$ref": "#/responses/empty" 159 // "404": 160 // "$ref": "#/responses/notFound" 161 // "422": 162 // "$ref": "#/responses/invalidTopicsError" 163 164 topicName := strings.TrimSpace(strings.ToLower(ctx.Params(":topic"))) 165 166 if !repo_model.ValidateTopic(topicName) { 167 ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ 168 "invalidTopics": topicName, 169 "message": "Topic name is invalid", 170 }) 171 return 172 } 173 174 // Prevent adding more topics than allowed to repo 175 count, err := repo_model.CountTopics(ctx, &repo_model.FindTopicOptions{ 176 RepoID: ctx.Repo.Repository.ID, 177 }) 178 if err != nil { 179 log.Error("CountTopics failed: %v", err) 180 ctx.InternalServerError(err) 181 return 182 } 183 if count >= 25 { 184 ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ 185 "message": "Exceeding maximum allowed topics per repo.", 186 }) 187 return 188 } 189 190 _, err = repo_model.AddTopic(ctx, ctx.Repo.Repository.ID, topicName) 191 if err != nil { 192 log.Error("AddTopic failed: %v", err) 193 ctx.InternalServerError(err) 194 return 195 } 196 197 ctx.Status(http.StatusNoContent) 198 } 199 200 // DeleteTopic removes topic name from repo 201 func DeleteTopic(ctx *context.APIContext) { 202 // swagger:operation DELETE /repos/{owner}/{repo}/topics/{topic} repository repoDeleteTopic 203 // --- 204 // summary: Delete a topic from a repository 205 // produces: 206 // - application/json 207 // parameters: 208 // - name: owner 209 // in: path 210 // description: owner of the repo 211 // type: string 212 // required: true 213 // - name: repo 214 // in: path 215 // description: name of the repo 216 // type: string 217 // required: true 218 // - name: topic 219 // in: path 220 // description: name of the topic to delete 221 // type: string 222 // required: true 223 // responses: 224 // "204": 225 // "$ref": "#/responses/empty" 226 // "404": 227 // "$ref": "#/responses/notFound" 228 // "422": 229 // "$ref": "#/responses/invalidTopicsError" 230 231 topicName := strings.TrimSpace(strings.ToLower(ctx.Params(":topic"))) 232 233 if !repo_model.ValidateTopic(topicName) { 234 ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ 235 "invalidTopics": topicName, 236 "message": "Topic name is invalid", 237 }) 238 return 239 } 240 241 topic, err := repo_model.DeleteTopic(ctx, ctx.Repo.Repository.ID, topicName) 242 if err != nil { 243 log.Error("DeleteTopic failed: %v", err) 244 ctx.InternalServerError(err) 245 return 246 } 247 248 if topic == nil { 249 ctx.NotFound() 250 return 251 } 252 253 ctx.Status(http.StatusNoContent) 254 } 255 256 // TopicSearch search for creating topic 257 func TopicSearch(ctx *context.APIContext) { 258 // swagger:operation GET /topics/search repository topicSearch 259 // --- 260 // summary: search topics via keyword 261 // produces: 262 // - application/json 263 // parameters: 264 // - name: q 265 // in: query 266 // description: keywords to search 267 // required: true 268 // type: string 269 // - name: page 270 // in: query 271 // description: page number of results to return (1-based) 272 // type: integer 273 // - name: limit 274 // in: query 275 // description: page size of results 276 // type: integer 277 // responses: 278 // "200": 279 // "$ref": "#/responses/TopicListResponse" 280 // "403": 281 // "$ref": "#/responses/forbidden" 282 // "404": 283 // "$ref": "#/responses/notFound" 284 285 opts := &repo_model.FindTopicOptions{ 286 Keyword: ctx.FormString("q"), 287 ListOptions: utils.GetListOptions(ctx), 288 } 289 290 topics, total, err := repo_model.FindTopics(ctx, opts) 291 if err != nil { 292 ctx.InternalServerError(err) 293 return 294 } 295 296 topicResponses := make([]*api.TopicResponse, len(topics)) 297 for i, topic := range topics { 298 topicResponses[i] = convert.ToTopicResponse(topic) 299 } 300 301 ctx.SetTotalCountHeader(total) 302 ctx.JSON(http.StatusOK, map[string]any{ 303 "topics": topicResponses, 304 }) 305 }