From 537fa6996214ef42348cfe89bab6f2e04380aeac Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 11 Dec 2023 16:56:48 +0800
Subject: [PATCH] Second part of refactor `db.Find` (#28194)

Continue of #27798 and move more functions to `db.Find` and `db.Count`.
---
 models/git/branch_list.go                  |  34 ++---
 models/git/branch_test.go                  |   6 +-
 models/issues/milestone.go                 |   9 +-
 models/issues/milestone_list.go            | 143 ++++-----------------
 models/issues/milestone_test.go            |  76 +++++++----
 modules/context/repo.go                    |   6 +-
 modules/repository/branch.go               |   8 +-
 routers/api/v1/repo/branch.go              |   7 +-
 routers/api/v1/repo/milestone.go           |  15 ++-
 routers/web/repo/issue.go                  |  15 +--
 routers/web/repo/milestone.go              |  12 +-
 routers/web/user/home.go                   |  17 ++-
 services/migrations/gitea_uploader_test.go |  12 +-
 services/repository/branch.go              |  11 +-
 14 files changed, 149 insertions(+), 222 deletions(-)

diff --git a/models/git/branch_list.go b/models/git/branch_list.go
index 2efe495264a..0e8d28038a5 100644
--- a/models/git/branch_list.go
+++ b/models/git/branch_list.go
@@ -12,7 +12,6 @@ import (
 	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
-	"xorm.io/xorm"
 )
 
 type BranchList []*Branch
@@ -91,31 +90,20 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
 	return cond
 }
 
