Include description in repository search. (#7942)
* Add description in repository search. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Refactor SearchRepositoryByName with a general function SearchRepository Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Allow to specify if description shall be included in API repo search. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Add new app.ini setting for whether to search within repo description. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Search keyword in description (if setting enabled) on: - Explore page - Organization profile page - User profile page - Admin repo page Do not search keyword in description on: - Any non-keyword search (not relevant) - Incremental search (uses API) Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Put parameters related to keyword directly after it Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Add test cases for including (and not including) repository description in search. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Rename test function from TestSearchRepositoryByName to TestSearchRepository. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Make setting SEARCH_REPO_DESCRIPTION default to true Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
This commit is contained in:
parent
8c24bb9e43
commit
c9546d4cdd
11 changed files with 134 additions and 73 deletions
|
@ -116,6 +116,8 @@ DEFAULT_THEME = gitea
|
||||||
THEMES = gitea,arc-green
|
THEMES = gitea,arc-green
|
||||||
; Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
|
; Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
|
||||||
DEFAULT_SHOW_FULL_NAME = false
|
DEFAULT_SHOW_FULL_NAME = false
|
||||||
|
; Whether to search within description at repository search on explore page.
|
||||||
|
SEARCH_REPO_DESCRIPTION = true
|
||||||
|
|
||||||
[ui.admin]
|
[ui.admin]
|
||||||
; Number of users that are displayed on one page
|
; Number of users that are displayed on one page
|
||||||
|
|
|
@ -96,6 +96,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
- `THEMES`: **gitea,arc-green**: All available themes. Allow users select personalized themes
|
- `THEMES`: **gitea,arc-green**: All available themes. Allow users select personalized themes
|
||||||
regardless of the value of `DEFAULT_THEME`.
|
regardless of the value of `DEFAULT_THEME`.
|
||||||
- `DEFAULT_SHOW_FULL_NAME`: false: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
|
- `DEFAULT_SHOW_FULL_NAME`: false: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
|
||||||
|
- `SEARCH_REPO_DESCRIPTION`: true: Whether to search within description at repository search on explore page.
|
||||||
|
|
||||||
### UI - Admin (`ui.admin`)
|
### UI - Admin (`ui.admin`)
|
||||||
|
|
||||||
|
|
|
@ -165,6 +165,7 @@
|
||||||
owner_id: 14
|
owner_id: 14
|
||||||
lower_name: test_repo_14
|
lower_name: test_repo_14
|
||||||
name: test_repo_14
|
name: test_repo_14
|
||||||
|
description: test_description_14
|
||||||
is_private: false
|
is_private: false
|
||||||
num_issues: 0
|
num_issues: 0
|
||||||
num_closed_issues: 0
|
num_closed_issues: 0
|
||||||
|
|
|
@ -136,6 +136,8 @@ type SearchRepoOptions struct {
|
||||||
Mirror util.OptionalBool
|
Mirror util.OptionalBool
|
||||||
// only search topic name
|
// only search topic name
|
||||||
TopicOnly bool
|
TopicOnly bool
|
||||||
|
// include description in keyword search
|
||||||
|
IncludeDescription bool
|
||||||
}
|
}
|
||||||
|
|
||||||
//SearchOrderBy is used to sort the result
|
//SearchOrderBy is used to sort the result
|
||||||
|
@ -163,9 +165,9 @@ const (
|
||||||
SearchOrderByForksReverse SearchOrderBy = "num_forks DESC"
|
SearchOrderByForksReverse SearchOrderBy = "num_forks DESC"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SearchRepositoryByName takes keyword and part of repository name to search,
|
// SearchRepository returns repositories based on search options,
|
||||||
// it returns results in given range and number of total results.
|
// it returns results in given range and number of total results.
|
||||||
func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, error) {
|
func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) {
|
||||||
if opts.Page <= 0 {
|
if opts.Page <= 0 {
|
||||||
opts.Page = 1
|
opts.Page = 1
|
||||||
}
|
}
|
||||||
|
@ -264,6 +266,9 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
|
||||||
var likes = builder.NewCond()
|
var likes = builder.NewCond()
|
||||||
for _, v := range strings.Split(opts.Keyword, ",") {
|
for _, v := range strings.Split(opts.Keyword, ",") {
|
||||||
likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)})
|
likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)})
|
||||||
|
if opts.IncludeDescription {
|
||||||
|
likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
keywordCond = keywordCond.Or(likes)
|
keywordCond = keywordCond.Or(likes)
|
||||||
}
|
}
|
||||||
|
@ -311,6 +316,13 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
|
||||||
return repos, count, nil
|
return repos, count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SearchRepositoryByName takes keyword and part of repository name to search,
|
||||||
|
// it returns results in given range and number of total results.
|
||||||
|
func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, error) {
|
||||||
|
opts.IncludeDescription = false
|
||||||
|
return SearchRepository(opts)
|
||||||
|
}
|
||||||
|
|
||||||
// FindUserAccessibleRepoIDs find all accessible repositories' ID by user's id
|
// FindUserAccessibleRepoIDs find all accessible repositories' ID by user's id
|
||||||
func FindUserAccessibleRepoIDs(userID int64) ([]int64, error) {
|
func FindUserAccessibleRepoIDs(userID int64) ([]int64, error) {
|
||||||
var accessCond builder.Cond = builder.Eq{"is_private": false}
|
var accessCond builder.Cond = builder.Eq{"is_private": false}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSearchRepositoryByName(t *testing.T) {
|
func TestSearchRepository(t *testing.T) {
|
||||||
assert.NoError(t, PrepareTestDatabase())
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
// test search public repository on explore page
|
// test search public repository on explore page
|
||||||
|
@ -74,6 +74,34 @@ func TestSearchRepositoryByName(t *testing.T) {
|
||||||
assert.Empty(t, repos)
|
assert.Empty(t, repos)
|
||||||
assert.Equal(t, int64(0), count)
|
assert.Equal(t, int64(0), count)
|
||||||
|
|
||||||
|
// Test search within description
|
||||||
|
repos, count, err = SearchRepository(&SearchRepoOptions{
|
||||||
|
Keyword: "description_14",
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
Collaborate: util.OptionalBoolFalse,
|
||||||
|
IncludeDescription: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.Len(t, repos, 1) {
|
||||||
|
assert.Equal(t, "test_repo_14", repos[0].Name)
|
||||||
|
}
|
||||||
|
assert.Equal(t, int64(1), count)
|
||||||
|
|
||||||
|
// Test NOT search within description
|
||||||
|
repos, count, err = SearchRepository(&SearchRepoOptions{
|
||||||
|
Keyword: "description_14",
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
Collaborate: util.OptionalBoolFalse,
|
||||||
|
IncludeDescription: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, repos)
|
||||||
|
assert.Equal(t, int64(0), count)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
opts *SearchRepoOptions
|
opts *SearchRepoOptions
|
||||||
|
|
|
@ -150,19 +150,20 @@ var (
|
||||||
|
|
||||||
// UI settings
|
// UI settings
|
||||||
UI = struct {
|
UI = struct {
|
||||||
ExplorePagingNum int
|
ExplorePagingNum int
|
||||||
IssuePagingNum int
|
IssuePagingNum int
|
||||||
RepoSearchPagingNum int
|
RepoSearchPagingNum int
|
||||||
FeedMaxCommitNum int
|
FeedMaxCommitNum int
|
||||||
GraphMaxCommitNum int
|
GraphMaxCommitNum int
|
||||||
CodeCommentLines int
|
CodeCommentLines int
|
||||||
ReactionMaxUserNum int
|
ReactionMaxUserNum int
|
||||||
ThemeColorMetaTag string
|
ThemeColorMetaTag string
|
||||||
MaxDisplayFileSize int64
|
MaxDisplayFileSize int64
|
||||||
ShowUserEmail bool
|
ShowUserEmail bool
|
||||||
DefaultShowFullName bool
|
DefaultShowFullName bool
|
||||||
DefaultTheme string
|
DefaultTheme string
|
||||||
Themes []string
|
Themes []string
|
||||||
|
SearchRepoDescription bool
|
||||||
|
|
||||||
Admin struct {
|
Admin struct {
|
||||||
UserPagingNum int
|
UserPagingNum int
|
||||||
|
@ -942,6 +943,7 @@ func NewContext() {
|
||||||
|
|
||||||
UI.ShowUserEmail = Cfg.Section("ui").Key("SHOW_USER_EMAIL").MustBool(true)
|
UI.ShowUserEmail = Cfg.Section("ui").Key("SHOW_USER_EMAIL").MustBool(true)
|
||||||
UI.DefaultShowFullName = Cfg.Section("ui").Key("DEFAULT_SHOW_FULL_NAME").MustBool(false)
|
UI.DefaultShowFullName = Cfg.Section("ui").Key("DEFAULT_SHOW_FULL_NAME").MustBool(false)
|
||||||
|
UI.SearchRepoDescription = Cfg.Section("ui").Key("SEARCH_REPO_DESCRIPTION").MustBool(true)
|
||||||
|
|
||||||
HasRobotsTxt = com.IsFile(path.Join(CustomPath, "robots.txt"))
|
HasRobotsTxt = com.IsFile(path.Join(CustomPath, "robots.txt"))
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,10 @@ func Search(ctx *context.APIContext) {
|
||||||
// in: query
|
// in: query
|
||||||
// description: Limit search to repositories with keyword as topic
|
// description: Limit search to repositories with keyword as topic
|
||||||
// type: boolean
|
// type: boolean
|
||||||
|
// - name: includeDesc
|
||||||
|
// in: query
|
||||||
|
// description: include search of keyword within repository description
|
||||||
|
// type: boolean
|
||||||
// - name: uid
|
// - name: uid
|
||||||
// in: query
|
// in: query
|
||||||
// description: search only for repos that the user with the given id owns or contributes to
|
// description: search only for repos that the user with the given id owns or contributes to
|
||||||
|
@ -103,16 +107,17 @@ func Search(ctx *context.APIContext) {
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
opts := &models.SearchRepoOptions{
|
opts := &models.SearchRepoOptions{
|
||||||
Keyword: strings.Trim(ctx.Query("q"), " "),
|
Keyword: strings.Trim(ctx.Query("q"), " "),
|
||||||
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"),
|
TopicOnly: ctx.QueryBool("topic"),
|
||||||
Collaborate: util.OptionalBoolNone,
|
Collaborate: util.OptionalBoolNone,
|
||||||
Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")),
|
Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")),
|
||||||
UserIsAdmin: ctx.IsUserSiteAdmin(),
|
UserIsAdmin: ctx.IsUserSiteAdmin(),
|
||||||
UserID: ctx.Data["SignedUserID"].(int64),
|
UserID: ctx.Data["SignedUserID"].(int64),
|
||||||
StarredByID: ctx.QueryInt64("starredBy"),
|
StarredByID: ctx.QueryInt64("starredBy"),
|
||||||
|
IncludeDescription: ctx.QueryBool("includeDesc"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.QueryBool("exclusive") {
|
if ctx.QueryBool("exclusive") {
|
||||||
|
@ -157,7 +162,7 @@ func Search(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
repos, count, err := models.SearchRepositoryByName(opts)
|
repos, count, err := models.SearchRepository(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(500, api.SearchError{
|
ctx.JSON(500, api.SearchError{
|
||||||
OK: false,
|
OK: false,
|
||||||
|
|
|
@ -133,18 +133,19 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
|
||||||
keyword := strings.Trim(ctx.Query("q"), " ")
|
keyword := strings.Trim(ctx.Query("q"), " ")
|
||||||
topicOnly := ctx.QueryBool("topic")
|
topicOnly := ctx.QueryBool("topic")
|
||||||
|
|
||||||
repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{
|
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: opts.PageSize,
|
PageSize: opts.PageSize,
|
||||||
OrderBy: orderBy,
|
OrderBy: orderBy,
|
||||||
Private: opts.Private,
|
Private: opts.Private,
|
||||||
Keyword: keyword,
|
Keyword: keyword,
|
||||||
OwnerID: opts.OwnerID,
|
OwnerID: opts.OwnerID,
|
||||||
AllPublic: true,
|
AllPublic: true,
|
||||||
TopicOnly: topicOnly,
|
TopicOnly: topicOnly,
|
||||||
|
IncludeDescription: setting.UI.SearchRepoDescription,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("SearchRepositoryByName", err)
|
ctx.ServerError("SearchRepository", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Keyword"] = keyword
|
ctx.Data["Keyword"] = keyword
|
||||||
|
|
|
@ -499,19 +499,20 @@ func showOrgProfile(ctx *context.Context) {
|
||||||
count int64
|
count int64
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{
|
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
|
||||||
Keyword: keyword,
|
Keyword: keyword,
|
||||||
OwnerID: org.ID,
|
OwnerID: org.ID,
|
||||||
OrderBy: orderBy,
|
OrderBy: orderBy,
|
||||||
Private: ctx.IsSigned,
|
Private: ctx.IsSigned,
|
||||||
UserIsAdmin: ctx.IsUserSiteAdmin(),
|
UserIsAdmin: ctx.IsUserSiteAdmin(),
|
||||||
UserID: ctx.Data["SignedUserID"].(int64),
|
UserID: ctx.Data["SignedUserID"].(int64),
|
||||||
Page: page,
|
Page: page,
|
||||||
IsProfile: true,
|
IsProfile: true,
|
||||||
PageSize: setting.UI.User.RepoPagingNum,
|
PageSize: setting.UI.User.RepoPagingNum,
|
||||||
|
IncludeDescription: setting.UI.SearchRepoDescription,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("SearchRepositoryByName", err)
|
ctx.ServerError("SearchRepository", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -169,40 +169,42 @@ func Profile(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
case "stars":
|
case "stars":
|
||||||
ctx.Data["PageIsProfileStarList"] = true
|
ctx.Data["PageIsProfileStarList"] = true
|
||||||
repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{
|
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
|
||||||
Keyword: keyword,
|
Keyword: keyword,
|
||||||
OrderBy: orderBy,
|
OrderBy: orderBy,
|
||||||
Private: ctx.IsSigned,
|
Private: ctx.IsSigned,
|
||||||
UserIsAdmin: ctx.IsUserSiteAdmin(),
|
UserIsAdmin: ctx.IsUserSiteAdmin(),
|
||||||
UserID: ctx.Data["SignedUserID"].(int64),
|
UserID: ctx.Data["SignedUserID"].(int64),
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: setting.UI.User.RepoPagingNum,
|
PageSize: setting.UI.User.RepoPagingNum,
|
||||||
StarredByID: ctxUser.ID,
|
StarredByID: ctxUser.ID,
|
||||||
Collaborate: util.OptionalBoolFalse,
|
Collaborate: util.OptionalBoolFalse,
|
||||||
TopicOnly: topicOnly,
|
TopicOnly: topicOnly,
|
||||||
|
IncludeDescription: setting.UI.SearchRepoDescription,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("SearchRepositoryByName", err)
|
ctx.ServerError("SearchRepository", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total = int(count)
|
total = int(count)
|
||||||
default:
|
default:
|
||||||
repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{
|
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
|
||||||
Keyword: keyword,
|
Keyword: keyword,
|
||||||
OwnerID: ctxUser.ID,
|
OwnerID: ctxUser.ID,
|
||||||
OrderBy: orderBy,
|
OrderBy: orderBy,
|
||||||
Private: ctx.IsSigned,
|
Private: ctx.IsSigned,
|
||||||
UserIsAdmin: ctx.IsUserSiteAdmin(),
|
UserIsAdmin: ctx.IsUserSiteAdmin(),
|
||||||
UserID: ctx.Data["SignedUserID"].(int64),
|
UserID: ctx.Data["SignedUserID"].(int64),
|
||||||
Page: page,
|
Page: page,
|
||||||
IsProfile: true,
|
IsProfile: true,
|
||||||
PageSize: setting.UI.User.RepoPagingNum,
|
PageSize: setting.UI.User.RepoPagingNum,
|
||||||
Collaborate: util.OptionalBoolFalse,
|
Collaborate: util.OptionalBoolFalse,
|
||||||
TopicOnly: topicOnly,
|
TopicOnly: topicOnly,
|
||||||
|
IncludeDescription: setting.UI.SearchRepoDescription,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("SearchRepositoryByName", err)
|
ctx.ServerError("SearchRepository", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1099,6 +1099,12 @@
|
||||||
"name": "topic",
|
"name": "topic",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "include search of keyword within repository description",
|
||||||
|
"name": "includeDesc",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
|
|
Reference in a new issue