mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	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:
		
				
					committed by
					
						 Lauris BH
						Lauris BH
					
				
			
			
				
	
			
			
			
						parent
						
							8c24bb9e43
						
					
				
				
					commit
					c9546d4cdd
				
			| @@ -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 New Issue
	
	Block a user