diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2018-04-11 10:51:44 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-04-11 10:51:44 +0800 |
commit | bec69f702ba8ecdc9a77db34ff94b7e55879be59 (patch) | |
tree | f6791e83ddbab715dc2fc9c8e3d004faddc92f6d /models | |
parent | 1946ce2954d49474b750938a1a6bb541f081485f (diff) | |
download | gitea-bec69f702ba8ecdc9a77db34ff94b7e55879be59.tar.gz gitea-bec69f702ba8ecdc9a77db34ff94b7e55879be59.zip |
Add topic support (#3711)
* add topic models and unit tests
* fix comments
* fix comment
* add the UI to show or add topics for a repo
* show topics on repositories list
* fix test
* don't show manage topics link when no permission
* use green basic as topic label
* fix topic label color
* remove trace content
* remove debug function
Diffstat (limited to 'models')
-rw-r--r-- | models/fixtures/repo_topic.yml | 11 | ||||
-rw-r--r-- | models/fixtures/topic.yml | 13 | ||||
-rw-r--r-- | models/repo.go | 1 | ||||
-rw-r--r-- | models/topic.go | 192 | ||||
-rw-r--r-- | models/topic_test.go | 57 |
5 files changed, 274 insertions, 0 deletions
diff --git a/models/fixtures/repo_topic.yml b/models/fixtures/repo_topic.yml new file mode 100644 index 0000000000..58937031cd --- /dev/null +++ b/models/fixtures/repo_topic.yml @@ -0,0 +1,11 @@ +- + repo_id: 1 + topic_id: 1 + +- + repo_id: 1 + topic_id: 2 + +- + repo_id: 1 + topic_id: 3 diff --git a/models/fixtures/topic.yml b/models/fixtures/topic.yml new file mode 100644 index 0000000000..b6b94ff4d9 --- /dev/null +++ b/models/fixtures/topic.yml @@ -0,0 +1,13 @@ +- + id: 1 + name: golang + repo_count: 1 + +- + id: 2 + name: database + repo_count: 1 + +- id: 3 + name: SQL + repo_count: 1 diff --git a/models/repo.go b/models/repo.go index 300bdbe875..a7e8bd2a52 100644 --- a/models/repo.go +++ b/models/repo.go @@ -199,6 +199,7 @@ type Repository struct { Size int64 `xorm:"NOT NULL DEFAULT 0"` IndexerStatus *RepoIndexerStatus `xorm:"-"` IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"` + Topics []string `xorm:"TEXT JSON"` CreatedUnix util.TimeStamp `xorm:"INDEX created"` UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` diff --git a/models/topic.go b/models/topic.go new file mode 100644 index 0000000000..3b1737f8af --- /dev/null +++ b/models/topic.go @@ -0,0 +1,192 @@ +// 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" + "strings" + + "code.gitea.io/gitea/modules/util" + + "github.com/go-xorm/builder" +) + +func init() { + tables = append(tables, + new(Topic), + new(RepoTopic), + ) +} + +// Topic represents a topic of repositories +type Topic struct { + ID int64 + Name string `xorm:"unique"` + RepoCount int + CreatedUnix util.TimeStamp `xorm:"INDEX created"` + UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` +} + +// RepoTopic represents associated repositories and topics +type RepoTopic struct { + RepoID int64 `xorm:"unique(s)"` + TopicID int64 `xorm:"unique(s)"` +} + +// 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) +} + +// GetTopicByName retrieves topic by name +func GetTopicByName(name string) (*Topic, error) { + var topic Topic + if has, err := x.Where("name = ?", name).Get(&topic); err != nil { + return nil, err + } else if !has { + return nil, ErrTopicNotExist{name} + } + return &topic, nil +} + +// FindTopicOptions represents the options when fdin topics +type FindTopicOptions struct { + RepoID int64 + Keyword string + Limit int + Page int +} + +func (opts *FindTopicOptions) toConds() builder.Cond { + var cond = builder.NewCond() + 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 +func FindTopics(opts *FindTopicOptions) (topics []*Topic, err error) { + sess := x.Select("topic.*").Where(opts.toConds()) + if opts.RepoID > 0 { + sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") + } + if opts.Limit > 0 { + sess.Limit(opts.Limit, opts.Page*opts.Limit) + } + return topics, sess.Desc("topic.repo_count").Find(&topics) +} + +// SaveTopics save topics to a repository +func SaveTopics(repoID int64, topicNames ...string) error { + topics, err := FindTopics(&FindTopicOptions{ + RepoID: repoID, + }) + if err != nil { + return err + } + + sess := x.NewSession() + 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 { + var topic Topic + if has, err := sess.Where("name = ?", topicName).Get(&topic); err != nil { + return err + } else if !has { + topic.Name = topicName + topic.RepoCount = 1 + if _, err := sess.Insert(&topic); err != nil { + return err + } + } else { + topic.RepoCount++ + if _, err := sess.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil { + return err + } + } + + if _, err := sess.Insert(&RepoTopic{ + RepoID: repoID, + TopicID: topic.ID, + }); err != nil { + return err + } + } + + for _, topic := range removeTopics { + topic.RepoCount-- + if _, err := sess.ID(topic.ID).Cols("repo_count").Update(topic); err != nil { + return err + } + + if _, err := sess.Delete(&RepoTopic{ + RepoID: repoID, + TopicID: topic.ID, + }); err != nil { + return err + } + } + + if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{ + Topics: topicNames, + }); err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/topic_test.go b/models/topic_test.go new file mode 100644 index 0000000000..472f4e52d9 --- /dev/null +++ b/models/topic_test.go @@ -0,0 +1,57 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddTopic(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + topics, err := FindTopics(&FindTopicOptions{}) + assert.NoError(t, err) + assert.EqualValues(t, 3, len(topics)) + + topics, err = FindTopics(&FindTopicOptions{ + Limit: 2, + }) + assert.NoError(t, err) + assert.EqualValues(t, 2, len(topics)) + + topics, err = FindTopics(&FindTopicOptions{ + RepoID: 1, + }) + assert.NoError(t, err) + assert.EqualValues(t, 3, len(topics)) + + assert.NoError(t, SaveTopics(2, "golang")) + topics, err = FindTopics(&FindTopicOptions{}) + assert.NoError(t, err) + assert.EqualValues(t, 3, len(topics)) + + topics, err = FindTopics(&FindTopicOptions{ + RepoID: 2, + }) + assert.NoError(t, err) + assert.EqualValues(t, 1, len(topics)) + + assert.NoError(t, SaveTopics(2, "golang", "gitea")) + topic, err := GetTopicByName("gitea") + assert.NoError(t, err) + assert.EqualValues(t, 1, topic.RepoCount) + + topics, err = FindTopics(&FindTopicOptions{}) + assert.NoError(t, err) + assert.EqualValues(t, 4, len(topics)) + + topics, err = FindTopics(&FindTopicOptions{ + RepoID: 2, + }) + assert.NoError(t, err) + assert.EqualValues(t, 2, len(topics)) +} |