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 6.8KB

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