2018-04-11 05:51:44 +03:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2018-04-11 05:51:44 +03:00
2021-12-12 18:48:20 +03:00
package repo
2018-04-11 05:51:44 +03:00
import (
2021-12-12 18:48:20 +03:00
"context"
2018-04-11 05:51:44 +03:00
"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"
2022-10-12 08:18:26 +03:00
"code.gitea.io/gitea/modules/container"
2019-08-15 17:46:21 +03:00
"code.gitea.io/gitea/modules/timeutil"
2022-10-18 08:50:37 +03:00
"code.gitea.io/gitea/modules/util"
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
}
2023-08-03 12:18:06 +03:00
var topicPattern = regexp . MustCompile ( ` ^[a-z0-9][-.a-z0-9]*$ ` )
2018-06-21 12:09:46 +03:00
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
2021-12-12 18:48:20 +03:00
type RepoTopic struct { //revive:disable-line:exported
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 )
}
2022-10-18 08:50:37 +03:00
func ( err ErrTopicNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
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 )
2022-10-12 08:18:26 +03:00
mValidTopics := make ( container . Set [ string ] )
2019-09-03 18:46:24 +03:00
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
2022-10-12 08:18:26 +03:00
if mValidTopics . Contains ( topic ) {
2019-09-03 18:46:24 +03:00
continue
}
if ValidateTopic ( topic ) {
validTopics = append ( validTopics , topic )
2022-10-12 08:18:26 +03:00
mValidTopics . Add ( topic )
2019-09-03 18:46:24 +03:00
} 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-23 18:45:36 +03:00
if has , err := db . GetEngine ( db . DefaultContext ) . 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
2022-05-20 17:08:52 +03:00
func addTopicByNameToRepo ( ctx context . Context , repoID int64 , topicName string ) ( * Topic , error ) {
2019-09-03 18:46:24 +03:00
var topic Topic
2022-05-20 17:08:52 +03:00
e := db . GetEngine ( ctx )
2019-09-03 18:46:24 +03:00
has , err := e . Where ( "name = ?" , topicName ) . Get ( & topic )
if err != nil {
return nil , err
}
if ! has {
topic . Name = topicName
topic . RepoCount = 1
2022-05-20 17:08:52 +03:00
if err := db . Insert ( ctx , & topic ) ; err != nil {
2019-09-03 18:46:24 +03:00
return nil , err
}
} else {
topic . RepoCount ++
if _ , err := e . ID ( topic . ID ) . Cols ( "repo_count" ) . Update ( & topic ) ; err != nil {
return nil , err
}
}
2022-05-20 17:08:52 +03:00
if err := db . Insert ( ctx , & RepoTopic {
2019-09-03 18:46:24 +03:00
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
2022-05-20 17:08:52 +03:00
func removeTopicFromRepo ( ctx context . Context , repoID int64 , topic * Topic ) error {
2019-09-03 18:46:24 +03:00
topic . RepoCount --
2022-05-20 17:08:52 +03:00
e := db . GetEngine ( ctx )
2019-09-03 18:46:24 +03:00
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
}
2021-12-12 18:48:20 +03:00
// RemoveTopicsFromRepo remove all topics from the repo and decrements respective topics repo count
func RemoveTopicsFromRepo ( ctx context . Context , repoID int64 ) error {
e := db . GetEngine ( ctx )
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 {
2021-09-24 14:32:56 +03:00
db . 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-23 18:45:36 +03:00
sess := db . GetEngine ( db . DefaultContext ) . Select ( "topic.*" ) . Where ( opts . toConds ( ) )
2023-04-14 22:29:05 +03:00
orderBy := "topic.repo_count DESC"
2018-04-11 05:51:44 +03:00
if opts . RepoID > 0 {
sess . Join ( "INNER" , "repo_topic" , "repo_topic.topic_id = topic.id" )
2023-04-14 22:29:05 +03:00
orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result
2018-04-11 05:51:44 +03:00
}
2020-01-24 22:00:29 +03:00
if opts . PageSize != 0 && opts . Page != 0 {
2021-09-24 14:32:56 +03:00
sess = db . SetSessionPagination ( sess , opts )
2018-04-11 05:51:44 +03:00
}
2021-08-12 15:43:08 +03:00
topics := make ( [ ] * Topic , 0 , 10 )
2023-04-14 22:29:05 +03:00
total , err := sess . OrderBy ( orderBy ) . FindAndCount ( & topics )
2021-08-12 15:43:08 +03:00
return topics , total , err
}
// CountTopics counts the number of topics matching the FindTopicOptions
func CountTopics ( opts * FindTopicOptions ) ( int64 , error ) {
2021-09-23 18:45:36 +03:00
sess := db . GetEngine ( db . DefaultContext ) . 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
2022-05-20 17:08:52 +03:00
func GetRepoTopicByName ( ctx context . Context , 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 } )
2022-05-20 17:08:52 +03:00
sess := db . GetEngine ( ctx ) . Table ( "topic" ) . Where ( cond )
2019-09-03 18:46:24 +03:00
sess . Join ( "INNER" , "repo_topic" , "repo_topic.topic_id = topic.id" )
2022-05-20 17:08:52 +03:00
has , err := sess . Select ( "topic.*" ) . Get ( & topic )
2019-09-03 18:46:24 +03:00
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 ) {
2022-11-12 23:18:50 +03:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-11-21 18:41:00 +03:00
if err != nil {
2020-10-24 17:11:30 +03:00
return nil , err
}
2021-11-21 18:41:00 +03:00
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-10-24 17:11:30 +03:00
2022-05-20 17:08:52 +03:00
topic , err := GetRepoTopicByName ( ctx , 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
}
2022-05-20 17:08:52 +03:00
topic , err = addTopicByNameToRepo ( ctx , repoID , topicName )
2020-10-24 17:11:30 +03:00
if err != nil {
return nil , err
}
2023-05-21 12:03:20 +03:00
if err = syncTopicsInRepository ( sess , repoID ) ; err != nil {
2020-10-24 17:11:30 +03:00
return nil , err
}
2021-11-21 18:41:00 +03:00
return topic , committer . 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 ) {
2022-05-20 17:08:52 +03:00
topic , err := GetRepoTopicByName ( db . DefaultContext , repoID , topicName )
2019-09-03 18:46:24 +03:00
if err != nil {
return nil , err
}
if topic == nil {
// Repo doesn't have topic, can't be removed
return nil , nil
}
2022-05-20 17:08:52 +03:00
err = removeTopicFromRepo ( db . DefaultContext , repoID , topic )
2023-05-21 12:03:20 +03:00
if err != nil {
return nil , err
}
err = syncTopicsInRepository ( db . GetEngine ( db . DefaultContext ) , repoID )
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
}
2022-11-12 23:18:50 +03:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-11-21 18:41:00 +03:00
if err != nil {
2018-04-11 05:51:44 +03:00
return err
}
2021-11-21 18:41:00 +03:00
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2018-04-11 05:51:44 +03:00
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 {
2022-05-20 17:08:52 +03:00
_ , err := addTopicByNameToRepo ( ctx , repoID , topicName )
2019-09-03 18:46:24 +03:00
if err != nil {
2018-04-11 05:51:44 +03:00
return err
}
}
for _ , topic := range removeTopics {
2022-05-20 17:08:52 +03:00
err := removeTopicFromRepo ( ctx , repoID , topic )
2019-09-03 18:46:24 +03:00
if err != nil {
2018-04-11 05:51:44 +03:00
return err
}
}
2023-05-21 12:03:20 +03:00
if err := syncTopicsInRepository ( sess , repoID ) ; err != nil {
2018-04-11 05:51:44 +03:00
return err
}
2021-11-21 18:41:00 +03:00
return committer . Commit ( )
2018-04-11 05:51:44 +03:00
}
2021-12-12 18:48:20 +03:00
// GenerateTopics generates topics from a template repository
func GenerateTopics ( ctx context . Context , templateRepo , generateRepo * Repository ) error {
for _ , topic := range templateRepo . Topics {
2022-05-20 17:08:52 +03:00
if _ , err := addTopicByNameToRepo ( ctx , generateRepo . ID , topic ) ; err != nil {
2021-12-12 18:48:20 +03:00
return err
}
}
2023-05-21 12:03:20 +03:00
return syncTopicsInRepository ( db . GetEngine ( ctx ) , generateRepo . ID )
}
// syncTopicsInRepository makes sure topics in the topics table are copied into the topics field of the repository
func syncTopicsInRepository ( sess db . Engine , repoID int64 ) error {
topicNames := make ( [ ] string , 0 , 25 )
if err := sess . Table ( "topic" ) . Cols ( "name" ) .
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 err
}
if _ , err := sess . ID ( repoID ) . Cols ( "topics" ) . Update ( & Repository {
Topics : topicNames ,
} ) ; err != nil {
return err
}
2021-12-12 18:48:20 +03:00
return nil
}