feat(repo): support search repository by topic name (#4505)

* feat(repo): support search repository by topic name
tokarchuk/v1.17
Bo-Yi Wu 6 years ago committed by GitHub
parent 7dd93b2441
commit ea20adaa84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      integrations/api_admin_test.go
  2. 2
      integrations/api_issue_test.go
  3. 6
      integrations/api_repo_test.go
  4. 8
      models/fixtures/repo_topic.yml
  5. 22
      models/fixtures/repository.yml
  6. 6
      models/fixtures/topic.yml
  7. 15
      models/fixtures/user.yml
  8. 33
      models/repo_list.go
  9. 31
      models/repo_list_test.go
  10. 8
      models/ssh_key.go
  11. 6
      models/topic_test.go
  12. 4
      models/user_test.go
  13. 1
      routers/api/v1/repo/repo.go
  14. 2
      routers/home.go
  15. 4
      routers/user/profile.go
  16. 2
      templates/explore/repo_list.tmpl
  17. 6
      templates/repo/home.tmpl

@ -9,10 +9,10 @@ import (
"net/http" "net/http"
"testing" "testing"
"github.com/stretchr/testify/assert"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
api "code.gitea.io/sdk/gitea" api "code.gitea.io/sdk/gitea"
"github.com/stretchr/testify/assert"
) )
func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) { func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) {

@ -5,13 +5,13 @@
package integrations package integrations
import ( import (
"fmt"
"net/http" "net/http"
"testing" "testing"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
api "code.gitea.io/sdk/gitea" api "code.gitea.io/sdk/gitea"
"fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

@ -67,9 +67,9 @@ func TestAPISearchRepo(t *testing.T) {
expectedResults expectedResults
}{ }{
{name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50", expectedResults: expectedResults{ {name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50", expectedResults: expectedResults{
nil: {count: 17}, nil: {count: 19},
user: {count: 17}, user: {count: 19},
user2: {count: 17}}, user2: {count: 19}},
}, },
{name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10", expectedResults: expectedResults{ {name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10", expectedResults: expectedResults{
nil: {count: 10}, nil: {count: 10},

@ -9,3 +9,11 @@
- -
repo_id: 1 repo_id: 1
topic_id: 3 topic_id: 3
-
repo_id: 33
topic_id: 1
-
repo_id: 33
topic_id: 4

@ -407,3 +407,25 @@
lower_name: utf8 lower_name: utf8
name: utf8 name: utf8
is_private: false 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 id: 1
name: golang name: golang
repo_count: 1 repo_count: 2
- -
id: 2 id: 2
@ -11,3 +11,7 @@
- id: 3 - id: 3
name: SQL name: SQL
repo_count: 1 repo_count: 1
- id: 4
name: graphql
repo_count: 1

@ -314,3 +314,18 @@
avatar_email: user20@example.com avatar_email: user20@example.com
num_repos: 4 num_repos: 4
is_active: true 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 // True -> include just mirrors
// False -> include just non-mirrors // False -> include just non-mirrors
Mirror util.OptionalBool Mirror util.OptionalBool
// only search topic name
TopicOnly bool
} }
//SearchOrderBy is used to sort the result //SearchOrderBy is used to sort the result
@ -184,7 +186,7 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
if opts.Collaborate != util.OptionalBoolFalse { if opts.Collaborate != util.OptionalBoolFalse {
collaborateCond := builder.And( 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}) builder.Neq{"owner_id": opts.OwnerID})
if !opts.Private { 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)) 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 != "" { 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 { 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") 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. count, err := sess.
Where(cond). Where(cond).
Count(new(Repository)) Count(new(Repository))
if err != nil { if err != nil {
return nil, 0, fmt.Errorf("Count: %v", err) 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") 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) repos := make(RepositoryList, 0, opts.PageSize)
if err = sess. if err = sess.
Where(cond). Where(cond).
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize). Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
OrderBy(opts.OrderBy.String()).
Find(&repos); err != nil { Find(&repos); err != nil {
return nil, 0, fmt.Errorf("Repo: %v", err) return nil, 0, fmt.Errorf("Repo: %v", err)
} }

@ -147,10 +147,10 @@ func TestSearchRepositoryByName(t *testing.T) {
count: 14}, count: 14},
{name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", {name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, AllPublic: true}, opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, AllPublic: true},
count: 17}, count: 19},
{name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", {name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true},
count: 21}, count: 23},
{name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", {name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true},
count: 13}, count: 13},
@ -159,7 +159,7 @@ func TestSearchRepositoryByName(t *testing.T) {
count: 11}, count: 11},
{name: "AllPublic/PublicRepositoriesOfOrganization", {name: "AllPublic/PublicRepositoriesOfOrganization",
opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse}, opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse},
count: 17}, count: 19},
} }
for _, testCase := range testCases { 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" "sync"
"time" "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/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
"golang.org/x/crypto/ssh"
) )
const ( const (

@ -15,7 +15,7 @@ func TestAddTopic(t *testing.T) {
topics, err := FindTopics(&FindTopicOptions{}) topics, err := FindTopics(&FindTopicOptions{})
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 3, len(topics)) assert.EqualValues(t, 4, len(topics))
topics, err = FindTopics(&FindTopicOptions{ topics, err = FindTopics(&FindTopicOptions{
Limit: 2, Limit: 2,
@ -32,7 +32,7 @@ func TestAddTopic(t *testing.T) {
assert.NoError(t, SaveTopics(2, "golang")) assert.NoError(t, SaveTopics(2, "golang"))
topics, err = FindTopics(&FindTopicOptions{}) topics, err = FindTopics(&FindTopicOptions{})
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 3, len(topics)) assert.EqualValues(t, 4, len(topics))
topics, err = FindTopics(&FindTopicOptions{ topics, err = FindTopics(&FindTopicOptions{
RepoID: 2, RepoID: 2,
@ -47,7 +47,7 @@ func TestAddTopic(t *testing.T) {
topics, err = FindTopics(&FindTopicOptions{}) topics, err = FindTopics(&FindTopicOptions{})
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 4, len(topics)) assert.EqualValues(t, 5, len(topics))
topics, err = FindTopics(&FindTopicOptions{ topics, err = FindTopics(&FindTopicOptions{
RepoID: 2, RepoID: 2,

@ -77,13 +77,13 @@ func TestSearchUsers(t *testing.T) {
} }
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1}, 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}, testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse},
[]int64{9}) []int64{9})
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue}, 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}, testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) []int64{1, 10, 11, 12, 13, 14, 15, 16, 18})

@ -91,6 +91,7 @@ func Search(ctx *context.APIContext) {
OwnerID: ctx.QueryInt64("uid"), OwnerID: ctx.QueryInt64("uid"),
Page: ctx.QueryInt("page"), Page: ctx.QueryInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")), PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")),
TopicOnly: ctx.QueryBool("topic"),
Collaborate: util.OptionalBoolNone, Collaborate: util.OptionalBoolNone,
} }

@ -122,6 +122,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
} }
keyword := strings.Trim(ctx.Query("q"), " ") keyword := strings.Trim(ctx.Query("q"), " ")
topicOnly := ctx.QueryBool("topic")
repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{ repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{
Page: page, Page: page,
@ -131,6 +132,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
Keyword: keyword, Keyword: keyword,
OwnerID: opts.OwnerID, OwnerID: opts.OwnerID,
AllPublic: true, AllPublic: true,
TopicOnly: topicOnly,
}) })
if err != nil { if err != nil {
ctx.ServerError("SearchRepositoryByName", err) ctx.ServerError("SearchRepositoryByName", err)

@ -105,6 +105,8 @@ func Profile(ctx *context.Context) {
page = 1 page = 1
} }
topicOnly := ctx.QueryBool("topic")
var ( var (
repos []*models.Repository repos []*models.Repository
count int64 count int64
@ -174,6 +176,7 @@ func Profile(ctx *context.Context) {
PageSize: setting.UI.User.RepoPagingNum, PageSize: setting.UI.User.RepoPagingNum,
Starred: true, Starred: true,
Collaborate: util.OptionalBoolFalse, Collaborate: util.OptionalBoolFalse,
TopicOnly: topicOnly,
}) })
if err != nil { if err != nil {
ctx.ServerError("SearchRepositoryByName", err) ctx.ServerError("SearchRepositoryByName", err)
@ -217,6 +220,7 @@ func Profile(ctx *context.Context) {
IsProfile: true, IsProfile: true,
PageSize: setting.UI.User.RepoPagingNum, PageSize: setting.UI.User.RepoPagingNum,
Collaborate: util.OptionalBoolFalse, Collaborate: util.OptionalBoolFalse,
TopicOnly: topicOnly,
}) })
if err != nil { if err != nil {
ctx.ServerError("SearchRepositoryByName", err) ctx.ServerError("SearchRepositoryByName", err)

@ -20,7 +20,7 @@
{{if .Topics }} {{if .Topics }}
<div> <div>
{{range .Topics}} {{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}} {{end}}
</div> </div>
{{end}} {{end}}

@ -24,8 +24,8 @@
{{end}} {{end}}
</div> </div>
<div class="ui repo-topic" id="repo-topic"> <div class="ui repo-topic" id="repo-topic">
{{range .Topics}}<div class="ui green basic label topic" style="cursor:pointer;">{{.Name}}</div>{{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;">{{.i18n.Tr "repo.topic.manage_topics"}}</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> </div>
{{if .IsRepositoryAdmin}} {{if .IsRepositoryAdmin}}
<div class="ui repo-topic-edit grid form segment error" id="topic_edit" > <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"> <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}}"> <input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if lt (Add $i 1) (len $.Topics)}},{{end}}{{end}}">
{{range .Topics}} {{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}} {{end}}
<div class="text"></div> <div class="text"></div>
</div> </div>

Loading…
Cancel
Save