Do not mutate incoming options to RenderUserSearch and SearchUsers (#34544)

This PR changes the `opts` argument in `SearchUsers()` to be passed by
value instead of by pointer, as its mutations do not escape the function
scope and are not used elsewhere. This simplifies reasoning about the
function and avoids unnecessary pointer usage.

This insight emerged during an initial attempt to refactor
`RenderUserSearch()`, which currently intermixes multiple concerns.

Co-authored-by: Philip Peterson <philip-peterson@users.noreply.github.com>
This commit is contained in:
Philip Peterson
2025-05-27 12:36:02 -07:00
committed by GitHub
parent 498088c053
commit b0936f4f41
12 changed files with 31 additions and 31 deletions

View File

@ -137,7 +137,7 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
// SearchUsers takes options i.e. keyword and part of user name to search, // SearchUsers takes options i.e. keyword and part of user name to search,
// it returns results in given range and number of total results. // it returns results in given range and number of total results.
func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _ int64, _ error) { func SearchUsers(ctx context.Context, opts SearchUserOptions) (users []*User, _ int64, _ error) {
sessCount := opts.toSearchQueryBase(ctx) sessCount := opts.toSearchQueryBase(ctx)
defer sessCount.Close() defer sessCount.Close()
count, err := sessCount.Count(new(User)) count, err := sessCount.Count(new(User))
@ -152,7 +152,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String()) sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
defer sessQuery.Close() defer sessQuery.Close()
if opts.Page > 0 { if opts.Page > 0 {
sessQuery = db.SetSessionPagination(sessQuery, opts) sessQuery = db.SetSessionPagination(sessQuery, &opts)
} }
// the sql may contain JOIN, so we must only select User related columns // the sql may contain JOIN, so we must only select User related columns

View File

@ -88,7 +88,7 @@ func TestCanCreateOrganization(t *testing.T) {
func TestSearchUsers(t *testing.T) { func TestSearchUsers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
testSuccess := func(opts *user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) { testSuccess := func(opts user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) {
users, _, err := user_model.SearchUsers(db.DefaultContext, opts) users, _, err := user_model.SearchUsers(db.DefaultContext, opts)
assert.NoError(t, err) assert.NoError(t, err)
cassText := fmt.Sprintf("ids: %v, opts: %v", expectedUserOrOrgIDs, opts) cassText := fmt.Sprintf("ids: %v, opts: %v", expectedUserOrOrgIDs, opts)
@ -100,61 +100,61 @@ func TestSearchUsers(t *testing.T) {
} }
// test orgs // test orgs
testOrgSuccess := func(opts *user_model.SearchUserOptions, expectedOrgIDs []int64) { testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) {
opts.Type = user_model.UserTypeOrganization opts.Type = user_model.UserTypeOrganization
testSuccess(opts, expectedOrgIDs) testSuccess(opts, expectedOrgIDs)
} }
testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1, PageSize: 2}}, testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1, PageSize: 2}},
[]int64{3, 6}) []int64{3, 6})
testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 2, PageSize: 2}}, testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 2, PageSize: 2}},
[]int64{7, 17}) []int64{7, 17})
testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 3, PageSize: 2}}, testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 3, PageSize: 2}},
[]int64{19, 25}) []int64{19, 25})
testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}}, testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}},
[]int64{26, 41}) []int64{26, 41})
testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 5, PageSize: 2}}, testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 5, PageSize: 2}},
[]int64{42}) []int64{42})
testOrgSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 6, PageSize: 2}}, testOrgSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 6, PageSize: 2}},
[]int64{}) []int64{})
// test users // test users
testUserSuccess := func(opts *user_model.SearchUserOptions, expectedUserIDs []int64) { testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) {
opts.Type = user_model.UserTypeIndividual opts.Type = user_model.UserTypeIndividual
testSuccess(opts, expectedUserIDs) testSuccess(opts, expectedUserIDs)
} }
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}}, testUserSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)}, testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)},
[]int64{9}) []int64{9})
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, testUserSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, testUserSuccess(user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) []int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
// order by name asc default // order by name asc default
testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, testUserSuccess(user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) []int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)}, testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)},
[]int64{1}) []int64{1})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)}, testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)},
[]int64{29}) []int64{29})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)}, testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)},
[]int64{37}) []int64{37})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)}, testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)},
[]int64{24}) []int64{24})
} }

View File

