From 70c4aad8e1cbc46b049b015dcd6f2e5be5a69e72 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 15 Jan 2024 10:19:25 +0800
Subject: [PATCH] Move more functions to db.Find (#28419)

Following #28220

This PR move more functions to use `db.Find`.

---------

Co-authored-by: delvh <dev.lh@web.de>
---
 cmd/admin.go                                  |  4 +-
 models/asymkey/gpg_key.go                     | 55 +++++++----------
 models/asymkey/gpg_key_commit_verification.go | 23 ++++++-
 models/asymkey/gpg_key_list.go                | 38 ++++++++++++
 models/asymkey/ssh_key.go                     |  4 +-
 models/asymkey/ssh_key_principals.go          | 23 -------
 models/auth/webauthn.go                       |  3 +-
 models/db/list.go                             | 61 ++++++++++---------
 models/db/paginator/paginator_test.go         |  3 -
 models/issues/comment.go                      |  3 +-
 models/issues/tracked_time.go                 | 20 ++++--
 models/repo/archiver.go                       | 13 +---
 models/repo/collaboration.go                  | 55 ++++++++---------
 models/repo/collaboration_test.go             | 12 +++-
 models/repo/fork.go                           | 11 ++--
 models/repo/release.go                        | 42 +++----------
 modules/context/repo.go                       |  6 +-
 modules/repository/repo.go                    |  3 +-
 routers/api/v1/repo/collaborators.go          |  5 +-
 routers/api/v1/repo/pull.go                   | 26 +++-----
 routers/api/v1/repo/release.go                |  6 +-
 routers/api/v1/user/gpg_key.go                | 27 +++++---
 routers/web/feed/release.go                   |  4 +-
 routers/web/repo/release.go                   |  6 +-
 routers/web/repo/release_test.go              |  4 +-
 routers/web/repo/repo.go                      |  5 +-
 routers/web/user/home.go                      |  5 +-
 routers/web/user/setting/keys.go              | 15 ++++-
 services/asymkey/sign.go                      | 20 ++++--
 services/convert/repository.go                |  7 ++-
 services/migrations/gitea_uploader_test.go    |  6 +-
 services/repository/archiver/archiver.go      |  2 +-
 services/repository/push.go                   |  7 ++-
 services/user/delete.go                       |  4 +-
 tests/integration/mirror_pull_test.go         | 12 ++--
 tests/integration/repo_tag_test.go            |  3 +-
 36 files changed, 305 insertions(+), 238 deletions(-)
 create mode 100644 models/asymkey/gpg_key_list.go

diff --git a/cmd/admin.go b/cmd/admin.go
index b5903cd4fd..74bfa5a6c6 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -157,10 +157,10 @@ func runRepoSyncReleases(_ *cli.Context) error {
 }
 
 func getReleaseCount(ctx context.Context, id int64) (int64, error) {
-	return repo_model.GetReleaseCountByRepoID(
+	return db.Count[repo_model.Release](
 		ctx,
-		id,
 		repo_model.FindReleasesOptions{
+			RepoID:      id,
 			IncludeTags: true,
 		},
 	)
diff --git a/models/asymkey/gpg_key.go b/models/asymkey/gpg_key.go
index 421f24d4de..5236b2d450 100644
--- a/models/asymkey/gpg_key.go
+++ b/models/asymkey/gpg_key.go
@@ -11,21 +11,13 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
 
 	"github.com/keybase/go-crypto/openpgp"
 	"github.com/keybase/go-crypto/openpgp/packet"
-	"xorm.io/xorm"
+	"xorm.io/builder"
 )
 
-//   __________________  ________   ____  __.
-//  /  _____/\______   \/  _____/  |    |/ _|____ ___.__.
-// /   \  ___ |     ___/   \  ___  |      <_/ __ <   |  |
-// \    \_\  \|    |   \    \_\  \ |    |  \  ___/\___  |
-//	\______  /|____|    \______  / |____|__ \___  > ____|
-//				 \/                  \/          \/   \/\/
-
 // GPGKey represents a GPG key.
 type GPGKey struct {
 	ID                int64              `xorm:"pk autoincr"`
@@ -54,12 +46,11 @@ func (key *GPGKey) BeforeInsert() {
 	key.AddedUnix = timeutil.TimeStampNow()
 }
 
-// AfterLoad is invoked from XORM after setting the values of all fields of this object.
-func (key *GPGKey) AfterLoad(session *xorm.Session) {
-	err := session.Where("primary_key_id=?", key.KeyID).Find(&key.SubsKey)
-	if err != nil {
-		log.Error("Find Sub GPGkeys[%s]: %v", key.KeyID, err)
+func (key *GPGKey) LoadSubKeys(ctx context.Context) error {
+	if err := db.GetEngine(ctx).Where("primary_key_id=?", key.KeyID).Find(&key.SubsKey); err != nil {
+		return fmt.Errorf("find Sub GPGkeys[%s]: %v", key.KeyID, err)
 	}
+	return nil
 }
 
 // PaddedKeyID show KeyID padded to 16 characters
@@ -76,20 +67,26 @@ func PaddedKeyID(keyID string) string {
 	return zeros[0:16-len(keyID)] + keyID
 }
 
-// ListGPGKeys returns a list of public keys belongs to given user.
-func ListGPGKeys(ctx context.Context, uid int64, listOptions db.ListOptions) ([]*GPGKey, error) {
-	sess := db.GetEngine(ctx).Table(&GPGKey{}).Where("owner_id=? AND primary_key_id=''", uid)
-	if listOptions.Page != 0 {
-		sess = db.SetSessionPagination(sess, &listOptions)
-	}
-
-	keys := make([]*GPGKey, 0, 2)
-	return keys, sess.Find(&keys)
+type FindGPGKeyOptions struct {
+	db.ListOptions
+	OwnerID        int64
+	KeyID          string
+	IncludeSubKeys bool
 }
 
-// CountUserGPGKeys return number of gpg keys a user own
-func CountUserGPGKeys(ctx context.Context, userID int64) (int64, error) {
-	return db.GetEngine(ctx).Where("owner_id=? AND primary_key_id=''", userID).Count(&GPGKey{})
+func (opts FindGPGKeyOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if !opts.IncludeSubKeys {
+		cond = cond.And(builder.Eq{"primary_key_id": ""})
+	}
+
+	if opts.OwnerID > 0 {
+		cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
+	}
+	if opts.KeyID != "" {
+		cond = cond.And(builder.Eq{"key_id": opts.KeyID})
+	}
+	return cond
 }
 
 func GetGPGKeyForUserByID(ctx context.Context, ownerID, keyID int64) (*GPGKey, error) {
@@ -103,12 +100,6 @@ func GetGPGKeyForUserByID(ctx context.Context, ownerID, keyID int64) (*GPGKey, e
 	return key, nil
 }
 
-// GetGPGKeysByKeyID returns public key by given ID.
-func GetGPGKeysByKeyID(ctx context.Context, keyID string) ([]*GPGKey, error) {
-	keys := make([]*GPGKey, 0, 1)
-	return keys, db.GetEngine(ctx).Where("key_id=?", keyID).Find(&keys)
-}
-
 // GPGKeyToEntity retrieve the imported key and the traducted entity
 func GPGKeyToEntity(ctx context.Context, k *GPGKey) (*openpgp.Entity, error) {
 	impKey, err := GetGPGImportByKeyID(ctx, k.KeyID)
diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go
index 8ac4364404..83fbab5d36 100644
--- a/models/asymkey/gpg_key_commit_verification.go
+++ b/models/asymkey/gpg_key_commit_verification.go
@@ -166,7 +166,9 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerific
 
 	// Now try to associate the signature with the committer, if present
 	if committer.ID != 0 {
-		keys, err := ListGPGKeys(ctx, committer.ID, db.ListOptions{})
+		keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
+			OwnerID: committer.ID,
+		})
 		if err != nil { // Skipping failed to get gpg keys of user
 			log.Error("ListGPGKeys: %v", err)
 			return &CommitVerification{
@@ -176,6 +178,15 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerific
 			}
 		}
 
+		if err := GPGKeyList(keys).LoadSubKeys(ctx); err != nil {
+			log.Error("LoadSubKeys: %v", err)
+			return &CommitVerification{
+				CommittingUser: committer,
+				Verified:       false,
+				Reason:         "gpg.error.failed_retrieval_gpg_keys",
+			}
+		}
+
 		committerEmailAddresses, _ := user_model.GetEmailAddresses(ctx, committer.ID)
 		activated := false
 		for _, e := range committerEmailAddresses {
@@ -392,7 +403,10 @@ func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload s
 	if keyID == "" {
 		return nil
 	}
-	keys, err := GetGPGKeysByKeyID(ctx, keyID)
+	keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
+		KeyID:          keyID,
+		IncludeSubKeys: true,
+	})
 	if err != nil {
 		log.Error("GetGPGKeysByKeyID: %v", err)
 		return &CommitVerification{
@@ -407,7 +421,10 @@ func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload s
 	for _, key := range keys {
 		var primaryKeys []*GPGKey
 		if key.PrimaryKeyID != "" {
-			primaryKeys, err = GetGPGKeysByKeyID(ctx, key.PrimaryKeyID)
+			primaryKeys, err = db.Find[GPGKey](ctx, FindGPGKeyOptions{
+				KeyID:          key.PrimaryKeyID,
+				IncludeSubKeys: true,
+			})
 			if err != nil {
 				log.Error("GetGPGKeysByKeyID: %v", err)
 				return &CommitVerification{
diff --git a/models/asymkey/gpg_key_list.go b/models/asymkey/gpg_key_list.go
new file mode 100644
index 0000000000..89548e495e
--- /dev/null
+++ b/models/asymkey/gpg_key_list.go
@@ -0,0 +1,38 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models/db"
+)
+
+type GPGKeyList []*GPGKey
+
+func (keys GPGKeyList) keyIDs() []string {
+	ids := make([]string, len(keys))
+	for i, key := range keys {
+		ids[i] = key.KeyID
+	}
+	return ids
+}
+
+func (keys GPGKeyList) LoadSubKeys(ctx context.Context) error {
+	subKeys := make([]*GPGKey, 0, len(keys))
+	if err := db.GetEngine(ctx).In("primary_key_id", keys.keyIDs()).Find(&subKeys); err != nil {
+		return err
+	}
+	subKeysMap := make(map[string][]*GPGKey, len(subKeys))
+	for _, key := range subKeys {
+		subKeysMap[key.PrimaryKeyID] = append(subKeysMap[key.PrimaryKeyID], key)
+	}
+
+	for _, key := range keys {
+		if subKeys, ok := subKeysMap[key.KeyID]; ok {
+			key.SubsKey = subKeys
+		}
+	}
+	return nil
+}
diff --git a/models/asymkey/ssh_key.go b/models/asymkey/ssh_key.go
index 116d6351b0..a409d8e841 100644
--- a/models/asymkey/ssh_key.go
+++ b/models/asymkey/ssh_key.go
@@ -197,10 +197,10 @@ func (opts FindPublicKeyOptions) ToConds() builder.Cond {
 		cond = cond.And(builder.Eq{"fingerprint": opts.Fingerprint})
 	}
 	if len(opts.KeyTypes) > 0 {
-		cond = cond.And(builder.In("type", opts.KeyTypes))
+		cond = cond.And(builder.In("`type`", opts.KeyTypes))
 	}
 	if opts.NotKeytype > 0 {
-		cond = cond.And(builder.Neq{"type": opts.NotKeytype})
+		cond = cond.And(builder.Neq{"`type`": opts.NotKeytype})
 	}
 	if opts.LoginSourceID > 0 {
 		cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID})
diff --git a/models/asymkey/ssh_key_principals.go b/models/asymkey/ssh_key_principals.go
index 92789e26f8..4e7dee2c91 100644
--- a/models/asymkey/ssh_key_principals.go
+++ b/models/asymkey/ssh_key_principals.go
@@ -15,15 +15,6 @@ import (
 	"code.gitea.io/gitea/modules/util"
 )
 
-// __________       .__              .__             .__
-// \______   _______|__| ____   ____ |_____________  |  |   ______
-//  |     ___\_  __ |  |/    \_/ ___\|  \____ \__  \ |  |  /  ___/
-//  |    |    |  | \|  |   |  \  \___|  |  |_> / __ \|  |__\___ \
-//  |____|    |__|  |__|___|  /\___  |__|   __(____  |____/____  >
-//                          \/     \/   |__|       \/          \/
-//
-// This file contains functions related to principals
-
 // AddPrincipalKey adds new principal to database and authorized_principals file.
 func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*PublicKey, error) {
 	dbCtx, committer, err := db.TxContext(ctx)
@@ -103,17 +94,3 @@ func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content
 
 	return "", fmt.Errorf("didn't match allowed principals: %s", setting.SSH.AuthorizedPrincipalsAllow)
 }
-
-// ListPrincipalKeys returns a list of principals belongs to given user.
-func ListPrincipalKeys(ctx context.Context, uid int64, listOptions db.ListOptions) ([]*PublicKey, error) {
-	sess := db.GetEngine(ctx).Where("owner_id = ? AND type = ?", uid, KeyTypePrincipal)
-	if listOptions.Page != 0 {
-		sess = db.SetSessionPagination(sess, &listOptions)
-
-		keys := make([]*PublicKey, 0, listOptions.PageSize)
-		return keys, sess.Find(&keys)
-	}
-
-	keys := make([]*PublicKey, 0, 5)
-	return keys, sess.Find(&keys)
-}
diff --git a/models/auth/webauthn.go b/models/auth/webauthn.go
index d12713bd37..a65d2e1e34 100644
--- a/models/auth/webauthn.go
+++ b/models/auth/webauthn.go
@@ -13,7 +13,6 @@ import (
 	"code.gitea.io/gitea/modules/util"
 
 	"github.com/go-webauthn/webauthn/webauthn"
-	"xorm.io/xorm"
 )
 
 // ErrWebAuthnCredentialNotExist represents a "ErrWebAuthnCRedentialNotExist" kind of error.
@@ -83,7 +82,7 @@ func (cred *WebAuthnCredential) BeforeUpdate() {
 }
 
 // AfterLoad is invoked from XORM after setting the values of all fields of this object.
-func (cred *WebAuthnCredential) AfterLoad(session *xorm.Session) {
+func (cred *WebAuthnCredential) AfterLoad() {
 	cred.LowerName = strings.ToLower(cred.Name)
 }
 
diff --git a/models/db/list.go b/models/db/list.go
index b2f932e89b..4aeaf3e084 100644
--- a/models/db/list.go
+++ b/models/db/list.go
@@ -21,17 +21,9 @@ const (
 // Paginator is the base for different ListOptions types
 type Paginator interface {
 	GetSkipTake() (skip, take int)
-	GetStartEnd() (start, end int)
 	IsListAll() bool
 }
 
-// GetPaginatedSession creates a paginated database session
-func GetPaginatedSession(p Paginator) *xorm.Session {
-	skip, take := p.GetSkipTake()
-
-	return x.Limit(take, skip)
-}
-
 // SetSessionPagination sets pagination for a database session
 func SetSessionPagination(sess Engine, p Paginator) *xorm.Session {
 	skip, take := p.GetSkipTake()
@@ -39,13 +31,6 @@ func SetSessionPagination(sess Engine, p Paginator) *xorm.Session {
 	return sess.Limit(take, skip)
 }
 
-// SetEnginePagination sets pagination for a database engine
-func SetEnginePagination(e Engine, p Paginator) Engine {
-	skip, take := p.GetSkipTake()
-
-	return e.Limit(take, skip)
-}
-
 // ListOptions options to paginate results
 type ListOptions struct {
 	PageSize int
@@ -66,13 +51,6 @@ func (opts *ListOptions) GetSkipTake() (skip, take int) {
 	return (opts.Page - 1) * opts.PageSize, opts.PageSize
 }
 
-// GetStartEnd returns the start and end of the ListOptions
-func (opts *ListOptions) GetStartEnd() (start, end int) {
-	start, take := opts.GetSkipTake()
-	end = start + take
-	return start, end
-}
-
 func (opts ListOptions) GetPage() int {
 	return opts.Page
 }
@@ -135,11 +113,6 @@ func (opts *AbsoluteListOptions) GetSkipTake() (skip, take int) {
 	return opts.skip, opts.take
 }
 
-// GetStartEnd returns the start and end values
-func (opts *AbsoluteListOptions) GetStartEnd() (start, end int) {
-	return opts.skip, opts.skip + opts.take
-}
-
 // FindOptions represents a find options
 type FindOptions interface {
 	GetPage() int
@@ -148,15 +121,34 @@ type FindOptions interface {
 	ToConds() builder.Cond
 }
 
+type JoinFunc func(sess Engine) error
+
+type FindOptionsJoin interface {
+	ToJoins() []JoinFunc
+}
+
 type FindOptionsOrder interface {
 	ToOrders() string
 }
 
 // Find represents a common find function which accept an options interface
 func Find[T any](ctx context.Context, opts FindOptions) ([]*T, error) {
-	sess := GetEngine(ctx).Where(opts.ToConds())
+	sess := GetEngine(ctx)
+
+	if joinOpt, ok := opts.(FindOptionsJoin); ok && len(joinOpt.ToJoins()) > 0 {
+		for _, joinFunc := range joinOpt.ToJoins() {
+			if err := joinFunc(sess); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	sess = sess.Where(opts.ToConds())
 	page, pageSize := opts.GetPage(), opts.GetPageSize()
-	if !opts.IsListAll() && pageSize > 0 && page >= 1 {
+	if !opts.IsListAll() && pageSize > 0 {
+		if page == 0 {
+			page = 1
+		}
 		sess.Limit(pageSize, (page-1)*pageSize)
 	}
 	if newOpt, ok := opts.(FindOptionsOrder); ok && newOpt.ToOrders() != "" {
@@ -176,8 +168,17 @@ func Find[T any](ctx context.Context, opts FindOptions) ([]*T, error) {
 
 // Count represents a common count function which accept an options interface
 func Count[T any](ctx context.Context, opts FindOptions) (int64, error) {
+	sess := GetEngine(ctx)
+	if joinOpt, ok := opts.(FindOptionsJoin); ok && len(joinOpt.ToJoins()) > 0 {
+		for _, joinFunc := range joinOpt.ToJoins() {
+			if err := joinFunc(sess); err != nil {
+				return 0, err
+			}
+		}
+	}
+
 	var object T
-	return GetEngine(ctx).Where(opts.ToConds()).Count(&object)
+	return sess.Where(opts.ToConds()).Count(&object)
 }
 
 // FindAndCount represents a common findandcount function which accept an options interface
diff --git a/models/db/paginator/paginator_test.go b/models/db/paginator/paginator_test.go
index a1117fc7a4..20602212d9 100644
--- a/models/db/paginator/paginator_test.go
+++ b/models/db/paginator/paginator_test.go
@@ -52,11 +52,8 @@ func TestPaginator(t *testing.T) {
 
 	for _, c := range cases {
 		skip, take := c.Paginator.GetSkipTake()
-		start, end := c.Paginator.GetStartEnd()
 
 		assert.Equal(t, c.Skip, skip)
 		assert.Equal(t, c.Take, take)
-		assert.Equal(t, c.Start, start)
-		assert.Equal(t, c.End, end)
 	}
 }
diff --git a/models/issues/comment.go b/models/issues/comment.go
index 7b068d4983..a1698d4824 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -28,7 +28,6 @@ import (
 	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
-	"xorm.io/xorm"
 )
 
 // ErrCommentNotExist represents a "CommentNotExist" kind of error.
@@ -338,7 +337,7 @@ func (c *Comment) BeforeUpdate() {
 }
 
 // AfterLoad is invoked from XORM after setting the values of all fields of this object.
-func (c *Comment) AfterLoad(session *xorm.Session) {
+func (c *Comment) AfterLoad() {
 	c.Patch = c.PatchQuoted
 	if len(c.PatchQuoted) > 0 && c.PatchQuoted[0] == '"' {
 		unquoted, err := strconv.Unquote(c.PatchQuoted)
diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go
index 795bddeb34..884a445d26 100644
--- a/models/issues/tracked_time.go
+++ b/models/issues/tracked_time.go
@@ -94,7 +94,7 @@ type FindTrackedTimesOptions struct {
 }
 
 // toCond will convert each condition into a xorm-Cond
-func (opts *FindTrackedTimesOptions) toCond() builder.Cond {
+func (opts *FindTrackedTimesOptions) ToConds() builder.Cond {
 	cond := builder.NewCond().And(builder.Eq{"tracked_time.deleted": false})
 	if opts.IssueID != 0 {
 		cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
@@ -117,6 +117,18 @@ func (opts *FindTrackedTimesOptions) toCond() builder.Cond {
 	return cond
 }
 
+func (opts *FindTrackedTimesOptions) ToJoins() []db.JoinFunc {
+	if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
+		return []db.JoinFunc{
+			func(e db.Engine) error {
+				e.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
+				return nil
+			},
+		}
+	}
+	return nil
+}
+
 // toSession will convert the given options to a xorm Session by using the conditions from toCond and joining with issue table if required
 func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine {
 	sess := e
@@ -124,10 +136,10 @@ func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine {
 		sess = e.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
 	}
 
-	sess = sess.Where(opts.toCond())
+	sess = sess.Where(opts.ToConds())
 
 	if opts.Page != 0 {
-		sess = db.SetEnginePagination(sess, opts)
+		sess = db.SetSessionPagination(sess, opts)
 	}
 
 	return sess
@@ -141,7 +153,7 @@ func GetTrackedTimes(ctx context.Context, options *FindTrackedTimesOptions) (tra
 
 // CountTrackedTimes returns count of tracked times that fit to the given options.
 func CountTrackedTimes(ctx context.Context, opts *FindTrackedTimesOptions) (int64, error) {
-	sess := db.GetEngine(ctx).Where(opts.toCond())
+	sess := db.GetEngine(ctx).Where(opts.ToConds())
 	if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
 		sess = sess.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
 	}
diff --git a/models/repo/archiver.go b/models/repo/archiver.go
index 1fccb29499..d9520c670c 100644
--- a/models/repo/archiver.go
+++ b/models/repo/archiver.go
@@ -111,7 +111,7 @@ type FindRepoArchiversOption struct {
 	OlderThan time.Duration
 }
 
-func (opts FindRepoArchiversOption) toConds() builder.Cond {
+func (opts FindRepoArchiversOption) ToConds() builder.Cond {
 	cond := builder.NewCond()
 	if opts.OlderThan > 0 {
 		cond = cond.And(builder.Lt{"created_unix": time.Now().Add(-opts.OlderThan).Unix()})
@@ -119,15 +119,8 @@ func (opts FindRepoArchiversOption) toConds() builder.Cond {
 	return cond
 }
 
-// FindRepoArchives find repo archivers
-func FindRepoArchives(ctx context.Context, opts FindRepoArchiversOption) ([]*RepoArchiver, error) {
-	archivers := make([]*RepoArchiver, 0, opts.PageSize)
-	start, limit := opts.GetSkipTake()
-	err := db.GetEngine(ctx).Where(opts.toConds()).
-		Asc("created_unix").
-		Limit(limit, start).
-		Find(&archivers)
-	return archivers, err
+func (opts FindRepoArchiversOption) ToOrders() string {
+	return "created_unix ASC"
 }
 
 // SetArchiveRepoState sets if a repo is archived
diff --git a/models/repo/collaboration.go b/models/repo/collaboration.go
index 2018ae2a7d..7288082614 100644
--- a/models/repo/collaboration.go
+++ b/models/repo/collaboration.go
@@ -11,8 +11,9 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
+
+	"xorm.io/builder"
 )
 
 // Collaboration represent the relation between an individual and a repository.
@@ -37,35 +38,38 @@ type Collaborator struct {
 
 // GetCollaborators returns the collaborators for a repository
 func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaborator, error) {
-	collaborations, err := getCollaborations(ctx, repoID, listOptions)
+	collaborations, err := db.Find[Collaboration](ctx, FindCollaborationOptions{
+		ListOptions: listOptions,
+		RepoID:      repoID,
+	})
 	if err != nil {
-		return nil, fmt.Errorf("getCollaborations: %w", err)
+		return nil, fmt.Errorf("db.Find[Collaboration]: %w", err)
 	}
 
 	collaborators := make([]*Collaborator, 0, len(collaborations))
+	userIDs := make([]int64, 0, len(collaborations))
 	for _, c := range collaborations {
-		user, err := user_model.GetUserByID(ctx, c.UserID)
-		if err != nil {
-			if user_model.IsErrUserNotExist(err) {
-				log.Warn("Inconsistent DB: User: %d is listed as collaborator of %-v but does not exist", c.UserID, repoID)
-				user = user_model.NewGhostUser()
-			} else {
-				return nil, err
-			}
+		userIDs = append(userIDs, c.UserID)
+	}
+
+	usersMap := make(map[int64]*user_model.User)
+	if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil {
+		return nil, fmt.Errorf("Find users map by user ids: %w", err)
+	}
+
+	for _, c := range collaborations {
+		u := usersMap[c.UserID]
+		if u == nil {
+			u = user_model.NewGhostUser()
 		}
 		collaborators = append(collaborators, &Collaborator{
-			User:          user,
+			User:          u,
 			Collaboration: c,
 		})
 	}
 	return collaborators, nil
 }
 
-// CountCollaborators returns total number of collaborators for a repository
-func CountCollaborators(ctx context.Context, repoID int64) (int64, error) {
-	return db.GetEngine(ctx).Where("repo_id = ? ", repoID).Count(&Collaboration{})
-}
-
 // GetCollaboration get collaboration for a repository id with a user id
 func GetCollaboration(ctx context.Context, repoID, uid int64) (*Collaboration, error) {
 	collaboration := &Collaboration{
@@ -84,18 +88,13 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
 	return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
 }
 
-func getCollaborations(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaboration, error) {
-	if listOptions.Page == 0 {
-		collaborations := make([]*Collaboration, 0, 8)
-		return collaborations, db.GetEngine(ctx).Find(&collaborations, &Collaboration{RepoID: repoID})
-	}
+type FindCollaborationOptions struct {
+	db.ListOptions
+	RepoID int64
+}
 
-	e := db.GetEngine(ctx)
-
-	e = db.SetEnginePagination(e, &listOptions)
-
-	collaborations := make([]*Collaboration, 0, listOptions.PageSize)
-	return collaborations, e.Find(&collaborations, &Collaboration{RepoID: repoID})
+func (opts FindCollaborationOptions) ToConds() builder.Cond {
+	return builder.And(builder.Eq{"repo_id": opts.RepoID})
 }
 
 // ChangeCollaborationAccessMode sets new access mode for the collaboration.
diff --git a/models/repo/collaboration_test.go b/models/repo/collaboration_test.go
index 38114c307f..21a99dd557 100644
--- a/models/repo/collaboration_test.go
+++ b/models/repo/collaboration_test.go
@@ -89,17 +89,23 @@ func TestRepository_CountCollaborators(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
-	count, err := repo_model.CountCollaborators(db.DefaultContext, repo1.ID)
+	count, err := db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
+		RepoID: repo1.ID,
+	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 2, count)
 
 	repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
-	count, err = repo_model.CountCollaborators(db.DefaultContext, repo2.ID)
+	count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
+		RepoID: repo2.ID,
+	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 2, count)
 
 	// Non-existent repository.
-	count, err = repo_model.CountCollaborators(db.DefaultContext, unittest.NonexistentID)
+	count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
+		RepoID: unittest.NonexistentID,
+	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, count)
 }
diff --git a/models/repo/fork.go b/models/repo/fork.go
index 6be6ebc3f5..07cd31c269 100644
--- a/models/repo/fork.go
+++ b/models/repo/fork.go
@@ -56,13 +56,16 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error)
 
 // GetForks returns all the forks of the repository
 func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions) ([]*Repository, error) {
+	sess := db.GetEngine(ctx)
+
+	var forks []*Repository
 	if listOptions.Page == 0 {
-		forks := make([]*Repository, 0, repo.NumForks)
-		return forks, db.GetEngine(ctx).Find(&forks, &Repository{ForkID: repo.ID})
+		forks = make([]*Repository, 0, repo.NumForks)
+	} else {
+		forks = make([]*Repository, 0, listOptions.PageSize)
+		sess = db.SetSessionPagination(sess, &listOptions)
 	}
 
-	sess := db.GetPaginatedSession(&listOptions)
-	forks := make([]*Repository, 0, listOptions.PageSize)
 	return forks, sess.Find(&forks, &Repository{ForkID: repo.ID})
 }
 
diff --git a/models/repo/release.go b/models/repo/release.go
index 4514a034ed..72a73f8e80 100644
--- a/models/repo/release.go
+++ b/models/repo/release.go
@@ -225,6 +225,7 @@ func GetReleaseForRepoByID(ctx context.Context, repoID, id int64) (*Release, err
 // FindReleasesOptions describes the conditions to Find releases
 type FindReleasesOptions struct {
 	db.ListOptions
+	RepoID        int64
 	IncludeDrafts bool
 	IncludeTags   bool
 	IsPreRelease  util.OptionalBool
@@ -233,9 +234,8 @@ type FindReleasesOptions struct {
 	HasSha1       util.OptionalBool // useful to find draft releases which are created with existing tags
 }
 
-func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond {
-	cond := builder.NewCond()
-	cond = cond.And(builder.Eq{"repo_id": repoID})
+func (opts FindReleasesOptions) ToConds() builder.Cond {
+	var cond builder.Cond = builder.Eq{"repo_id": opts.RepoID}
 
 	if !opts.IncludeDrafts {
 		cond = cond.And(builder.Eq{"is_draft": false})
@@ -262,18 +262,8 @@ func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond {
 	return cond
 }
 
-// GetReleasesByRepoID returns a list of releases of repository.
-func GetReleasesByRepoID(ctx context.Context, repoID int64, opts FindReleasesOptions) ([]*Release, error) {
-	sess := db.GetEngine(ctx).
-		Desc("created_unix", "id").
-		Where(opts.toConds(repoID))
-
-	if opts.PageSize != 0 {
-		sess = db.SetSessionPagination(sess, &opts.ListOptions)
-	}
-
-	rels := make([]*Release, 0, opts.PageSize)
-	return rels, sess.Find(&rels)
+func (opts FindReleasesOptions) ToOrders() string {
+	return "created_unix DESC, id DESC"
 }
 
 // GetTagNamesByRepoID returns a list of release tag names of repository.
@@ -286,23 +276,19 @@ func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) {
 		IncludeDrafts: true,
 		IncludeTags:   true,
 		HasSha1:       util.OptionalBoolTrue,
+		RepoID:        repoID,
 	}
 
 	tags := make([]string, 0)
 	sess := db.GetEngine(ctx).
 		Table("release").
 		Desc("created_unix", "id").
-		Where(opts.toConds(repoID)).
+		Where(opts.ToConds()).
 		Cols("tag_name")
 
 	return tags, sess.Find(&tags)
 }
 
-// CountReleasesByRepoID returns a number of releases matching FindReleaseOptions and RepoID.
-func CountReleasesByRepoID(ctx context.Context, repoID int64, opts FindReleasesOptions) (int64, error) {
-	return db.GetEngine(ctx).Where(opts.toConds(repoID)).Count(new(Release))
-}
-
 // GetLatestReleaseByRepoID returns the latest release for a repository
 func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, error) {
 	cond := builder.NewCond().
@@ -325,20 +311,6 @@ func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, erro
 	return rel, nil
 }
 
-// GetReleasesByRepoIDAndNames returns a list of releases of repository according repoID and tagNames.
-func GetReleasesByRepoIDAndNames(ctx context.Context, repoID int64, tagNames []string) (rels []*Release, err error) {
-	err = db.GetEngine(ctx).
-		In("tag_name", tagNames).
-		Desc("created_unix").
-		Find(&rels, Release{RepoID: repoID})
-	return rels, err
-}
-
-// GetReleaseCountByRepoID returns the count of releases of repository
-func GetReleaseCountByRepoID(ctx context.Context, repoID int64, opts FindReleasesOptions) (int64, error) {
-	return db.GetEngine(ctx).Where(opts.toConds(repoID)).Count(&Release{})
-}
-
 type releaseMetaSearch struct {
 	ID  []int64
 	Rel []*Release
diff --git a/modules/context/repo.go b/modules/context/repo.go
index 882a406731..8d82be1990 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -536,18 +536,20 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 		ctx.Data["RepoExternalIssuesLink"] = unit.ExternalTrackerConfig().ExternalTrackerURL
 	}
 
-	ctx.Data["NumTags"], err = repo_model.GetReleaseCountByRepoID(ctx, ctx.Repo.Repository.ID, repo_model.FindReleasesOptions{
+	ctx.Data["NumTags"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
 		IncludeDrafts: true,
 		IncludeTags:   true,
 		HasSha1:       util.OptionalBoolTrue, // only draft releases which are created with existing tags
+		RepoID:        ctx.Repo.Repository.ID,
 	})
 	if err != nil {
 		ctx.ServerError("GetReleaseCountByRepoID", err)
 		return nil
 	}
-	ctx.Data["NumReleases"], err = repo_model.GetReleaseCountByRepoID(ctx, ctx.Repo.Repository.ID, repo_model.FindReleasesOptions{
+	ctx.Data["NumReleases"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
 		// only show draft releases for users who can write, read-only users shouldn't see draft releases.
 		IncludeDrafts: ctx.Repo.CanWrite(unit_model.TypeReleases),
+		RepoID:        ctx.Repo.Repository.ID,
 	})
 	if err != nil {
 		ctx.ServerError("GetReleaseCountByRepoID", err)
diff --git a/modules/repository/repo.go b/modules/repository/repo.go
index a9a2773501..33363e4689 100644
--- a/modules/repository/repo.go
+++ b/modules/repository/repo.go
@@ -299,10 +299,11 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR
 		IncludeDrafts: true,
 		IncludeTags:   true,
 		ListOptions:   db.ListOptions{PageSize: 50},
+		RepoID:        repo.ID,
 	}
 	for page := 1; ; page++ {
 		opts.Page = page
-		rels, err := repo_model.GetReleasesByRepoID(gitRepo.Ctx, repo.ID, opts)
+		rels, err := db.Find[repo_model.Release](gitRepo.Ctx, opts)
 		if err != nil {
 			return fmt.Errorf("unable to GetReleasesByRepoID in Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
 		}
diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go
index 2538bcdbc6..a222e50a5e 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -8,6 +8,7 @@ import (
 	"errors"
 	"net/http"
 
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -53,7 +54,9 @@ func ListCollaborators(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	count, err := repo_model.CountCollaborators(ctx, ctx.Repo.Repository.ID)
+	count, err := db.Count[repo_model.Collaboration](ctx, repo_model.FindCollaborationOptions{
+		RepoID: ctx.Repo.Repository.ID,
+	})
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index d6b9dddd9d..34129ad595 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -1329,17 +1329,16 @@ func GetPullRequestCommits(ctx *context.APIContext) {
 
 	userCache := make(map[string]*user_model.User)
 
-	start, end := listOptions.GetStartEnd()
+	start, limit := listOptions.GetSkipTake()
 
-	if end > totalNumberOfCommits {
-		end = totalNumberOfCommits
-	}
+	limit = min(limit, totalNumberOfCommits-start)
+	limit = max(limit, 0)
 
 	verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
 	files := ctx.FormString("files") == "" || ctx.FormBool("files")
 
-	apiCommits := make([]*api.Commit, 0, end-start)
-	for i := start; i < end; i++ {
+	apiCommits := make([]*api.Commit, 0, limit)
+	for i := start; i < start+limit; i++ {
 		apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, baseGitRepo, commits[i], userCache,
 			convert.ToCommitOptions{
 				Stat:         true,
@@ -1477,19 +1476,14 @@ func GetPullRequestFiles(ctx *context.APIContext) {
 	totalNumberOfFiles := diff.NumFiles
 	totalNumberOfPages := int(math.Ceil(float64(totalNumberOfFiles) / float64(listOptions.PageSize)))
 
-	start, end := listOptions.GetStartEnd()
+	start, limit := listOptions.GetSkipTake()
 
-	if end > totalNumberOfFiles {
-		end = totalNumberOfFiles
-	}
+	limit = min(limit, totalNumberOfFiles-start)
 
-	lenFiles := end - start
-	if lenFiles < 0 {
-		lenFiles = 0
-	}
+	limit = max(limit, 0)
 
-	apiFiles := make([]*api.ChangedFile, 0, lenFiles)
-	for i := start; i < end; i++ {
+	apiFiles := make([]*api.ChangedFile, 0, limit)
+	for i := start; i < start+limit; i++ {
 		apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID))
 	}
 
diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go
index b1d3b5f457..a41c5ba7d8 100644
--- a/routers/api/v1/repo/release.go
+++ b/routers/api/v1/repo/release.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
@@ -154,9 +155,10 @@ func ListReleases(ctx *context.APIContext) {
 		IncludeTags:   false,
 		IsDraft:       ctx.FormOptionalBool("draft"),
 		IsPreRelease:  ctx.FormOptionalBool("pre-release"),
+		RepoID:        ctx.Repo.Repository.ID,
 	}
 
-	releases, err := repo_model.GetReleasesByRepoID(ctx, ctx.Repo.Repository.ID, opts)
+	releases, err := db.Find[repo_model.Release](ctx, opts)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetReleasesByRepoID", err)
 		return
@@ -170,7 +172,7 @@ func ListReleases(ctx *context.APIContext) {
 		rels[i] = convert.ToAPIRelease(ctx, ctx.Repo.Repository, release)
 	}
 
-	filteredCount, err := repo_model.CountReleasesByRepoID(ctx, ctx.Repo.Repository.ID, opts)
+	filteredCount, err := db.Count[repo_model.Release](ctx, opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go
index 4f8bcaca3e..234da5dfdc 100644
--- a/routers/api/v1/user/gpg_key.go
+++ b/routers/api/v1/user/gpg_key.go
@@ -18,23 +18,25 @@ import (
 )
 
 func listGPGKeys(ctx *context.APIContext, uid int64, listOptions db.ListOptions) {
-	keys, err := asymkey_model.ListGPGKeys(ctx, uid, listOptions)
+	keys, total, err := db.FindAndCount[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+		ListOptions: listOptions,
+		OwnerID:     uid,
+	})
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "ListGPGKeys", err)
 		return
 	}
 
+	if err := asymkey_model.GPGKeyList(keys).LoadSubKeys(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "ListGPGKeys", err)
+		return
+	}
+
 	apiKeys := make([]*api.GPGKey, len(keys))
 	for i := range keys {
 		apiKeys[i] = convert.ToGPGKey(keys[i])
 	}
 
-	total, err := asymkey_model.CountUserGPGKeys(ctx, uid)
-	if err != nil {
-		ctx.InternalServerError(err)
-		return
-	}
-
 	ctx.SetTotalCountHeader(total)
 	ctx.JSON(http.StatusOK, &apiKeys)
 }
@@ -121,6 +123,10 @@ func GetGPGKey(ctx *context.APIContext) {
 		}
 		return
 	}
+	if err := key.LoadSubKeys(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadSubKeys", err)
+		return
+	}
 	ctx.JSON(http.StatusOK, convert.ToGPGKey(key))
 }
 
@@ -198,7 +204,10 @@ func VerifyUserGPGKey(ctx *context.APIContext) {
 		ctx.Error(http.StatusInternalServerError, "VerifyUserGPGKey", err)
 	}
 
-	key, err := asymkey_model.GetGPGKeysByKeyID(ctx, form.KeyID)
+	keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+		KeyID:          form.KeyID,
+		IncludeSubKeys: true,
+	})
 	if err != nil {
 		if asymkey_model.IsErrGPGKeyNotExist(err) {
 			ctx.NotFound()
@@ -207,7 +216,7 @@ func VerifyUserGPGKey(ctx *context.APIContext) {
 		}
 		return
 	}
-	ctx.JSON(http.StatusOK, convert.ToGPGKey(key[0]))
+	ctx.JSON(http.StatusOK, convert.ToGPGKey(keys[0]))
 }
 
 // swagger:parameters userCurrentPostGPGKey
diff --git a/routers/web/feed/release.go b/routers/web/feed/release.go
index fbfa11c63e..57b0c92766 100644
--- a/routers/web/feed/release.go
+++ b/routers/web/feed/release.go
@@ -6,6 +6,7 @@ package feed
 import (
 	"time"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/context"
 
@@ -14,8 +15,9 @@ import (
 
 // shows tags and/or releases on the repo as RSS / Atom feed
 func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleasesOnly bool, formatType string) {
-	releases, err := repo_model.GetReleasesByRepoID(ctx, ctx.Repo.Repository.ID, repo_model.FindReleasesOptions{
+	releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
 		IncludeTags: !isReleasesOnly,
+		RepoID:      ctx.Repo.Repository.ID,
 	})
 	if err != nil {
 		ctx.ServerError("GetReleasesByRepoID", err)
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 595d599fe1..86cb93e49b 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -95,9 +95,10 @@ func Releases(ctx *context.Context) {
 		ListOptions: listOptions,
 		// only show draft releases for users who can write, read-only users shouldn't see draft releases.
 		IncludeDrafts: writeAccess,
+		RepoID:        ctx.Repo.Repository.ID,
 	}
 
-	releases, err := repo_model.GetReleasesByRepoID(ctx, ctx.Repo.Repository.ID, opts)
+	releases, err := db.Find[repo_model.Release](ctx, opts)
 	if err != nil {
 		ctx.ServerError("GetReleasesByRepoID", err)
 		return
@@ -194,9 +195,10 @@ func TagsList(ctx *context.Context) {
 		IncludeDrafts: true,
 		IncludeTags:   true,
 		HasSha1:       util.OptionalBoolTrue,
+		RepoID:        ctx.Repo.Repository.ID,
 	}
 
-	releases, err := repo_model.GetReleasesByRepoID(ctx, ctx.Repo.Repository.ID, opts)
+	releases, err := db.Find[repo_model.Release](ctx, opts)
 	if err != nil {
 		ctx.ServerError("GetReleasesByRepoID", err)
 		return
diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go
index a5a923d464..c4a2c1904e 100644
--- a/routers/web/repo/release_test.go
+++ b/routers/web/repo/release_test.go
@@ -6,6 +6,7 @@ package repo
 import (
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/models/unittest"
@@ -74,8 +75,9 @@ func TestCalReleaseNumCommitsBehind(t *testing.T) {
 	contexttest.LoadGitRepo(t, ctx)
 	t.Cleanup(func() { ctx.Repo.GitRepo.Close() })
 
-	releases, err := repo_model.GetReleasesByRepoID(ctx, ctx.Repo.Repository.ID, repo_model.FindReleasesOptions{
+	releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
 		IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases),
+		RepoID:        ctx.Repo.Repository.ID,
 	})
 	assert.NoError(t, err)
 
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index b16e283836..cd5eac057e 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -379,7 +379,10 @@ func RedirectDownload(ctx *context.Context) {
 	)
 	tagNames := []string{vTag}
 	curRepo := ctx.Repo.Repository
-	releases, err := repo_model.GetReleasesByRepoIDAndNames(ctx, curRepo.ID, tagNames)
+	releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
+		RepoID:   curRepo.ID,
+		TagNames: tagNames,
+	})
 	if err != nil {
 		ctx.ServerError("RedirectDownload", err)
 		return
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 204d4adbd4..debbb9753c 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -663,7 +663,10 @@ func ShowSSHKeys(ctx *context.Context) {
 
 // ShowGPGKeys output all the public GPG keys of user by uid
 func ShowGPGKeys(ctx *context.Context) {
-	keys, err := asymkey_model.ListGPGKeys(ctx, ctx.ContextUser.ID, db.ListOptions{})
+	keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+		ListOptions: db.ListOptionsAll,
+		OwnerID:     ctx.ContextUser.ID,
+	})
 	if err != nil {
 		ctx.ServerError("ListGPGKeys", err)
 		return
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index 0dfb506fa9..16410d06ff 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -277,18 +277,29 @@ func loadKeysData(ctx *context.Context) {
 	}
 	ctx.Data["ExternalKeys"] = externalKeys
 
-	gpgkeys, err := asymkey_model.ListGPGKeys(ctx, ctx.Doer.ID, db.ListOptions{})
+	gpgkeys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+		ListOptions: db.ListOptionsAll,
+		OwnerID:     ctx.Doer.ID,
+	})
 	if err != nil {
 		ctx.ServerError("ListGPGKeys", err)
 		return
 	}
+	if err := asymkey_model.GPGKeyList(gpgkeys).LoadSubKeys(ctx); err != nil {
+		ctx.ServerError("LoadSubKeys", err)
+		return
+	}
 	ctx.Data["GPGKeys"] = gpgkeys
 	tokenToSign := asymkey_model.VerificationToken(ctx.Doer, 1)
 
 	// generate a new aes cipher using the csrfToken
 	ctx.Data["TokenToSign"] = tokenToSign
 
-	principals, err := asymkey_model.ListPrincipalKeys(ctx, ctx.Doer.ID, db.ListOptions{})
+	principals, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
+		ListOptions: db.ListOptionsAll,
+		OwnerID:     ctx.Doer.ID,
+		KeyTypes:    []asymkey_model.KeyType{asymkey_model.KeyTypePrincipal},
+	})
 	if err != nil {
 		ctx.ServerError("ListPrincipalKeys", err)
 		return
diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go
index 1598f32165..0c4aac8156 100644
--- a/services/asymkey/sign.go
+++ b/services/asymkey/sign.go
@@ -143,7 +143,10 @@ Loop:
 		case always:
 			break Loop
 		case pubkey:
-			keys, err := asymkey_model.ListGPGKeys(ctx, u.ID, db.ListOptions{})
+			keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+				OwnerID:        u.ID,
+				IncludeSubKeys: true,
+			})
 			if err != nil {
 				return false, "", nil, err
 			}
@@ -179,7 +182,10 @@ Loop:
 		case always:
 			break Loop
 		case pubkey:
-			keys, err := asymkey_model.ListGPGKeys(ctx, u.ID, db.ListOptions{})
+			keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+				OwnerID:        u.ID,
+				IncludeSubKeys: true,
+			})
 			if err != nil {
 				return false, "", nil, err
 			}
@@ -232,7 +238,10 @@ Loop:
 		case always:
 			break Loop
 		case pubkey:
-			keys, err := asymkey_model.ListGPGKeys(ctx, u.ID, db.ListOptions{})
+			keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+				OwnerID:        u.ID,
+				IncludeSubKeys: true,
+			})
 			if err != nil {
 				return false, "", nil, err
 			}
@@ -294,7 +303,10 @@ Loop:
 		case always:
 			break Loop
 		case pubkey:
-			keys, err := asymkey_model.ListGPGKeys(ctx, u.ID, db.ListOptions{})
+			keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+				OwnerID:        u.ID,
+				IncludeSubKeys: true,
+			})
 			if err != nil {
 				return false, "", nil, err
 			}
diff --git a/services/convert/repository.go b/services/convert/repository.go
index 71038cd062..c16180c0af 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -8,6 +8,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -133,7 +134,11 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		return nil
 	}
 
-	numReleases, _ := repo_model.GetReleaseCountByRepoID(ctx, repo.ID, repo_model.FindReleasesOptions{IncludeDrafts: false, IncludeTags: false})
+	numReleases, _ := db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
+		IncludeDrafts: false,
+		IncludeTags:   false,
+		RepoID:        repo.ID,
+	})
 
 	mirrorInterval := ""
 	var mirrorUpdated time.Time
diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go
index 3dec3a26fc..4ec0361dfb 100644
--- a/services/migrations/gitea_uploader_test.go
+++ b/services/migrations/gitea_uploader_test.go
@@ -83,22 +83,24 @@ func TestGiteaUploadRepo(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, labels, 12)
 
-	releases, err := repo_model.GetReleasesByRepoID(db.DefaultContext, repo.ID, repo_model.FindReleasesOptions{
+	releases, err := db.Find[repo_model.Release](db.DefaultContext, repo_model.FindReleasesOptions{
 		ListOptions: db.ListOptions{
 			PageSize: 10,
 			Page:     0,
 		},
 		IncludeTags: true,
+		RepoID:      repo.ID,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, releases, 8)
 
-	releases, err = repo_model.GetReleasesByRepoID(db.DefaultContext, repo.ID, repo_model.FindReleasesOptions{
+	releases, err = db.Find[repo_model.Release](db.DefaultContext, repo_model.FindReleasesOptions{
 		ListOptions: db.ListOptions{
 			PageSize: 10,
 			Page:     0,
 		},
 		IncludeTags: false,
+		RepoID:      repo.ID,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, releases, 1)
diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go
index b73d0eed48..c2ad4d484a 100644
--- a/services/repository/archiver/archiver.go
+++ b/services/repository/archiver/archiver.go
@@ -324,7 +324,7 @@ func DeleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration) e
 	log.Trace("Doing: ArchiveCleanup")
 
 	for {
-		archivers, err := repo_model.FindRepoArchives(ctx, repo_model.FindRepoArchiversOption{
+		archivers, err := db.Find[repo_model.RepoArchiver](ctx, repo_model.FindRepoArchiversOption{
 			ListOptions: db.ListOptions{
 				PageSize: 100,
 				Page:     1,
diff --git a/services/repository/push.go b/services/repository/push.go
index 3bc7a78cb9..e86eebde81 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -327,9 +327,12 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
 		lowerTags = append(lowerTags, strings.ToLower(tag))
 	}
 
-	releases, err := repo_model.GetReleasesByRepoIDAndNames(ctx, repo.ID, lowerTags)
+	releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
+		RepoID:   repo.ID,
+		TagNames: lowerTags,
+	})
 	if err != nil {
-		return fmt.Errorf("GetReleasesByRepoIDAndNames: %w", err)
+		return fmt.Errorf("db.Find[repo_model.Release]: %w", err)
 	}
 	relMap := make(map[string]*repo_model.Release)
 	for _, rel := range releases {
diff --git a/services/user/delete.go b/services/user/delete.go
index 748f5c2d1e..0e9c866171 100644
--- a/services/user/delete.go
+++ b/services/user/delete.go
@@ -159,7 +159,9 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
 	// ***** END: PublicKey *****
 
 	// ***** START: GPGPublicKey *****
-	keys, err := asymkey_model.ListGPGKeys(ctx, u.ID, db.ListOptions{})
+	keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+		OwnerID: u.ID,
+	})
 	if err != nil {
 		return fmt.Errorf("ListGPGKeys: %w", err)
 	}
diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go
index c02e16bfc0..1e0edd9a2d 100644
--- a/tests/integration/mirror_pull_test.go
+++ b/tests/integration/mirror_pull_test.go
@@ -58,8 +58,12 @@ func TestMirrorPull(t *testing.T) {
 	assert.NoError(t, err)
 	defer gitRepo.Close()
 
-	findOptions := repo_model.FindReleasesOptions{IncludeDrafts: true, IncludeTags: true}
-	initCount, err := repo_model.GetReleaseCountByRepoID(db.DefaultContext, mirror.ID, findOptions)
+	findOptions := repo_model.FindReleasesOptions{
+		IncludeDrafts: true,
+		IncludeTags:   true,
+		RepoID:        mirror.ID,
+	}
+	initCount, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
 	assert.NoError(t, err)
 
 	assert.NoError(t, release_service.CreateRelease(gitRepo, &repo_model.Release{
@@ -82,7 +86,7 @@ func TestMirrorPull(t *testing.T) {
 	ok := mirror_service.SyncPullMirror(ctx, mirror.ID)
 	assert.True(t, ok)
 
-	count, err := repo_model.GetReleaseCountByRepoID(db.DefaultContext, mirror.ID, findOptions)
+	count, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
 	assert.NoError(t, err)
 	assert.EqualValues(t, initCount+1, count)
 
@@ -93,7 +97,7 @@ func TestMirrorPull(t *testing.T) {
 	ok = mirror_service.SyncPullMirror(ctx, mirror.ID)
 	assert.True(t, ok)
 
-	count, err = repo_model.GetReleaseCountByRepoID(db.DefaultContext, mirror.ID, findOptions)
+	count, err = db.Count[repo_model.Release](db.DefaultContext, findOptions)
 	assert.NoError(t, err)
 	assert.EqualValues(t, initCount, count)
 }
diff --git a/tests/integration/repo_tag_test.go b/tests/integration/repo_tag_test.go
index e588097994..7e77906473 100644
--- a/tests/integration/repo_tag_test.go
+++ b/tests/integration/repo_tag_test.go
@@ -74,9 +74,10 @@ func TestCreateNewTagProtected(t *testing.T) {
 	})
 
 	// Cleanup
-	releases, err := repo_model.GetReleasesByRepoID(db.DefaultContext, repo.ID, repo_model.FindReleasesOptions{
+	releases, err := db.Find[repo_model.Release](db.DefaultContext, repo_model.FindReleasesOptions{
 		IncludeTags: true,
 		TagNames:    []string{"v-1", "v-1.1"},
+		RepoID:      repo.ID,
 	})
 	assert.NoError(t, err)