-func CountBranches(ctx context.Context, opts FindBranchOptions) (int64, error) {
-	return db.GetEngine(ctx).Where(opts.ToConds()).Count(&Branch{})
-}
-
-func orderByBranches(sess *xorm.Session, opts FindBranchOptions) *xorm.Session {
+func (opts FindBranchOptions) ToOrders() string {
+	orderBy := opts.OrderBy
 	if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end
-		sess = sess.OrderBy("is_deleted ASC")
+		if orderBy != "" {
+			orderBy += ", "
+		}
+		orderBy += "is_deleted ASC"
 	}
-
-	if opts.OrderBy == "" {
+	if orderBy == "" {
 		// the commit_time might be the same, so add the "name" to make sure the order is stable
-		opts.OrderBy = "commit_time DESC, name ASC"
+		return "commit_time DESC, name ASC"
 	}
-	return sess.OrderBy(opts.OrderBy)
-}
 
-func FindBranches(ctx context.Context, opts FindBranchOptions) (BranchList, error) {
-	sess := db.GetEngine(ctx).Where(opts.ToConds())
-	if opts.PageSize > 0 && !opts.IsListAll() {
-		sess = db.SetSessionPagination(sess, &opts.ListOptions)
-	}
-	sess = orderByBranches(sess, opts)
-
-	var branches []*Branch
-	return branches, sess.Find(&branches)
+	return orderBy
 }
 
 func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, error) {
@@ -123,9 +111,9 @@ func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, err
 	if opts.PageSize > 0 && !opts.IsListAll() {
 		sess = db.SetSessionPagination(sess, &opts.ListOptions)
 	}
-	sess = orderByBranches(sess, opts)
+
 	var branches []string
-	if err := sess.Table("branch").Find(&branches); err != nil {
+	if err := sess.Table("branch").OrderBy(opts.ToOrders()).Find(&branches); err != nil {
 		return nil, err
 	}
 	return branches, nil
diff --git a/models/git/branch_test.go b/models/git/branch_test.go
index 07b243e5e6e..ce4cbd56a12 100644
--- a/models/git/branch_test.go
+++ b/models/git/branch_test.go
@@ -45,10 +45,8 @@ func TestGetDeletedBranches(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 
-	branches, err := git_model.FindBranches(db.DefaultContext, git_model.FindBranchOptions{
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+	branches, err := db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{
+		ListOptions:     db.ListOptionsAll,
 		RepoID:          repo.ID,
 		IsDeletedBranch: util.OptionalBoolTrue,
 	})
diff --git a/models/issues/milestone.go b/models/issues/milestone.go
index ad1d5d04532..9db7cb2b15e 100644
--- a/models/issues/milestone.go
+++ b/models/issues/milestone.go
@@ -295,16 +295,15 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
 		return err
 	}
 
-	numMilestones, err := CountMilestones(ctx, GetMilestonesOption{
+	numMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
 		RepoID: repo.ID,
-		State:  api.StateAll,
 	})
 	if err != nil {
 		return err
 	}
-	numClosedMilestones, err := CountMilestones(ctx, GetMilestonesOption{
-		RepoID: repo.ID,
-		State:  api.StateClosed,
+	numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
+		RepoID:   repo.ID,
+		IsClosed: util.OptionalBoolTrue,
 	})
 	if err != nil {
 		return err
diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go
index d55c18a9954..f331b2590fa 100644
--- a/models/issues/milestone_list.go
+++ b/models/issues/milestone_list.go
@@ -8,8 +8,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/setting"
-	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
 )
@@ -25,31 +24,31 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
 	return ids
 }
 
-// GetMilestonesOption contain options to get milestones
-type GetMilestonesOption struct {
+// FindMilestoneOptions contain options to get milestones
+type FindMilestoneOptions struct {
 	db.ListOptions
 	RepoID   int64
-	State    api.StateType
+	IsClosed util.OptionalBool
 	Name     string
 	SortType string
+	RepoCond builder.Cond
+	RepoIDs  []int64
 }
 
-func (opts GetMilestonesOption) toCond() builder.Cond {
+func (opts FindMilestoneOptions) ToConds() builder.Cond {
 	cond := builder.NewCond()
 	if opts.RepoID != 0 {
 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
 	}
-
-	switch opts.State {
-	case api.StateClosed:
-		cond = cond.And(builder.Eq{"is_closed": true})
-	case api.StateAll:
-		break
-	// api.StateOpen:
-	default:
-		cond = cond.And(builder.Eq{"is_closed": false})
+	if opts.IsClosed != util.OptionalBoolNone {
+		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()})
+	}
+	if opts.RepoCond != nil && opts.RepoCond.IsValid() {
+		cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))
+	}
+	if len(opts.RepoIDs) > 0 {
+		cond = cond.And(builder.In("repo_id", opts.RepoIDs))
 	}
-
 	if len(opts.Name) != 0 {
 		cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
 	}
@@ -57,34 +56,23 @@ func (opts GetMilestonesOption) toCond() builder.Cond {
 	return cond
 }
 
-// GetMilestones returns milestones filtered by GetMilestonesOption's
-func GetMilestones(ctx context.Context, opts GetMilestonesOption) (MilestoneList, int64, error) {
-	sess := db.GetEngine(ctx).Where(opts.toCond())
-
-	if opts.Page != 0 {
-		sess = db.SetSessionPagination(sess, &opts)
-	}
-
+func (opts FindMilestoneOptions) ToOrders() string {
 	switch opts.SortType {
 	case "furthestduedate":
-		sess.Desc("deadline_unix")
+		return "deadline_unix DESC"
 	case "leastcomplete":
-		sess.Asc("completeness")
+		return "completeness ASC"
 	case "mostcomplete":
-		sess.Desc("completeness")
+		return "completeness DESC"
 	case "leastissues":
-		sess.Asc("num_issues")
+		return "num_issues ASC"
 	case "mostissues":
-		sess.Desc("num_issues")
+		return "num_issues DESC"
 	case "id":
-		sess.Asc("id")
+		return "id ASC"
 	default:
-		sess.Asc("deadline_unix").Asc("id")
+		return "deadline_unix ASC, id ASC"
 	}
-
-	miles := make([]*Milestone, 0, opts.PageSize)
-	total, err := sess.FindAndCount(&miles)
-	return miles, total, err
 }
 
 // GetMilestoneIDsByNames returns a list of milestone ids by given names.
@@ -99,49 +87,6 @@ func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error
 		Find(&ids)
 }
 
-// SearchMilestones search milestones
-func SearchMilestones(ctx context.Context, repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) {
-	miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
-	sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
-	if len(keyword) > 0 {
-		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
-	}
-	if repoCond.IsValid() {
-		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
-	}
-	if page > 0 {
-		sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
-	}
-
-	switch sortType {
-	case "furthestduedate":
-		sess.Desc("deadline_unix")
-	case "leastcomplete":
-		sess.Asc("completeness")
-	case "mostcomplete":
-		sess.Desc("completeness")
-	case "leastissues":
-		sess.Asc("num_issues")
-	case "mostissues":
-		sess.Desc("num_issues")
-	default:
-		sess.Asc("deadline_unix")
-	}
-	return miles, sess.Find(&miles)
-}
-
-// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
-func GetMilestonesByRepoIDs(ctx context.Context, repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
-	return SearchMilestones(
-		ctx,
-		builder.In("repo_id", repoIDs),
-		page,
-		isClosed,
-		sortType,
-		"",
-	)
-}
-
 // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
 func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error {
 	type totalTimesByMilestone struct {
@@ -183,47 +128,9 @@ func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error
 	return nil
 }
 
-// CountMilestones returns number of milestones in given repository with other options
-func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) {
-	return db.GetEngine(ctx).
-		Where(opts.toCond()).
-		Count(new(Milestone))
-}
-
-// CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
-func CountMilestonesByRepoCond(ctx context.Context, repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
-	sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
-	if repoCond.IsValid() {
-		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
-	}
-
-	countsSlice := make([]*struct {
-		RepoID int64
-		Count  int64
-	}, 0, 10)
-	if err := sess.GroupBy("repo_id").
-		Select("repo_id AS repo_id, COUNT(*) AS count").
-		Table("milestone").
-		Find(&countsSlice); err != nil {
-		return nil, err
-	}
-
-	countMap := make(map[int64]int64, len(countsSlice))
-	for _, c := range countsSlice {
-		countMap[c.RepoID] = c.Count
-	}
-	return countMap, nil
-}
-
 // CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
-func CountMilestonesByRepoCondAndKw(ctx context.Context, repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) {
-	sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
-	if len(keyword) > 0 {
-		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
-	}
-	if repoCond.IsValid() {
-		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
-	}
+func CountMilestonesMap(ctx context.Context, opts FindMilestoneOptions) (map[int64]int64, error) {
+	sess := db.GetEngine(ctx).Where(opts.ToConds())
 
 	countsSlice := make([]*struct {
 		RepoID int64
diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go
index ecfccf6ddbe..0581d3d1488 100644
--- a/models/issues/milestone_test.go
+++ b/models/issues/milestone_test.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 	"xorm.io/builder"
@@ -39,10 +40,15 @@ func TestGetMilestoneByRepoID(t *testing.T) {
 func TestGetMilestonesByRepoID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	test := func(repoID int64, state api.StateType) {
+		var isClosed util.OptionalBool
+		switch state {
+		case api.StateClosed, api.StateOpen:
+			isClosed = util.OptionalBoolOf(state == api.StateClosed)
+		}
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
-		milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
-			RepoID: repo.ID,
-			State:  state,
+		milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
+			RepoID:   repo.ID,
+			IsClosed: isClosed,
 		})
 		assert.NoError(t, err)
 
@@ -77,9 +83,9 @@ func TestGetMilestonesByRepoID(t *testing.T) {
 	test(3, api.StateClosed)
 	test(3, api.StateAll)
 
-	milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
-		RepoID: unittest.NonexistentID,
-		State:  api.StateOpen,
+	milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
+		RepoID:   unittest.NonexistentID,
+		IsClosed: util.OptionalBoolFalse,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, milestones, 0)
@@ -90,13 +96,13 @@ func TestGetMilestones(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
 		for _, page := range []int{0, 1} {
-			milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
+			milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 				ListOptions: db.ListOptions{
 					Page:     page,
 					PageSize: setting.UI.IssuePagingNum,
 				},
 				RepoID:   repo.ID,
-				State:    api.StateOpen,
+				IsClosed: util.OptionalBoolFalse,
 				SortType: sortType,
 			})
 			assert.NoError(t, err)
@@ -107,13 +113,13 @@ func TestGetMilestones(t *testing.T) {
 			}
 			assert.True(t, sort.IntsAreSorted(values))
 
-			milestones, _, err = issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
+			milestones, err = db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 				ListOptions: db.ListOptions{
 					Page:     page,
 					PageSize: setting.UI.IssuePagingNum,
 				},
 				RepoID:   repo.ID,
-				State:    api.StateClosed,
+				IsClosed: util.OptionalBoolTrue,
 				Name:     "",
 				SortType: sortType,
 			})
@@ -150,9 +156,8 @@ func TestCountRepoMilestones(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	test := func(repoID int64) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
-		count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
+		count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 			RepoID: repoID,
-			State:  api.StateAll,
 		})
 		assert.NoError(t, err)
 		assert.EqualValues(t, repo.NumMilestones, count)
@@ -161,9 +166,8 @@ func TestCountRepoMilestones(t *testing.T) {
 	test(2)
 	test(3)
 
-	count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
+	count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoID: unittest.NonexistentID,
-		State:  api.StateAll,
 	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, count)
@@ -173,9 +177,9 @@ func TestCountRepoClosedMilestones(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	test := func(repoID int64) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
-		count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
-			RepoID: repoID,
-			State:  api.StateClosed,
+		count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
+			RepoID:   repoID,
+			IsClosed: util.OptionalBoolTrue,
 		})
 		assert.NoError(t, err)
 		assert.EqualValues(t, repo.NumClosedMilestones, count)
@@ -184,9 +188,9 @@ func TestCountRepoClosedMilestones(t *testing.T) {
 	test(2)
 	test(3)
 
-	count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
-		RepoID: unittest.NonexistentID,
-		State:  api.StateClosed,
+	count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
+		RepoID:   unittest.NonexistentID,
+		IsClosed: util.OptionalBoolTrue,
 	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, count)
@@ -201,12 +205,19 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
 	repo1OpenCount, repo1ClosedCount := milestonesCount(1)
 	repo2OpenCount, repo2ClosedCount := milestonesCount(2)
 
-	openCounts, err := issues_model.CountMilestonesByRepoCond(db.DefaultContext, builder.In("repo_id", []int64{1, 2}), false)
+	openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{
+		RepoIDs:  []int64{1, 2},
+		IsClosed: util.OptionalBoolFalse,
+	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, repo1OpenCount, openCounts[1])
 	assert.EqualValues(t, repo2OpenCount, openCounts[2])
 
-	closedCounts, err := issues_model.CountMilestonesByRepoCond(db.DefaultContext, builder.In("repo_id", []int64{1, 2}), true)
+	closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext,
+		issues_model.FindMilestoneOptions{
+			RepoIDs:  []int64{1, 2},
+			IsClosed: util.OptionalBoolTrue,
+		})
 	assert.NoError(t, err)
 	assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
 	assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
@@ -218,7 +229,15 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 	repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
 	test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
 		for _, page := range []int{0, 1} {
-			openMilestones, err := issues_model.GetMilestonesByRepoIDs(db.DefaultContext, []int64{repo1.ID, repo2.ID}, page, false, sortType)
+			openMilestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
+				ListOptions: db.ListOptions{
+					Page:     page,
+					PageSize: setting.UI.IssuePagingNum,
+				},
+				RepoIDs:  []int64{repo1.ID, repo2.ID},
+				IsClosed: util.OptionalBoolFalse,
+				SortType: sortType,
+			})
 			assert.NoError(t, err)
 			assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
 			values := make([]int, len(openMilestones))
