Add paging headers (#36521)

Adds support for paging in admin/hooks api endpoint

fixes: https://github.com/go-gitea/gitea/issues/36516

---------

Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-authored-by: techknowlogick <matti@mdranta.net>
This commit is contained in:
TheFox0x7
2026-02-06 14:12:05 +01:00
committed by GitHub
parent ef9c19691d
commit 403a73dca0
23 changed files with 123 additions and 72 deletions
+8 -1
View File
@@ -57,8 +57,13 @@ func ListHooks(ctx *context.APIContext) {
case "all":
isSystemWebhook = optional.None[bool]()
}
listOptions := utils.GetListOptions(ctx)
opts := &webhook.ListSystemWebhookOptions{
ListOptions: listOptions,
IsSystem: isSystemWebhook,
}
sysHooks, err := webhook.GetSystemOrDefaultWebhooks(ctx, isSystemWebhook)
sysHooks, total, err := webhook.GetGlobalWebhooks(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -72,6 +77,8 @@ func ListHooks(ctx *context.APIContext) {
}
hooks[i] = h
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, hooks)
}
+1 -1
View File
@@ -125,8 +125,8 @@ func ListRepoNotifications(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(totalCount), opts.PageSize)
ctx.SetTotalCountHeader(totalCount)
ctx.JSON(http.StatusOK, convert.ToNotifications(ctx, nl))
}
+1
View File
@@ -86,6 +86,7 @@ func ListNotifications(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(totalCount), opts.PageSize)
ctx.SetTotalCountHeader(totalCount)
ctx.JSON(http.StatusOK, convert.ToNotifications(ctx, nl))
}
+4 -2
View File
@@ -67,6 +67,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(count), opts.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
@@ -240,9 +241,10 @@ func (Action) ListVariables(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
OwnerID: ctx.Org.Organization.ID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
@@ -259,7 +261,7 @@ func (Action) ListVariables(ctx *context.APIContext) {
Description: v.Description,
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
+3 -1
View File
@@ -20,11 +20,12 @@ import (
// listMembers list an organization's members
func listMembers(ctx *context.APIContext, isMember bool) {
listOptions := utils.GetListOptions(ctx)
opts := &organization.FindOrgMembersOpts{
Doer: ctx.Doer,
IsDoerMember: isMember,
OrgID: ctx.Org.Organization.ID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
}
count, err := organization.CountOrgMembers(ctx, opts)
@@ -44,6 +45,7 @@ func listMembers(ctx *context.APIContext, isMember bool) {
apiMembers[i] = convert.ToUser(ctx, member, ctx.Doer)
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiMembers)
}
+13 -5
View File
@@ -54,8 +54,9 @@ func ListTeams(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
teams, count, err := organization.SearchTeam(ctx, &organization.SearchTeamOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
OrgID: ctx.Org.Organization.ID,
})
if err != nil {
@@ -69,6 +70,7 @@ func ListTeams(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiTeams)
}
@@ -93,8 +95,9 @@ func ListUserTeams(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/TeamList"
listOptions := utils.GetListOptions(ctx)
teams, count, err := organization.SearchTeam(ctx, &organization.SearchTeamOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
UserID: ctx.Doer.ID,
})
if err != nil {
@@ -108,6 +111,7 @@ func ListUserTeams(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiTeams)
}
@@ -392,8 +396,9 @@ func GetTeamMembers(ctx *context.APIContext) {
return
}
listOptions := utils.GetListOptions(ctx)
teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
TeamID: ctx.Org.Team.ID,
})
if err != nil {
@@ -406,6 +411,7 @@ func GetTeamMembers(ctx *context.APIContext) {
members[i] = convert.ToUser(ctx, member, ctx.Doer)
}
ctx.SetLinkHeader(ctx.Org.Team.NumMembers, listOptions.PageSize)
ctx.SetTotalCountHeader(int64(ctx.Org.Team.NumMembers))
ctx.JSON(http.StatusOK, members)
}
@@ -559,8 +565,9 @@ func GetTeamRepos(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
team := ctx.Org.Team
listOptions := utils.GetListOptions(ctx)
teamRepos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
TeamID: team.ID,
})
if err != nil {
@@ -576,6 +583,7 @@ func GetTeamRepos(ctx *context.APIContext) {
}
repos[i] = convert.ToRepo(ctx, repo, permission)
}
ctx.SetLinkHeader(team.NumRepos, listOptions.PageSize)
ctx.SetTotalCountHeader(int64(team.NumRepos))
ctx.JSON(http.StatusOK, repos)
}
@@ -874,7 +882,7 @@ func ListTeamActivityFeeds(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}
+11 -4
View File
@@ -69,10 +69,11 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
listOptions := utils.GetListOptions(ctx)
opts := &secret_model.FindSecretsOptions{
RepoID: repo.ID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
}
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
@@ -89,7 +90,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
Created: v.CreatedUnix.AsTime(),
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
@@ -482,9 +483,11 @@ func (Action) ListVariables(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
RepoID: ctx.Repo.Repository.ID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
@@ -502,6 +505,7 @@ func (Action) ListVariables(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
@@ -807,9 +811,10 @@ func ListActionTasks(ctx *context.APIContext) {
// "$ref": "#/responses/conflict"
// "422":
// "$ref": "#/responses/validationError"
listOptions := utils.GetListOptions(ctx)
tasks, total, err := db.FindAndCount[actions_model.ActionTask](ctx, &actions_model.FindTaskOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
@@ -830,6 +835,8 @@ func ListActionTasks(ctx *context.APIContext) {
res.Entries[i] = convertedTask
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetTotalCountHeader(total) // Duplicates api response field but it's better to set it for consistency
ctx.JSON(http.StatusOK, &res)
}
+6 -14
View File
@@ -7,13 +7,13 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -77,23 +77,14 @@ func GetIssueDependencies(ctx *context.APIContext) {
return
}
page := max(ctx.FormInt("page"), 1)
limit := ctx.FormInt("limit")
if limit == 0 {
limit = setting.API.DefaultPagingNum
} else if limit > setting.API.MaxResponseItems {
limit = setting.API.MaxResponseItems
}
listOptions := utils.GetListOptions(ctx)
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
blockerIssues := make([]*issues_model.Issue, 0, limit)
blockerIssues := make([]*issues_model.Issue, 0, listOptions.PageSize)
// 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
blockersInfo, err := issue.BlockedByDependencies(ctx, db.ListOptions{
Page: page,
PageSize: limit,
})
blockersInfo, total, err := issue.BlockedByDependencies(ctx, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -149,7 +140,8 @@ func GetIssueDependencies(ctx *context.APIContext) {
}
blockerIssues = append(blockerIssues, &blocker.Issue)
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, blockerIssues))
}
+3 -2
View File
@@ -257,8 +257,8 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) {
}
repo := ctx.Repo.Repository
statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String(), utils.GetListOptions(ctx))
listOptions := utils.GetListOptions(ctx)
statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String(), listOptions)
if err != nil {
ctx.APIErrorInternal(fmt.Errorf("GetLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err))
return
@@ -269,6 +269,7 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) {
ctx.APIErrorInternal(fmt.Errorf("CountLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err))
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
combiStatus := convert.ToCombinedStatus(ctx, refCommit.Commit.ID.String(), statuses,
+2
View File
@@ -333,6 +333,7 @@ func ListWikiPages(ctx *context.APIContext) {
pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
}
ctx.SetLinkHeader(len(entries), limit)
ctx.SetTotalCountHeader(int64(len(entries)))
ctx.JSON(http.StatusOK, pages)
}
@@ -445,6 +446,7 @@ func ListPageRevisions(ctx *context.APIContext) {
return
}
// FIXME: SetLinkHeader missing
ctx.SetTotalCountHeader(commitsCount)
ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount))
}
+8 -4
View File
@@ -32,11 +32,12 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
listOptions := utils.GetListOptions(ctx)
opts := actions_model.FindRunJobOptions{
OwnerID: ownerID,
RepoID: repoID,
RunID: runID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
}
for _, status := range ctx.FormStrings("status") {
values, err := convertToInternal(status)
@@ -78,7 +79,8 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) {
}
res.Entries[i] = convertedWorkflowJob
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &res)
}
@@ -120,10 +122,11 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
listOptions := utils.GetListOptions(ctx)
opts := actions_model.FindRunOptions{
OwnerID: ownerID,
RepoID: repoID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
}
if event := ctx.FormString("event"); event != "" {
@@ -182,6 +185,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
}
res.Entries[i] = convertedRun
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &res)
}
+3 -1
View File
@@ -16,8 +16,9 @@ import (
)
func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
listOptions := utils.GetListOptions(ctx)
blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
BlockerID: blocker.ID,
})
if err != nil {
@@ -35,6 +36,7 @@ func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &users)
}
+3 -2
View File
@@ -333,10 +333,10 @@ func ListVariables(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
OwnerID: ctx.Doer.ID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
@@ -354,6 +354,7 @@ func ListVariables(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
+6 -2
View File
@@ -24,12 +24,14 @@ func responseAPIUsers(ctx *context.APIContext, users []*user_model.User) {
}
func listUserFollowers(ctx *context.APIContext, u *user_model.User) {
users, count, err := user_model.GetUserFollowers(ctx, u, ctx.Doer, utils.GetListOptions(ctx))
listOptions := utils.GetListOptions(ctx)
users, count, err := user_model.GetUserFollowers(ctx, u, ctx.Doer, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
responseAPIUsers(ctx, users)
}
@@ -88,12 +90,14 @@ func ListFollowers(ctx *context.APIContext) {
}
func listUserFollowing(ctx *context.APIContext, u *user_model.User) {
users, count, err := user_model.GetUserFollowing(ctx, u, ctx.Doer, utils.GetListOptions(ctx))
listOptions := utils.GetListOptions(ctx)
users, count, err := user_model.GetUserFollowing(ctx, u, ctx.Doer, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
responseAPIUsers(ctx, users)
}
+9 -9
View File
@@ -1,4 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
@@ -53,11 +54,11 @@ func composePublicKeysAPILink() string {
func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
var keys []*asymkey_model.PublicKey
var err error
var count int
var count int64
fingerprint := ctx.FormString("fingerprint")
username := ctx.PathParam("username")
listOptions := utils.GetListOptions(ctx)
if fingerprint != "" {
var userID int64 // Unrestricted
// Querying not just listing
@@ -65,20 +66,18 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
// Restrict to provided uid
userID = user.ID
}
keys, err = db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
keys, count, err = db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
ListOptions: listOptions,
OwnerID: userID,
Fingerprint: fingerprint,
})
count = len(keys)
} else {
var total int64
// Use ListPublicKeys
keys, total, err = db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
ListOptions: utils.GetListOptions(ctx),
keys, count, err = db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
ListOptions: listOptions,
OwnerID: user.ID,
NotKeytype: asymkey_model.KeyTypePrincipal,
})
count = int(total)
}
if err != nil {
@@ -95,7 +94,8 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
}
}
ctx.SetTotalCountHeader(int64(count))
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, &apiKeys)
}
+2
View File
@@ -76,6 +76,7 @@ func GetStarredRepos(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(ctx.ContextUser.NumStars, utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(int64(ctx.ContextUser.NumStars))
ctx.JSON(http.StatusOK, &repos)
}
@@ -107,6 +108,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(ctx.Doer.NumStars, utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(int64(ctx.Doer.NumStars))
ctx.JSON(http.StatusOK, &repos)
}
+2 -1
View File
@@ -71,6 +71,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(int(total), utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &repos)
}
@@ -99,7 +100,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
if err != nil {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(int(total), utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &repos)
}
+3 -2
View File
@@ -23,8 +23,9 @@ import (
// ListOwnerHooks lists the webhooks of the provided owner
func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) {
listOptions := GetListOptions(ctx)
opts := &webhook.ListWebhookOptions{
ListOptions: GetListOptions(ctx),
ListOptions: listOptions,
OwnerID: owner.ID,
}
@@ -42,7 +43,7 @@ func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) {
return
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiHooks)
}
+1 -1
View File
@@ -12,7 +12,7 @@ import (
// GetListOptions returns list options using the page and limit parameters
func GetListOptions(ctx *context.APIContext) db.ListOptions {
return db.ListOptions{
Page: ctx.FormInt("page"),
Page: max(ctx.FormInt("page"), 1),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
}
}