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.8KB

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