@ -101,7 +101,7 @@ func GetAllOrgs(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx) listOptions := utils.GetListOptions(ctx)
users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer, Actor: ctx.Doer,
Type: user_model.UserTypeOrganization, Type: user_model.UserTypeOrganization,
OrderBy: db.SearchOrderByAlphabetically, OrderBy: db.SearchOrderByAlphabetically,

View File

@ -423,7 +423,7 @@ func SearchUsers(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx) listOptions := utils.GetListOptions(ctx)
users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer, Actor: ctx.Doer,
Type: user_model.UserTypeIndividual, Type: user_model.UserTypeIndividual,
LoginName: ctx.FormTrim("login_name"), LoginName: ctx.FormTrim("login_name"),

View File

@ -201,7 +201,7 @@ func GetAll(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx) listOptions := utils.GetListOptions(ctx)
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer, Actor: ctx.Doer,
ListOptions: listOptions, ListOptions: listOptions,
Type: user_model.UserTypeOrganization, Type: user_model.UserTypeOrganization,

View File

@ -73,7 +73,7 @@ func Search(ctx *context.APIContext) {
if ctx.PublicOnly { if ctx.PublicOnly {
visible = []structs.VisibleType{structs.VisibleTypePublic} visible = []structs.VisibleType{structs.VisibleTypePublic}
} }
users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ users, maxResults, err = user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer, Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"), Keyword: ctx.FormTrim("q"),
UID: uid, UID: uid,

View File

@ -27,7 +27,7 @@ func Organizations(ctx *context.Context) {
ctx.SetFormString("sort", UserSearchDefaultAdminSort) ctx.SetFormString("sort", UserSearchDefaultAdminSort)
} }
explore.RenderUserSearch(ctx, &user_model.SearchUserOptions{ explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer, Actor: ctx.Doer,
Type: user_model.UserTypeOrganization, Type: user_model.UserTypeOrganization,
IncludeReserved: true, // administrator needs to list all accounts include reserved IncludeReserved: true, // administrator needs to list all accounts include reserved

View File

@ -64,7 +64,7 @@ func Users(ctx *context.Context) {
"SortType": sortType, "SortType": sortType,
} }
explore.RenderUserSearch(ctx, &user_model.SearchUserOptions{ explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer, Actor: ctx.Doer,
Type: user_model.UserTypeIndividual, Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{

View File

@ -44,7 +44,7 @@ func Organizations(ctx *context.Context) {
ctx.SetFormString("sort", sortOrder) ctx.SetFormString("sort", sortOrder)
} }
RenderUserSearch(ctx, &user_model.SearchUserOptions{ RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer, Actor: ctx.Doer,
Type: user_model.UserTypeOrganization, Type: user_model.UserTypeOrganization,
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},

View File

@ -32,7 +32,7 @@ func isKeywordValid(keyword string) bool {
} }
// RenderUserSearch render user search page // RenderUserSearch render user search page
func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, tplName templates.TplName) { func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, tplName templates.TplName) {
// Sitemap index for sitemap paths // Sitemap index for sitemap paths
opts.Page = int(ctx.PathParamInt64("idx")) opts.Page = int(ctx.PathParamInt64("idx"))
isSitemap := ctx.PathParam("idx") != "" isSitemap := ctx.PathParam("idx") != ""
@ -151,7 +151,7 @@ func Users(ctx *context.Context) {
ctx.SetFormString("sort", sortOrder) ctx.SetFormString("sort", sortOrder)
} }
RenderUserSearch(ctx, &user_model.SearchUserOptions{ RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer, Actor: ctx.Doer,
Type: user_model.UserTypeIndividual, Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},

View File

@ -68,7 +68,7 @@ func Home(ctx *context.Context) {
func HomeSitemap(ctx *context.Context) { func HomeSitemap(ctx *context.Context) {
m := sitemap.NewSitemapIndex() m := sitemap.NewSitemapIndex()
if !setting.Service.Explore.DisableUsersPage { if !setting.Service.Explore.DisableUsersPage {
_, cnt, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ _, cnt, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Type: user_model.UserTypeIndividual, Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{PageSize: 1}, ListOptions: db.ListOptions{PageSize: 1},
IsActive: optional.Some(true), IsActive: optional.Some(true),

View File

@ -16,7 +16,7 @@ import (
// SearchCandidates searches candidate users for dropdown list // SearchCandidates searches candidate users for dropdown list
func SearchCandidates(ctx *context.Context) { func SearchCandidates(ctx *context.Context) {
users, _, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ users, _, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer, Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"), Keyword: ctx.FormTrim("q"),
Type: user_model.UserTypeIndividual, Type: user_model.UserTypeIndividual,