2018-04-11 05:51:44 +03:00
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"fmt"
2018-06-21 12:09:46 +03:00
"regexp"
2018-04-11 05:51:44 +03:00
"strings"
2021-09-19 14:49:59 +03:00
"code.gitea.io/gitea/models/db"
2019-08-15 17:46:21 +03:00
"code.gitea.io/gitea/modules/timeutil"
2018-04-11 05:51:44 +03:00
2019-06-23 18:22:43 +03:00
"xorm.io/builder"
2018-04-11 05:51:44 +03:00
)
func init ( ) {
2021-09-19 14:49:59 +03:00
db . RegisterModel ( new ( Topic ) )
db . RegisterModel ( new ( RepoTopic ) )
2018-04-11 05:51:44 +03:00
}
2018-06-21 12:09:46 +03:00
var topicPattern = regexp . MustCompile ( ` ^[a-z0-9][a-z0-9-]*$ ` )
2018-04-11 05:51:44 +03:00
// Topic represents a topic of repositories
type Topic struct {
2020-09-10 22:45:01 +03:00
ID int64 ` xorm:"pk autoincr" `
2020-12-27 02:28:47 +03:00
Name string ` xorm:"UNIQUE VARCHAR(50)" `
2018-04-11 05:51:44 +03:00
RepoCount int
2019-08-15 17:46:21 +03:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
2018-04-11 05:51:44 +03:00
}
// RepoTopic represents associated repositories and topics
type RepoTopic struct {
2020-09-10 22:45:01 +03:00
RepoID int64 ` xorm:"pk" `
TopicID int64 ` xorm:"pk" `
2018-04-11 05:51:44 +03:00
}
// ErrTopicNotExist represents an error that a topic is not exist
type ErrTopicNotExist struct {
Name string
}
// IsErrTopicNotExist checks if an error is an ErrTopicNotExist.
func IsErrTopicNotExist ( err error ) bool {
_ , ok := err . ( ErrTopicNotExist )
return ok
}
// Error implements error interface
func ( err ErrTopicNotExist ) Error ( ) string {
return fmt . Sprintf ( "topic is not exist [name: %s]" , err . Name )
}
2019-09-03 18:46:24 +03:00
// ValidateTopic checks a topic by length and match pattern rules
2018-06-21 12:09:46 +03:00
func ValidateTopic ( topic string ) bool {
return len ( topic ) <= 35 && topicPattern . MatchString ( topic )
}
2019-09-03 18:46:24 +03:00
// SanitizeAndValidateTopics sanitizes and checks an array or topics
2021-03-14 21:52:12 +03:00
func SanitizeAndValidateTopics ( topics [ ] string ) ( validTopics , invalidTopics [ ] string ) {
2019-09-03 18:46:24 +03:00
validTopics = make ( [ ] string , 0 )
mValidTopics := make ( map [ string ] struct { } )
invalidTopics = make ( [ ] string , 0 )
for _ , topic := range topics {
topic = strings . TrimSpace ( strings . ToLower ( topic ) )
// ignore empty string
if len ( topic ) == 0 {
continue
}
// ignore same topic twice
if _ , ok := mValidTopics [ topic ] ; ok {
continue
}
if ValidateTopic ( topic ) {
validTopics = append ( validTopics , topic )
mValidTopics [ topic ] = struct { } { }
} else {
invalidTopics = append ( invalidTopics , topic )
}
}
return validTopics , invalidTopics
}
2018-04-11 05:51:44 +03:00
// GetTopicByName retrieves topic by name
func GetTopicByName ( name string ) ( * Topic , error ) {
var topic Topic
2021-09-19 14:49:59 +03:00
if has , err := db . DefaultContext ( ) . Engine ( ) . Where ( "name = ?" , name ) . Get ( & topic ) ; err != nil {
2018-04-11 05:51:44 +03:00
return nil , err
} else if ! has {
return nil , ErrTopicNotExist { name }
}
return & topic , nil
}
2019-09-03 18:46:24 +03:00
// addTopicByNameToRepo adds a topic name to a repo and increments the topic count.
// Returns topic after the addition
2021-09-19 14:49:59 +03:00
func addTopicByNameToRepo ( e db . Engine , repoID int64 , topicName string ) ( * Topic , error ) {
2019-09-03 18:46:24 +03:00
var topic Topic
has , err := e . Where ( "name = ?" , topicName ) . Get ( & topic )
if err != nil {
return nil , err
}
if ! has {
topic . Name = topicName
topic . RepoCount = 1
if _ , err := e . Insert ( & topic ) ; err != nil {
return nil , err
}
} else {
topic . RepoCount ++
if _ , err := e . ID ( topic . ID ) . Cols ( "repo_count" ) . Update ( & topic ) ; err != nil {
return nil , err
}
}
if _ , err := e . Insert ( & RepoTopic {
RepoID : repoID ,
TopicID : topic . ID ,
} ) ; err != nil {
return nil , err
}
return & topic , nil
}
// removeTopicFromRepo remove a topic from a repo and decrements the topic repo count
2021-09-19 14:49:59 +03:00
func removeTopicFromRepo ( e db . Engine , repoID int64 , topic * Topic ) error {
2019-09-03 18:46:24 +03:00
topic . RepoCount --
if _ , err := e . ID ( topic . ID ) . Cols ( "repo_count" ) . Update ( topic ) ; err != nil {
return err
}
if _ , err := e . Delete ( & RepoTopic {
RepoID : repoID ,
TopicID : topic . ID ,
} ) ; err != nil {
return err
}
return nil
}
2020-01-31 09:57:19 +03:00
// removeTopicsFromRepo remove all topics from the repo and decrements respective topics repo count
2021-09-19 14:49:59 +03:00
func removeTopicsFromRepo ( e db . Engine , repoID int64 ) error {
2020-01-31 09:57:19 +03:00
_ , err := e . Where (
builder . In ( "id" ,
builder . Select ( "topic_id" ) . From ( "repo_topic" ) . Where ( builder . Eq { "repo_id" : repoID } ) ,
) ,
) . Cols ( "repo_count" ) . SetExpr ( "repo_count" , "repo_count-1" ) . Update ( & Topic { } )
if err != nil {
return err
}
if _ , err = e . Delete ( & RepoTopic { RepoID : repoID } ) ; err != nil {
return err
}
return nil
}
2018-04-11 05:51:44 +03:00
// FindTopicOptions represents the options when fdin topics
type FindTopicOptions struct {
2020-01-24 22:00:29 +03:00
ListOptions
2018-04-11 05:51:44 +03:00
RepoID int64
Keyword string
}
func ( opts * FindTopicOptions ) toConds ( ) builder . Cond {
2021-03-14 21:52:12 +03:00
cond := builder . NewCond ( )
2018-04-11 05:51:44 +03:00
if opts . RepoID > 0 {
cond = cond . And ( builder . Eq { "repo_topic.repo_id" : opts . RepoID } )
}
if opts . Keyword != "" {
cond = cond . And ( builder . Like { "topic.name" , opts . Keyword } )
}
return cond
}
// FindTopics retrieves the topics via FindTopicOptions
2021-08-12 15:43:08 +03:00
func FindTopics ( opts * FindTopicOptions ) ( [ ] * Topic , int64 , error ) {
2021-09-19 14:49:59 +03:00
sess := db . DefaultContext ( ) . Engine ( ) . Select ( "topic.*" ) . Where ( opts . toConds ( ) )
2018-04-11 05:51:44 +03:00
if opts . RepoID > 0 {
sess . Join ( "INNER" , "repo_topic" , "repo_topic.topic_id = topic.id" )
}
2020-01-24 22:00:29 +03:00
if opts . PageSize != 0 && opts . Page != 0 {
2021-09-14 20:48:27 +03:00
sess = setSessionPagination ( sess , opts )
2018-04-11 05:51:44 +03:00
}
2021-08-12 15:43:08 +03:00
topics := make ( [ ] * Topic , 0 , 10 )
total , err := sess . Desc ( "topic.repo_count" ) . FindAndCount ( & topics )
return topics , total , err
}
// CountTopics counts the number of topics matching the FindTopicOptions
func CountTopics ( opts * FindTopicOptions ) ( int64 , error ) {
2021-09-19 14:49:59 +03:00
sess := db . DefaultContext ( ) . Engine ( ) . Where ( opts . toConds ( ) )
2021-08-12 15:43:08 +03:00
if opts . RepoID > 0 {
sess . Join ( "INNER" , "repo_topic" , "repo_topic.topic_id = topic.id" )
}
return sess . Count ( new ( Topic ) )
2018-04-11 05:51:44 +03:00
}
2021-07-08 14:38:13 +03:00
// GetRepoTopicByName retrieves topic from name for a repo if it exist
2019-09-03 18:46:24 +03:00
func GetRepoTopicByName ( repoID int64 , topicName string ) ( * Topic , error ) {
2021-09-19 14:49:59 +03:00
return getRepoTopicByName ( db . DefaultContext ( ) . Engine ( ) , repoID , topicName )
2020-10-24 17:11:30 +03:00
}
2021-03-14 21:52:12 +03:00
2021-09-19 14:49:59 +03:00
func getRepoTopicByName ( e db . Engine , repoID int64 , topicName string ) ( * Topic , error ) {
2021-03-14 21:52:12 +03:00
cond := builder . NewCond ( )
2019-09-03 18:46:24 +03:00
var topic Topic
cond = cond . And ( builder . Eq { "repo_topic.repo_id" : repoID } ) . And ( builder . Eq { "topic.name" : topicName } )
2020-10-24 17:11:30 +03:00
sess := e . Table ( "topic" ) . Where ( cond )
2019-09-03 18:46:24 +03:00
sess . Join ( "INNER" , "repo_topic" , "repo_topic.topic_id = topic.id" )
has , err := sess . Get ( & topic )
if has {
return & topic , err
}
return nil , err
}
// AddTopic adds a topic name to a repository (if it does not already have it)
func AddTopic ( repoID int64 , topicName string ) ( * Topic , error ) {
2021-09-19 14:49:59 +03:00
sess := db . DefaultContext ( ) . NewSession ( )
2020-10-24 17:11:30 +03:00
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return nil , err
}
topic , err := getRepoTopicByName ( sess , repoID , topicName )
2019-09-03 18:46:24 +03:00
if err != nil {
return nil , err
}
if topic != nil {
// Repo already have topic
return topic , nil
}
2020-10-24 17:11:30 +03:00
topic , err = addTopicByNameToRepo ( sess , repoID , topicName )
if err != nil {
return nil , err
}
topicNames := make ( [ ] string , 0 , 25 )
if err := sess . Select ( "name" ) . Table ( "topic" ) .
Join ( "INNER" , "repo_topic" , "repo_topic.topic_id = topic.id" ) .
Where ( "repo_topic.repo_id = ?" , repoID ) . Desc ( "topic.repo_count" ) . Find ( & topicNames ) ; err != nil {
return nil , err
}
if _ , err := sess . ID ( repoID ) . Cols ( "topics" ) . Update ( & Repository {
Topics : topicNames ,
} ) ; err != nil {
return nil , err
}
return topic , sess . Commit ( )
2019-09-03 18:46:24 +03:00
}
// DeleteTopic removes a topic name from a repository (if it has it)
func DeleteTopic ( repoID int64 , topicName string ) ( * Topic , error ) {
topic , err := GetRepoTopicByName ( repoID , topicName )
if err != nil {
return nil , err
}
if topic == nil {
// Repo doesn't have topic, can't be removed
return nil , nil
}
2021-09-19 14:49:59 +03:00
err = removeTopicFromRepo ( db . DefaultContext ( ) . Engine ( ) , repoID , topic )
2019-09-03 18:46:24 +03:00
return topic , err
}
2018-04-11 05:51:44 +03:00
// SaveTopics save topics to a repository
func SaveTopics ( repoID int64 , topicNames ... string ) error {
2021-08-12 15:43:08 +03:00
topics , _ , err := FindTopics ( & FindTopicOptions {
2018-04-11 05:51:44 +03:00
RepoID : repoID ,
} )
if err != nil {
return err
}
2021-09-19 14:49:59 +03:00
sess := db . DefaultContext ( ) . NewSession ( )
2018-04-11 05:51:44 +03:00
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return err
}
var addedTopicNames [ ] string
for _ , topicName := range topicNames {
if strings . TrimSpace ( topicName ) == "" {
continue
}
var found bool
for _ , t := range topics {
if strings . EqualFold ( topicName , t . Name ) {
found = true
break
}
}
if ! found {
addedTopicNames = append ( addedTopicNames , topicName )
}
}
var removeTopics [ ] * Topic
for _ , t := range topics {
var found bool
for _ , topicName := range topicNames {
if strings . EqualFold ( topicName , t . Name ) {
found = true
break
}
}
if ! found {
removeTopics = append ( removeTopics , t )
}
}
for _ , topicName := range addedTopicNames {
2019-09-03 18:46:24 +03:00
_ , err := addTopicByNameToRepo ( sess , repoID , topicName )
if err != nil {
2018-04-11 05:51:44 +03:00
return err
}
}
for _ , topic := range removeTopics {
2020-01-31 09:57:19 +03:00
err := removeTopicFromRepo ( sess , repoID , topic )
2019-09-03 18:46:24 +03:00
if err != nil {
2018-04-11 05:51:44 +03:00
return err
}
}
2018-06-27 08:23:10 +03:00
topicNames = make ( [ ] string , 0 , 25 )
2018-06-21 12:09:46 +03:00
if err := sess . Table ( "topic" ) . Cols ( "name" ) .
2018-06-27 08:23:10 +03:00
Join ( "INNER" , "repo_topic" , "repo_topic.topic_id = topic.id" ) .
Where ( "repo_topic.repo_id = ?" , repoID ) . Desc ( "topic.repo_count" ) . Find ( & topicNames ) ; err != nil {
2018-06-21 12:09:46 +03:00
return err
}
2018-04-11 05:51:44 +03:00
if _ , err := sess . ID ( repoID ) . Cols ( "topics" ) . Update ( & Repository {
Topics : topicNames ,
} ) ; err != nil {
return err
}
return sess . Commit ( )
}