You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

topic.go 7.4KB

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