@@ -227,7 +246,16 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 			}
 			assert.True(t, sort.IntsAreSorted(values))
 
-			closedMilestones, err := issues_model.GetMilestonesByRepoIDs(db.DefaultContext, []int64{repo1.ID, repo2.ID}, page, true, sortType)
+			closedMilestones, err := db.Find[issues_model.Milestone](db.DefaultContext,
+				issues_model.FindMilestoneOptions{
+					ListOptions: db.ListOptions{
+						Page:     page,
+						PageSize: setting.UI.IssuePagingNum,
+					},
+					RepoIDs:  []int64{repo1.ID, repo2.ID},
+					IsClosed: util.OptionalBoolTrue,
+					SortType: sortType,
+				})
 			assert.NoError(t, err)
 			assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
 			values = make([]int, len(closedMilestones))
diff --git a/modules/context/repo.go b/modules/context/repo.go
index f91dd6aa9a8..a18dc873b64 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -668,11 +668,9 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
 		IsDeletedBranch: util.OptionalBoolFalse,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		ListOptions:     db.ListOptionsAll,
 	}
-	branchesTotal, err := git_model.CountBranches(ctx, branchOpts)
+	branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts)
 	if err != nil {
 		ctx.ServerError("CountBranches", err)
 		return cancel
diff --git a/modules/repository/branch.go b/modules/repository/branch.go
index 8daceecb44f..cd45f162276 100644
--- a/modules/repository/branch.go
+++ b/modules/repository/branch.go
@@ -49,11 +49,9 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
 
 	dbBranches := make(map[string]*git_model.Branch)
 	{
-		branches, err := git_model.FindBranches(ctx, git_model.FindBranchOptions{
-			ListOptions: db.ListOptions{
-				ListAll: true,
-			},
-			RepoID: repo.ID,
+		branches, err := db.Find[git_model.Branch](ctx, git_model.FindBranchOptions{
+			ListOptions: db.ListOptionsAll,
+			RepoID:      repo.ID,
 		})
 		if err != nil {
 			return 0, err
diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go
index 677105bdb50..36c85c8a570 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -10,6 +10,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
 	"code.gitea.io/gitea/models/organization"
 	user_model "code.gitea.io/gitea/models/user"
@@ -137,7 +138,7 @@ func DeleteBranch(ctx *context.APIContext) {
 	}
 
 	// check whether branches of this repository has been synced
-	totalNumOfBranches, err := git_model.CountBranches(ctx, git_model.FindBranchOptions{
+	totalNumOfBranches, err := db.Count[git_model.Branch](ctx, git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
 		IsDeletedBranch: util.OptionalBoolFalse,
 	})
@@ -341,7 +342,7 @@ func ListBranches(ctx *context.APIContext) {
 			IsDeletedBranch: util.OptionalBoolFalse,
 		}
 		var err error
-		totalNumOfBranches, err = git_model.CountBranches(ctx, branchOpts)
+		totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts)
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, "CountBranches", err)
 			return
@@ -360,7 +361,7 @@ func ListBranches(ctx *context.APIContext) {
 			return
 		}
 
-		branches, err := git_model.FindBranches(ctx, branchOpts)
+		branches, err := db.Find[git_model.Branch](ctx, branchOpts)
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, "GetBranches", err)
 			return
diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go
index da470dfa74c..9c2ed16d930 100644
--- a/routers/api/v1/repo/milestone.go
+++ b/routers/api/v1/repo/milestone.go
@@ -9,10 +9,12 @@ import (
 	"strconv"
 	"time"
 
+	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	"code.gitea.io/gitea/services/convert"
@@ -58,14 +60,21 @@ func ListMilestones(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	milestones, total, err := issues_model.GetMilestones(ctx, issues_model.GetMilestonesOption{
+	state := api.StateType(ctx.FormString("state"))
+	var isClosed util.OptionalBool
+	switch state {
+	case api.StateClosed, api.StateOpen:
+		isClosed = util.OptionalBoolOf(state == api.StateClosed)
+	}
+
+	milestones, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
 		ListOptions: utils.GetListOptions(ctx),
 		RepoID:      ctx.Repo.Repository.ID,
-		State:       api.StateType(ctx.FormString("state")),
+		IsClosed:    isClosed,
 		Name:        ctx.FormString("name"),
 	})
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetMilestones", err)
+		ctx.Error(http.StatusInternalServerError, "db.FindAndCount[issues_model.Milestone]", err)
 		return
 	}
 
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index ba8b49774b9..14781be822d 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -510,9 +510,8 @@ func Issues(ctx *context.Context) {
 
 func renderMilestones(ctx *context.Context) {
 	// Get milestones
-	milestones, _, err := issues_model.GetMilestones(ctx, issues_model.GetMilestonesOption{
+	milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
 		RepoID: ctx.Repo.Repository.ID,
-		State:  api.StateAll,
 	})
 	if err != nil {
 		ctx.ServerError("GetAllRepoMilestones", err)
@@ -534,17 +533,17 @@ func renderMilestones(ctx *context.Context) {
 // RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
 func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
 	var err error
-	ctx.Data["OpenMilestones"], _, err = issues_model.GetMilestones(ctx, issues_model.GetMilestonesOption{
-		RepoID: repo.ID,
-		State:  api.StateOpen,
+	ctx.Data["OpenMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
+		RepoID:   repo.ID,
+		IsClosed: util.OptionalBoolFalse,
 	})
 	if err != nil {
 		ctx.ServerError("GetMilestones", err)
 		return
 	}
-	ctx.Data["ClosedMilestones"], _, err = issues_model.GetMilestones(ctx, issues_model.GetMilestonesOption{
-		RepoID: repo.ID,
-		State:  api.StateClosed,
+	ctx.Data["ClosedMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
+		RepoID:   repo.ID,
+		IsClosed: util.OptionalBoolTrue,
 	})
 	if err != nil {
 		ctx.ServerError("GetMilestones", err)
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 0e6f6307474..fbecabb2b1f 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -16,7 +16,6 @@ import (
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
@@ -46,18 +45,13 @@ func Milestones(ctx *context.Context) {
 		page = 1
 	}
 
-	state := structs.StateOpen
-	if isShowClosed {
-		state = structs.StateClosed
-	}
-
-	miles, total, err := issues_model.GetMilestones(ctx, issues_model.GetMilestonesOption{
+	miles, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
 		ListOptions: db.ListOptions{
 			Page:     page,
 			PageSize: setting.UI.IssuePagingNum,
 		},
 		RepoID:   ctx.Repo.Repository.ID,
-		State:    state,
+		IsClosed: util.OptionalBoolOf(isShowClosed),
 		SortType: sortType,
 		Name:     keyword,
 	})
@@ -80,7 +74,7 @@ func Milestones(ctx *context.Context) {
 		url.QueryEscape(keyword), url.QueryEscape(sortType))
 
 	if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
-		if err := miles.LoadTotalTrackedTimes(ctx); err != nil {
+		if err := issues_model.MilestoneList(miles).LoadTotalTrackedTimes(ctx); err != nil {
 			ctx.ServerError("LoadTotalTrackedTimes", err)
 			return
 		}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index d54e7624290..204d4adbd49 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -212,13 +212,26 @@ func Milestones(ctx *context.Context) {
 		}
 	}
 
-	counts, err := issues_model.CountMilestonesByRepoCondAndKw(ctx, userRepoCond, keyword, isShowClosed)
+	counts, err := issues_model.CountMilestonesMap(ctx, issues_model.FindMilestoneOptions{
+		RepoCond: userRepoCond,
+		Name:     keyword,
+		IsClosed: util.OptionalBoolOf(isShowClosed),
+	})
 	if err != nil {
 		ctx.ServerError("CountMilestonesByRepoIDs", err)
 		return
 	}
 
-	milestones, err := issues_model.SearchMilestones(ctx, repoCond, page, isShowClosed, sortType, keyword)
+	milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
+		ListOptions: db.ListOptions{
+			Page:     page,
+			PageSize: setting.UI.IssuePagingNum,
+		},
+		RepoCond: repoCond,
+		IsClosed: util.OptionalBoolOf(isShowClosed),
+		SortType: sortType,
+		Name:     keyword,
+	})
 	if err != nil {
 		ctx.ServerError("SearchMilestones", err)
 		return
diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go
index 84db83bc670..7c291eabafa 100644
--- a/services/migrations/gitea_uploader_test.go
+++ b/services/migrations/gitea_uploader_test.go
@@ -65,16 +65,16 @@ func TestGiteaUploadRepo(t *testing.T) {
 	assert.True(t, repo.HasWiki())
 	assert.EqualValues(t, repo_model.RepositoryReady, repo.Status)
 
-	milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
-		RepoID: repo.ID,
-		State:  structs.StateOpen,
+	milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
+		RepoID:   repo.ID,
+		IsClosed: util.OptionalBoolFalse,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, milestones, 1)
 
-	milestones, _, err = issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
-		RepoID: repo.ID,
-		State:  structs.StateClosed,
+	milestones, err = db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
+		RepoID:   repo.ID,
+		IsClosed: util.OptionalBoolTrue,
 	})
 	assert.NoError(t, err)
 	assert.Empty(t, milestones)
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 6dc286675f3..b3dbb252ca3 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -66,22 +66,17 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 		Keyword: keyword,
 	}
 
-	totalNumOfBranches, err := git_model.CountBranches(ctx, branchOpts)
+	dbBranches, totalNumOfBranches, err := db.FindAndCount[git_model.Branch](ctx, branchOpts)
 	if err != nil {
 		return nil, nil, 0, err
 	}
 
 	branchOpts.ExcludeBranchNames = []string{repo.DefaultBranch}
 
-	dbBranches, err := git_model.FindBranches(ctx, branchOpts)
-	if err != nil {
+	if err := git_model.BranchList(dbBranches).LoadDeletedBy(ctx); err != nil {
 		return nil, nil, 0, err
 	}
-
-	if err := dbBranches.LoadDeletedBy(ctx); err != nil {
-		return nil, nil, 0, err
-	}
-	if err := dbBranches.LoadPusher(ctx); err != nil {
+	if err := git_model.BranchList(dbBranches).LoadPusher(ctx); err != nil {
 		return nil, nil, 0, err
 	}