Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add topic support #3711

Merged
merged 13 commits into from
Apr 11, 2018
11 changes: 11 additions & 0 deletions models/fixtures/repo_topic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-
repo_id: 1
topic_id: 1

-
repo_id: 1
topic_id: 2

-
repo_id: 1
topic_id: 3
13 changes: 13 additions & 0 deletions models/fixtures/topic.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this saved to database?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For better performance when we list all topics for every repository on repositories list UI.


CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
Expand Down
192 changes: 192 additions & 0 deletions models/topic.go
Original file line number Diff line number Diff line change
@@ -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()
}
57 changes: 57 additions & 0 deletions models/topic_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
5 changes: 5 additions & 0 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -516,3 +516,8 @@ type AddTimeManuallyForm struct {
func (f *AddTimeManuallyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}

// SaveTopicForm form for save topics for repository
type SaveTopicForm struct {
Topics []string `binding:"topics;Required;"`
}
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,9 @@ branch.restore_success = %s successfully restored
branch.restore_failed = Failed to restore branch %s.
branch.protected_deletion_failed = It's not possible to delete protected branch %s.

topic.manage_topics = Manage Topics
topic.done = Done

[org]
org_name_holder = Organization Name
org_full_name_holder = Organization Full Name
Expand Down
2 changes: 1 addition & 1 deletion public/css/index.css

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions public/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1591,6 +1591,7 @@ $(document).ready(function () {
initTeamSettings();
initCtrlEnterSubmit();
initNavbarContentToggle();
initTopicbar();

// Repo clone url.
if ($('#repo-clone-url').length > 0) {
Expand Down Expand Up @@ -2122,3 +2123,74 @@ function initNavbarContentToggle() {
}
});
}

function initTopicbar() {
var mgrBtn = $("#manage_topic")
var editDiv = $("#topic_edit")
var viewDiv = $("#repo-topic")
var saveBtn = $("#save_topic")

mgrBtn.click(function() {
viewDiv.hide();
editDiv.show();
})

saveBtn.click(function() {
var topics = $("input[name=topics]").val();

$.post($(this).data('link'), {
"_csrf": csrf,
"topics": topics
}).success(function(res){
if (res["status"] != "ok") {
alert(res.message);
} else {
viewDiv.children(".topic").remove();
var topicArray = topics.split(",");
var last = viewDiv.children("a").last();
for (var i=0;i < topicArray.length; i++) {
$('<div class="ui green basic label topic" style="cursor:pointer;">'+topicArray[i]+'</div>').insertBefore(last)
}
}
}).done(function() {
editDiv.hide();
viewDiv.show();
})
})

$('#topic_edit .dropdown').dropdown({
allowAdditions: true,
fields: { name: "description", value: "data-value" },
saveRemoteData: false,
label: {
transition : 'horizontal flip',
duration : 200,
variation : false,
blue : true,
basic: true,
},
className: {
label: 'ui green basic label'
},
apiSettings: {
url: suburl + '/api/v1/topics/search?q={query}',
throttle: 500,
cache: false,
onResponse: function(res) {
var formattedResponse = {
success: false,
results: new Array(),
};

if (res.topics) {
formattedResponse.success = true;
for (var i=0;i < res.topics.length;i++) {
formattedResponse.results.push({"description": res.topics[i].Name, "data-value":res.topics[i].Name})
}
}

return formattedResponse;
},
},
});
}
Loading