* feat(repo): support search repository by topic nametags/v1.6.0-dev
"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) { |
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" | ||||
) | ) | ||||
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}, | |||||
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{ | {name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10", expectedResults: expectedResults{ | ||||
nil: {count: 10}, | nil: {count: 10}, |
- | - | ||||
repo_id: 1 | repo_id: 1 | ||||
topic_id: 3 | topic_id: 3 | ||||
- | |||||
repo_id: 33 | |||||
topic_id: 1 | |||||
- | |||||
repo_id: 33 | |||||
topic_id: 4 |
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 |
- | - | ||||
id: 1 | id: 1 | ||||
name: golang | name: golang | ||||
repo_count: 1 | |||||
repo_count: 2 | |||||
- | - | ||||
id: 2 | id: 2 | ||||
- id: 3 | - id: 3 | ||||
name: SQL | name: SQL | ||||
repo_count: 1 | repo_count: 1 | ||||
- id: 4 | |||||
name: graphql | |||||
repo_count: 1 |
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 |
// 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 | ||||
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)) | ||||
} | } | ||||
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 { | ||||
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) | ||||
} | } | ||||
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) | ||||
} | } |
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}, | ||||
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 { | ||||
}) | }) | ||||
} | } | ||||
} | } | ||||
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) | |||||
}) | |||||
} | |||||
} |
"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 ( |
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, | ||||
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, | ||||
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, |
} | } | ||||
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}) |
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, | ||||
} | } | ||||
} | } | ||||
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, | ||||
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) |
page = 1 | page = 1 | ||||
} | } | ||||
topicOnly := ctx.QueryBool("topic") | |||||
var ( | var ( | ||||
repos []*models.Repository | repos []*models.Repository | ||||
count int64 | count int64 | ||||
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) | ||||
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) |
{{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}} |
{{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}} | |||||
{{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> | </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" > | ||||
<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> |