* feat(repo): support search repository by topic nametags/v1.6.0-dev
@@ -9,10 +9,10 @@ import ( | |||
"net/http" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"code.gitea.io/gitea/models" | |||
api "code.gitea.io/sdk/gitea" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) { |
@@ -5,13 +5,13 @@ | |||
package integrations | |||
import ( | |||
"fmt" | |||
"net/http" | |||
"testing" | |||
"code.gitea.io/gitea/models" | |||
api "code.gitea.io/sdk/gitea" | |||
"fmt" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
@@ -67,9 +67,9 @@ func TestAPISearchRepo(t *testing.T) { | |||
expectedResults | |||
}{ | |||
{name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50", expectedResults: expectedResults{ | |||
nil: {count: 17}, | |||
user: {count: 17}, | |||
user2: {count: 17}}, | |||
nil: {count: 19}, | |||
user: {count: 19}, | |||
user2: {count: 19}}, | |||
}, | |||
{name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10", expectedResults: expectedResults{ | |||
nil: {count: 10}, |
@@ -9,3 +9,11 @@ | |||
- | |||
repo_id: 1 | |||
topic_id: 3 | |||
- | |||
repo_id: 33 | |||
topic_id: 1 | |||
- | |||
repo_id: 33 | |||
topic_id: 4 |
@@ -407,3 +407,25 @@ | |||
lower_name: utf8 | |||
name: utf8 | |||
is_private: false | |||
- | |||
id: 34 | |||
owner_id: 21 | |||
lower_name: golang | |||
name: golang | |||
is_private: false | |||
num_stars: 0 | |||
num_forks: 0 | |||
num_issues: 0 | |||
is_mirror: false | |||
- | |||
id: 35 | |||
owner_id: 21 | |||
lower_name: graphql | |||
name: graphql | |||
is_private: false | |||
num_stars: 0 | |||
num_forks: 0 | |||
num_issues: 0 | |||
is_mirror: false |
@@ -1,7 +1,7 @@ | |||
- | |||
id: 1 | |||
name: golang | |||
repo_count: 1 | |||
repo_count: 2 | |||
- | |||
id: 2 | |||
@@ -11,3 +11,7 @@ | |||
- id: 3 | |||
name: SQL | |||
repo_count: 1 | |||
- id: 4 | |||
name: graphql | |||
repo_count: 1 |
@@ -314,3 +314,18 @@ | |||
avatar_email: user20@example.com | |||
num_repos: 4 | |||
is_active: true | |||
- | |||
id: 21 | |||
lower_name: user21 | |||
name: user21 | |||
full_name: User 21 | |||
email: user21@example.com | |||
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password | |||
type: 0 # individual | |||
salt: ZogKvWdyEx | |||
is_admin: false | |||
avatar: avatar21 | |||
avatar_email: user21@example.com | |||
num_repos: 2 | |||
is_active: true |
@@ -131,6 +131,8 @@ type SearchRepoOptions struct { | |||
// True -> include just mirrors | |||
// False -> include just non-mirrors | |||
Mirror util.OptionalBool | |||
// only search topic name | |||
TopicOnly bool | |||
} | |||
//SearchOrderBy is used to sort the result | |||
@@ -184,7 +186,7 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err | |||
if opts.Collaborate != util.OptionalBoolFalse { | |||
collaborateCond := builder.And( | |||
builder.Expr("id IN (SELECT repo_id FROM `access` WHERE access.user_id = ?)", opts.OwnerID), | |||
builder.Expr("repository.id IN (SELECT repo_id FROM `access` WHERE access.user_id = ?)", opts.OwnerID), | |||
builder.Neq{"owner_id": opts.OwnerID}) | |||
if !opts.Private { | |||
collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false)) | |||
@@ -202,7 +204,14 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err | |||
} | |||
if opts.Keyword != "" { | |||
cond = cond.And(builder.Like{"lower_name", strings.ToLower(opts.Keyword)}) | |||
var keywordCond = builder.NewCond() | |||
if opts.TopicOnly { | |||
keywordCond = keywordCond.Or(builder.Like{"topic.name", strings.ToLower(opts.Keyword)}) | |||
} else { | |||
keywordCond = keywordCond.Or(builder.Like{"lower_name", strings.ToLower(opts.Keyword)}) | |||
keywordCond = keywordCond.Or(builder.Like{"topic.name", strings.ToLower(opts.Keyword)}) | |||
} | |||
cond = cond.And(keywordCond) | |||
} | |||
if opts.Fork != util.OptionalBoolNone { | |||
@@ -224,9 +233,15 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err | |||
sess.Join("INNER", "star", "star.repo_id = repository.id") | |||
} | |||
if opts.Keyword != "" { | |||
sess.Join("LEFT", "repo_topic", "repo_topic.repo_id = repository.id") | |||
sess.Join("LEFT", "topic", "repo_topic.topic_id = topic.id") | |||
} | |||
count, err := sess. | |||
Where(cond). | |||
Count(new(Repository)) | |||
if err != nil { | |||
return nil, 0, fmt.Errorf("Count: %v", err) | |||
} | |||
@@ -236,11 +251,23 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err | |||
sess.Join("INNER", "star", "star.repo_id = repository.id") | |||
} | |||
if opts.Keyword != "" { | |||
sess.Join("LEFT", "repo_topic", "repo_topic.repo_id = repository.id") | |||
sess.Join("LEFT", "topic", "repo_topic.topic_id = topic.id") | |||
} | |||
if opts.Keyword != "" { | |||
sess.Select("repository.*") | |||
sess.GroupBy("repository.id") | |||
sess.OrderBy("repository." + opts.OrderBy.String()) | |||
} else { | |||
sess.OrderBy(opts.OrderBy.String()) | |||
} | |||
repos := make(RepositoryList, 0, opts.PageSize) | |||
if err = sess. | |||
Where(cond). | |||
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize). | |||
OrderBy(opts.OrderBy.String()). | |||
Find(&repos); err != nil { | |||
return nil, 0, fmt.Errorf("Repo: %v", err) | |||
} |
@@ -147,10 +147,10 @@ func TestSearchRepositoryByName(t *testing.T) { | |||
count: 14}, | |||
{name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", | |||
opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, AllPublic: true}, | |||
count: 17}, | |||
count: 19}, | |||
{name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", | |||
opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, | |||
count: 21}, | |||
count: 23}, | |||
{name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", | |||
opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, | |||
count: 13}, | |||
@@ -159,7 +159,7 @@ func TestSearchRepositoryByName(t *testing.T) { | |||
count: 11}, | |||
{name: "AllPublic/PublicRepositoriesOfOrganization", | |||
opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse}, | |||
count: 17}, | |||
count: 19}, | |||
} | |||
for _, testCase := range testCases { | |||
@@ -222,3 +222,28 @@ func TestSearchRepositoryByName(t *testing.T) { | |||
}) | |||
} | |||
} | |||
func TestSearchRepositoryByTopicName(t *testing.T) { | |||
assert.NoError(t, PrepareTestDatabase()) | |||
testCases := []struct { | |||
name string | |||
opts *SearchRepoOptions | |||
count int | |||
}{ | |||
{name: "AllPublic/SearchPublicRepositoriesFromTopicAndName", | |||
opts: &SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"}, | |||
count: 2}, | |||
{name: "AllPublic/OnlySearchPublicRepositoriesFromTopic", | |||
opts: &SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true}, | |||
count: 1}, | |||
} | |||
for _, testCase := range testCases { | |||
t.Run(testCase.name, func(t *testing.T) { | |||
_, count, err := SearchRepositoryByName(testCase.opts) | |||
assert.NoError(t, err) | |||
assert.Equal(t, int64(testCase.count), count) | |||
}) | |||
} | |||
} |
@@ -18,14 +18,14 @@ import ( | |||
"sync" | |||
"time" | |||
"github.com/Unknwon/com" | |||
"github.com/go-xorm/xorm" | |||
"golang.org/x/crypto/ssh" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/process" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/util" | |||
"github.com/Unknwon/com" | |||
"github.com/go-xorm/xorm" | |||
"golang.org/x/crypto/ssh" | |||
) | |||
const ( |
@@ -15,7 +15,7 @@ func TestAddTopic(t *testing.T) { | |||
topics, err := FindTopics(&FindTopicOptions{}) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, 3, len(topics)) | |||
assert.EqualValues(t, 4, len(topics)) | |||
topics, err = FindTopics(&FindTopicOptions{ | |||
Limit: 2, | |||
@@ -32,7 +32,7 @@ func TestAddTopic(t *testing.T) { | |||
assert.NoError(t, SaveTopics(2, "golang")) | |||
topics, err = FindTopics(&FindTopicOptions{}) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, 3, len(topics)) | |||
assert.EqualValues(t, 4, len(topics)) | |||
topics, err = FindTopics(&FindTopicOptions{ | |||
RepoID: 2, | |||
@@ -47,7 +47,7 @@ func TestAddTopic(t *testing.T) { | |||
topics, err = FindTopics(&FindTopicOptions{}) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, 4, len(topics)) | |||
assert.EqualValues(t, 5, len(topics)) | |||
topics, err = FindTopics(&FindTopicOptions{ | |||
RepoID: 2, |
@@ -77,13 +77,13 @@ func TestSearchUsers(t *testing.T) { | |||
} | |||
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1}, | |||
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20}) | |||
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21}) | |||
testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse}, | |||
[]int64{9}) | |||
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue}, | |||
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20}) | |||
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21}) | |||
testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue}, | |||
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) |
@@ -91,6 +91,7 @@ func Search(ctx *context.APIContext) { | |||
OwnerID: ctx.QueryInt64("uid"), | |||
Page: ctx.QueryInt("page"), | |||
PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")), | |||
TopicOnly: ctx.QueryBool("topic"), | |||
Collaborate: util.OptionalBoolNone, | |||
} | |||
@@ -122,6 +122,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { | |||
} | |||
keyword := strings.Trim(ctx.Query("q"), " ") | |||
topicOnly := ctx.QueryBool("topic") | |||
repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{ | |||
Page: page, | |||
@@ -131,6 +132,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { | |||
Keyword: keyword, | |||
OwnerID: opts.OwnerID, | |||
AllPublic: true, | |||
TopicOnly: topicOnly, | |||
}) | |||
if err != nil { | |||
ctx.ServerError("SearchRepositoryByName", err) |
@@ -105,6 +105,8 @@ func Profile(ctx *context.Context) { | |||
page = 1 | |||
} | |||
topicOnly := ctx.QueryBool("topic") | |||
var ( | |||
repos []*models.Repository | |||
count int64 | |||
@@ -174,6 +176,7 @@ func Profile(ctx *context.Context) { | |||
PageSize: setting.UI.User.RepoPagingNum, | |||
Starred: true, | |||
Collaborate: util.OptionalBoolFalse, | |||
TopicOnly: topicOnly, | |||
}) | |||
if err != nil { | |||
ctx.ServerError("SearchRepositoryByName", err) | |||
@@ -217,6 +220,7 @@ func Profile(ctx *context.Context) { | |||
IsProfile: true, | |||
PageSize: setting.UI.User.RepoPagingNum, | |||
Collaborate: util.OptionalBoolFalse, | |||
TopicOnly: topicOnly, | |||
}) | |||
if err != nil { | |||
ctx.ServerError("SearchRepositoryByName", err) |
@@ -20,7 +20,7 @@ | |||
{{if .Topics }} | |||
<div> | |||
{{range .Topics}} | |||
{{if ne . "" }}<div class="ui green basic label topic">{{.}}</div>{{end}} | |||
{{if ne . "" }}<a href="/explore/repos?q={{.}}&topic=1"><div class="ui green basic label topic">{{.}}</div></a>{{end}} | |||
{{end}} | |||
</div> | |||
{{end}} |
@@ -24,8 +24,8 @@ | |||
{{end}} | |||
</div> | |||
<div class="ui repo-topic" id="repo-topic"> | |||
{{range .Topics}}<div class="ui green basic label topic" style="cursor:pointer;">{{.Name}}</div>{{end}} | |||
{{if .IsRepositoryAdmin}}<a id="manage_topic" style="cursor:pointer;margin-left:10px;">{{.i18n.Tr "repo.topic.manage_topics"}}</a>{{end}} | |||
{{range .Topics}}<a class="ui green basic label topic" style="cursor:pointer;" href="/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}} | |||
{{if .IsRepositoryAdmin}}<a id="manage_topic" style="cursor:pointer;margin-left:10px;" href="/explore/repos?q={{.Name}}&topic=1">{{.i18n.Tr "repo.topic.manage_topics"}}</a>{{end}} | |||
</div> | |||
{{if .IsRepositoryAdmin}} | |||
<div class="ui repo-topic-edit grid form segment error" id="topic_edit" > | |||
@@ -34,7 +34,7 @@ | |||
<div class="ui fluid multiple search selection dropdown"> | |||
<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if lt (Add $i 1) (len $.Topics)}},{{end}}{{end}}"> | |||
{{range .Topics}} | |||
<a class="ui green basic label topic transition visible" data-value="{{.Name}}" style="display: inline-block !important;">{{.Name}}<i class="delete icon"></i></a> | |||
<a class="ui green basic label topic transition visible" data-value="{{.Name}}" style="display: inline-block !important;" href="/explore/repos?q={{.Name}}&topic=1">{{.Name}}<i class="delete icon"></i></a> | |||
{{end}} | |||
<div class="text"></div> | |||
</div> |