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