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.

207 lines
4.6 KiB

  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/util"
  10. "github.com/go-xorm/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
  22. Name string `xorm:"UNIQUE VARCHAR(25)"`
  23. RepoCount int
  24. CreatedUnix util.TimeStamp `xorm:"INDEX created"`
  25. UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
  26. }
  27. // RepoTopic represents associated repositories and topics
  28. type RepoTopic struct {
  29. RepoID int64 `xorm:"UNIQUE(s)"`
  30. TopicID int64 `xorm:"UNIQUE(s)"`
  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 topics by length and match pattern rules
  46. func ValidateTopic(topic string) bool {
  47. return len(topic) <= 35 && topicPattern.MatchString(topic)
  48. }
  49. // GetTopicByName retrieves topic by name
  50. func GetTopicByName(name string) (*Topic, error) {
  51. var topic Topic
  52. if has, err := x.Where("name = ?", name).Get(&topic); err != nil {
  53. return nil, err
  54. } else if !has {
  55. return nil, ErrTopicNotExist{name}
  56. }
  57. return &topic, nil
  58. }
  59. // FindTopicOptions represents the options when fdin topics
  60. type FindTopicOptions struct {
  61. RepoID int64
  62. Keyword string
  63. Limit int
  64. Page int
  65. }
  66. func (opts *FindTopicOptions) toConds() builder.Cond {
  67. var cond = builder.NewCond()
  68. if opts.RepoID > 0 {
  69. cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID})
  70. }
  71. if opts.Keyword != "" {
  72. cond = cond.And(builder.Like{"topic.name", opts.Keyword})
  73. }
  74. return cond
  75. }
  76. // FindTopics retrieves the topics via FindTopicOptions
  77. func FindTopics(opts *FindTopicOptions) (topics []*Topic, err error) {
  78. sess := x.Select("topic.*").Where(opts.toConds())
  79. if opts.RepoID > 0 {
  80. sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
  81. }
  82. if opts.Limit > 0 {
  83. sess.Limit(opts.Limit, opts.Page*opts.Limit)
  84. }
  85. return topics, sess.Desc("topic.repo_count").Find(&topics)
  86. }
  87. // SaveTopics save topics to a repository
  88. func SaveTopics(repoID int64, topicNames ...string) error {
  89. topics, err := FindTopics(&FindTopicOptions{
  90. RepoID: repoID,
  91. })
  92. if err != nil {
  93. return err
  94. }
  95. sess := x.NewSession()
  96. defer sess.Close()
  97. if err := sess.Begin(); err != nil {
  98. return err
  99. }
  100. var addedTopicNames []string
  101. for _, topicName := range topicNames {
  102. if strings.TrimSpace(topicName) == "" {
  103. continue
  104. }
  105. var found bool
  106. for _, t := range topics {
  107. if strings.EqualFold(topicName, t.Name) {
  108. found = true
  109. break
  110. }
  111. }
  112. if !found {
  113. addedTopicNames = append(addedTopicNames, topicName)
  114. }
  115. }
  116. var removeTopics []*Topic
  117. for _, t := range topics {
  118. var found bool
  119. for _, topicName := range topicNames {
  120. if strings.EqualFold(topicName, t.Name) {
  121. found = true
  122. break
  123. }
  124. }
  125. if !found {
  126. removeTopics = append(removeTopics, t)
  127. }
  128. }
  129. for _, topicName := range addedTopicNames {
  130. var topic Topic
  131. if has, err := sess.Where("name = ?", topicName).Get(&topic); err != nil {
  132. return err
  133. } else if !has {
  134. topic.Name = topicName
  135. topic.RepoCount = 1
  136. if _, err := sess.Insert(&topic); err != nil {
  137. return err
  138. }
  139. } else {
  140. topic.RepoCount++
  141. if _, err := sess.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil {
  142. return err
  143. }
  144. }
  145. if _, err := sess.Insert(&RepoTopic{
  146. RepoID: repoID,
  147. TopicID: topic.ID,
  148. }); err != nil {
  149. return err
  150. }
  151. }
  152. for _, topic := range removeTopics {
  153. topic.RepoCount--
  154. if _, err := sess.ID(topic.ID).Cols("repo_count").Update(topic); err != nil {
  155. return err
  156. }
  157. if _, err := sess.Delete(&RepoTopic{
  158. RepoID: repoID,
  159. TopicID: topic.ID,
  160. }); err != nil {
  161. return err
  162. }
  163. }
  164. topicNames = make([]string, 0, 25)
  165. if err := sess.Table("topic").Cols("name").
  166. Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id").
  167. Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil {
  168. return err
  169. }
  170. if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{
  171. Topics: topicNames,
  172. }); err != nil {
  173. return err
  174. }
  175. return sess.Commit()
  176. }