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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. // Copyright 2018 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 models
  5. import (
  6. "fmt"
  7. "regexp"
  8. "strings"
  9. "code.gitea.io/gitea/modules/timeutil"
  10. "xorm.io/builder"
  11. )
  12. func init() {
  13. tables = append(tables,
  14. new(Topic),
  15. new(RepoTopic),
  16. )
  17. }
  18. var topicPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
  19. // Topic represents a topic of repositories
  20. type Topic struct {
  21. ID int64 `xorm:"pk autoincr"`
  22. Name string `xorm:"UNIQUE VARCHAR(50)"`
  23. RepoCount int
  24. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  25. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  26. }
  27. // RepoTopic represents associated repositories and topics
  28. type RepoTopic struct {
  29. RepoID int64 `xorm:"pk"`
  30. TopicID int64 `xorm:"pk"`
  31. }
  32. // ErrTopicNotExist represents an error that a topic is not exist
  33. type ErrTopicNotExist struct {
  34. Name string
  35. }
  36. // IsErrTopicNotExist checks if an error is an ErrTopicNotExist.
  37. func IsErrTopicNotExist(err error) bool {
  38. _, ok := err.(ErrTopicNotExist)
  39. return ok
  40. }
  41. // Error implements error interface
  42. func (err ErrTopicNotExist) Error() string {
  43. return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
  44. }
  45. // ValidateTopic checks a topic by length and match pattern rules
  46. func ValidateTopic(topic string) bool {
  47. return len(topic) <= 35 && topicPattern.MatchString(topic)
  48. }
  49. // SanitizeAndValidateTopics sanitizes and checks an array or topics
  50. func SanitizeAndValidateTopics(topics []string) (validTopics, invalidTopics []string) {
  51. validTopics = make([]string, 0)
  52. mValidTopics := make(map[string]struct{})
  53. invalidTopics = make([]string, 0)
  54. for _, topic := range topics {
  55. topic = strings.TrimSpace(strings.ToLower(topic))
  56. // ignore empty string
  57. if len(topic) == 0 {
  58. continue
  59. }
  60. // ignore same topic twice
  61. if _, ok := mValidTopics[topic]; ok {
  62. continue
  63. }
  64. if ValidateTopic(topic) {
  65. validTopics = append(validTopics, topic)
  66. mValidTopics[topic] = struct{}{}
  67. } else {
  68. invalidTopics = append(invalidTopics, topic)
  69. }
  70. }
  71. return validTopics, invalidTopics
  72. }
  73. // GetTopicByName retrieves topic by name
  74. func GetTopicByName(name string) (*Topic, error) {
  75. var topic Topic
  76. if has, err := x.Where("name = ?", name).Get(&topic); err != nil {
  77. return nil, err
  78. } else if !has {
  79. return nil, ErrTopicNotExist{name}
  80. }
  81. return &topic, nil
  82. }
  83. // addTopicByNameToRepo adds a topic name to a repo and increments the topic count.
  84. // Returns topic after the addition
  85. func addTopicByNameToRepo(e Engine, repoID int64, topicName string) (*Topic, error) {
  86. var topic Topic
  87. has, err := e.Where("name = ?", topicName).Get(&topic)
  88. if err != nil {
  89. return nil, err
  90. }
  91. if !has {
  92. topic.Name = topicName
  93. topic.RepoCount = 1
  94. if _, err := e.Insert(&topic); err != nil {
  95. return nil, err
  96. }
  97. } else {
  98. topic.RepoCount++
  99. if _, err := e.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil {
  100. return nil, err
  101. }
  102. }
  103. if _, err := e.Insert(&RepoTopic{
  104. RepoID: repoID,
  105. TopicID: topic.ID,
  106. }); err != nil {
  107. return nil, err
  108. }
  109. return &topic, nil
  110. }
  111. // removeTopicFromRepo remove a topic from a repo and decrements the topic repo count
  112. func removeTopicFromRepo(e Engine, repoID int64, topic *Topic) error {
  113. topic.RepoCount--
  114. if _, err := e.ID(topic.ID).Cols("repo_count").Update(topic); err != nil {
  115. return err
  116. }
  117. if _, err := e.Delete(&RepoTopic{
  118. RepoID: repoID,
  119. TopicID: topic.ID,
  120. }); err != nil {
  121. return err
  122. }
  123. return nil
  124. }
  125. // removeTopicsFromRepo remove all topics from the repo and decrements respective topics repo count
  126. func removeTopicsFromRepo(e Engine, repoID int64) error {
  127. _, err := e.Where(
  128. builder.In("id",
  129. builder.Select("topic_id").From("repo_topic").Where(builder.Eq{"repo_id": repoID}),
  130. ),
  131. ).Cols("repo_count").SetExpr("repo_count", "repo_count-1").Update(&Topic{})
  132. if err != nil {
  133. return err
  134. }
  135. if _, err = e.Delete(&RepoTopic{RepoID: repoID}); err != nil {
  136. return err
  137. }
  138. return nil
  139. }
  140. // FindTopicOptions represents the options when fdin topics
  141. type FindTopicOptions struct {
  142. ListOptions
  143. RepoID int64
  144. Keyword string
  145. }
  146. func (opts *FindTopicOptions) toConds() builder.Cond {
  147. cond := builder.NewCond()
  148. if opts.RepoID > 0 {
  149. cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID})
  150. }
  151. if opts.Keyword != "" {
  152. cond = cond.And(builder.Like{"topic.name", opts.Keyword})
  153. }
  154. return cond
  155. }
  156. // FindTopics retrieves the topics via FindTopicOptions
  157. func FindTopics(opts *FindTopicOptions) ([]*Topic, int64, error) {
  158. sess := x.Select("topic.*").Where(opts.toConds())
  159. if opts.RepoID > 0 {
  160. sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
  161. }
  162. if opts.PageSize != 0 && opts.Page != 0 {
  163. sess = opts.setSessionPagination(sess)
  164. }
  165. topics := make([]*Topic, 0, 10)
  166. total, err := sess.Desc("topic.repo_count").FindAndCount(&topics)
  167. return topics, total, err
  168. }
  169. // CountTopics counts the number of topics matching the FindTopicOptions
  170. func CountTopics(opts *FindTopicOptions) (int64, error) {
  171. sess := x.Where(opts.toConds())
  172. if opts.RepoID > 0 {
  173. sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
  174. }
  175. return sess.Count(new(Topic))
  176. }
  177. // GetRepoTopicByName retrieves topic from name for a repo if it exist
  178. func GetRepoTopicByName(repoID int64, topicName string) (*Topic, error) {
  179. return getRepoTopicByName(x, repoID, topicName)
  180. }
  181. func getRepoTopicByName(e Engine, repoID int64, topicName string) (*Topic, error) {
  182. cond := builder.NewCond()
  183. var topic Topic
  184. cond = cond.And(builder.Eq{"repo_topic.repo_id": repoID}).And(builder.Eq{"topic.name": topicName})
  185. sess := e.Table("topic").Where(cond)
  186. sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
  187. has, err := sess.Get(&topic)
  188. if has {
  189. return &topic, err
  190. }
  191. return nil, err
  192. }
  193. // AddTopic adds a topic name to a repository (if it does not already have it)
  194. func AddTopic(repoID int64, topicName string) (*Topic, error) {
  195. sess := x.NewSession()
  196. defer sess.Close()
  197. if err := sess.Begin(); err != nil {
  198. return nil, err
  199. }
  200. topic, err := getRepoTopicByName(sess, repoID, topicName)
  201. if err != nil {
  202. return nil, err
  203. }
  204. if topic != nil {
  205. // Repo already have topic
  206. return topic, nil
  207. }
  208. topic, err = addTopicByNameToRepo(sess, repoID, topicName)
  209. if err != nil {
  210. return nil, err
  211. }
  212. topicNames := make([]string, 0, 25)
  213. if err := sess.Select("name").Table("topic").
  214. Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id").
  215. Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil {
  216. return nil, err
  217. }
  218. if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{
  219. Topics: topicNames,
  220. }); err != nil {
  221. return nil, err
  222. }
  223. return topic, sess.Commit()
  224. }
  225. // DeleteTopic removes a topic name from a repository (if it has it)
  226. func DeleteTopic(repoID int64, topicName string) (*Topic, error) {
  227. topic, err := GetRepoTopicByName(repoID, topicName)
  228. if err != nil {
  229. return nil, err
  230. }
  231. if topic == nil {
  232. // Repo doesn't have topic, can't be removed
  233. return nil, nil
  234. }
  235. err = removeTopicFromRepo(x, repoID, topic)
  236. return topic, err
  237. }
  238. // SaveTopics save topics to a repository
  239. func SaveTopics(repoID int64, topicNames ...string) error {
  240. topics, _, err := FindTopics(&FindTopicOptions{
  241. RepoID: repoID,
  242. })
  243. if err != nil {
  244. return err
  245. }
  246. sess := x.NewSession()
  247. defer sess.Close()
  248. if err := sess.Begin(); err != nil {
  249. return err
  250. }
  251. var addedTopicNames []string
  252. for _, topicName := range topicNames {
  253. if strings.TrimSpace(topicName) == "" {
  254. continue
  255. }
  256. var found bool
  257. for _, t := range topics {
  258. if strings.EqualFold(topicName, t.Name) {
  259. found = true
  260. break
  261. }
  262. }
  263. if !found {
  264. addedTopicNames = append(addedTopicNames, topicName)
  265. }
  266. }
  267. var removeTopics []*Topic
  268. for _, t := range topics {
  269. var found bool
  270. for _, topicName := range topicNames {
  271. if strings.EqualFold(topicName, t.Name) {
  272. found = true
  273. break
  274. }
  275. }
  276. if !found {
  277. removeTopics = append(removeTopics, t)
  278. }
  279. }
  280. for _, topicName := range addedTopicNames {
  281. _, err := addTopicByNameToRepo(sess, repoID, topicName)
  282. if err != nil {
  283. return err
  284. }
  285. }
  286. for _, topic := range removeTopics {
  287. err := removeTopicFromRepo(sess, repoID, topic)
  288. if err != nil {
  289. return err
  290. }
  291. }
  292. topicNames = make([]string, 0, 25)
  293. if err := sess.Table("topic").Cols("name").
  294. Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id").
  295. Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil {
  296. return err
  297. }
  298. if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{
  299. Topics: topicNames,
  300. }); err != nil {
  301. return err
  302. }
  303. return sess.Commit()
  304. }