From 2289580bb7ef8dfa4124c2b3bfb89897dbb35f46 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Thu, 12 Aug 2021 14:43:08 +0200 Subject: [PATCH] [API] generalize list header (#16551) * Add info about list endpoints to CONTRIBUTING.md * Let all list endpoints return X-Total-Count header * Add TODOs for GetCombinedCommitStatusByRef * Fix models/issue_stopwatch.go * Rrefactor models.ListDeployKeys * Introduce helper func and use them for SetLinkHeader related func --- CONTRIBUTING.md | 8 ++- integrations/api_issue_tracked_time_test.go | 2 +- integrations/api_repo_topic_test.go | 33 +++++++++ models/access.go | 2 +- models/commit_status.go | 2 +- models/gpg_key.go | 5 ++ models/issue.go | 4 +- models/issue_comment.go | 13 +++- models/issue_label.go | 10 +++ models/issue_milestone.go | 24 +++++-- models/issue_milestone_test.go | 8 +-- models/issue_stopwatch.go | 5 ++ models/issue_tracked_time.go | 31 ++++++--- models/issue_tracked_time_test.go | 22 +++--- models/notification.go | 5 ++ models/oauth2_application.go | 8 ++- models/org.go | 14 ++-- models/org_team.go | 15 ----- models/org_team_test.go | 2 +- models/org_test.go | 2 +- models/repo.go | 8 +-- models/repo_collaboration.go | 5 ++ models/repo_generate.go | 2 +- models/repo_transfer.go | 4 +- models/review.go | 5 ++ models/ssh_key.go | 6 ++ models/ssh_key_deploy.go | 56 ++++++++++------ models/token.go | 9 +++ models/topic.go | 17 ++++- models/topic_test.go | 15 +++-- models/user.go | 8 ++- models/webhook.go | 74 ++++++++++----------- models/webhook_test.go | 9 +-- modules/context/api.go | 17 +++++ modules/context/org.go | 4 +- modules/git/repo_tag.go | 19 ++---- modules/git/repo_tag_test.go | 3 +- modules/migrations/gitea_uploader_test.go | 4 +- routers/api/v1/admin/adopt.go | 4 +- routers/api/v1/admin/cron.go | 11 +-- routers/api/v1/admin/org.go | 4 +- routers/api/v1/admin/user.go | 3 +- routers/api/v1/notify/repo.go | 8 +++ routers/api/v1/notify/user.go | 7 ++ routers/api/v1/org/hook.go | 21 ++++-- routers/api/v1/org/label.go | 7 ++ routers/api/v1/org/member.go | 17 +++-- routers/api/v1/org/org.go | 9 +-- routers/api/v1/org/team.go | 41 +++++++----- routers/api/v1/repo/branch.go | 5 +- routers/api/v1/repo/collaborators.go | 9 +++ routers/api/v1/repo/commits.go | 8 +-- routers/api/v1/repo/fork.go | 2 + routers/api/v1/repo/hook.go | 17 ++++- routers/api/v1/repo/issue.go | 6 +- routers/api/v1/repo/issue_comment.go | 28 ++++++-- routers/api/v1/repo/issue_stopwatch.go | 7 ++ routers/api/v1/repo/issue_tracked_time.go | 31 +++++++-- routers/api/v1/repo/key.go | 26 +++++--- routers/api/v1/repo/label.go | 7 ++ routers/api/v1/repo/milestone.go | 4 +- routers/api/v1/repo/pull.go | 10 +-- routers/api/v1/repo/pull_review.go | 14 +++- routers/api/v1/repo/release.go | 4 +- routers/api/v1/repo/repo.go | 3 +- routers/api/v1/repo/star.go | 2 + routers/api/v1/repo/status.go | 4 +- routers/api/v1/repo/subscriber.go | 2 + routers/api/v1/repo/tag.go | 3 +- routers/api/v1/repo/topic.go | 33 +++++---- routers/api/v1/user/app.go | 17 ++++- routers/api/v1/user/follower.go | 4 ++ routers/api/v1/user/gpg_key.go | 7 ++ routers/api/v1/user/key.go | 10 +++ routers/api/v1/user/repo.go | 7 +- routers/api/v1/user/star.go | 2 + routers/api/v1/user/user.go | 4 +- routers/api/v1/user/watch.go | 21 +++--- routers/web/org/home.go | 4 +- routers/web/org/members.go | 4 +- routers/web/org/setting.go | 2 +- routers/web/repo/issue.go | 12 ++-- routers/web/repo/milestone.go | 13 ++-- routers/web/repo/setting.go | 4 +- routers/web/repo/view.go | 2 +- routers/web/repo/webhook.go | 2 +- services/pull/review.go | 2 +- services/webhook/webhook.go | 12 +++- 88 files changed, 637 insertions(+), 329 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb515f2685..026536868d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -207,6 +207,10 @@ In general, HTTP methods are chosen as follows: An endpoint which changes/edits an object expects all fields to be optional (except ones to identify the object, which are required). +### Endpoints returning lists should + * support pagination (`page` & `limit` options in query) + * set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444)) + ## Developer Certificate of Origin (DCO) @@ -231,8 +235,8 @@ on, finishing, and issuing releases. The overall goal is to make a minor release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. All the feature pull requests should be -merged before feature freeze. And, during the frozen period, a corresponding -release branch is open for fixes backported from main branch. Release candidates +merged before feature freeze. And, during the frozen period, a corresponding +release branch is open for fixes backported from main branch. Release candidates are made during this period for user testing to obtain a final version that is maintained in this branch. A release is maintained by issuing patch releases to only correct critical problems diff --git a/integrations/api_issue_tracked_time_test.go b/integrations/api_issue_tracked_time_test.go index 1a0ee99a43..9731c5a93b 100644 --- a/integrations/api_issue_tracked_time_test.go +++ b/integrations/api_issue_tracked_time_test.go @@ -30,7 +30,7 @@ func TestAPIGetTrackedTimes(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) var apiTimes api.TrackedTimeList DecodeJSON(t, resp, &apiTimes) - expect, err := models.GetTrackedTimes(models.FindTrackedTimesOptions{IssueID: issue2.ID}) + expect, err := models.GetTrackedTimes(&models.FindTrackedTimesOptions{IssueID: issue2.ID}) assert.NoError(t, err) assert.Len(t, apiTimes, 3) diff --git a/integrations/api_repo_topic_test.go b/integrations/api_repo_topic_test.go index 5e42bc64bf..bab17a68be 100644 --- a/integrations/api_repo_topic_test.go +++ b/integrations/api_repo_topic_test.go @@ -7,6 +7,7 @@ package integrations import ( "fmt" "net/http" + "net/url" "testing" "code.gitea.io/gitea/models" @@ -15,6 +16,38 @@ import ( "github.com/stretchr/testify/assert" ) +func TestAPITopicSearch(t *testing.T) { + defer prepareTestEnv(t)() + searchURL, _ := url.Parse("/api/v1/topics/search") + var topics struct { + TopicNames []*api.TopicResponse `json:"topics"` + } + + query := url.Values{"page": []string{"1"}, "limit": []string{"4"}} + + searchURL.RawQuery = query.Encode() + res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Len(t, topics.TopicNames, 4) + assert.EqualValues(t, "6", res.Header().Get("x-total-count")) + + query.Add("q", "topic") + searchURL.RawQuery = query.Encode() + res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Len(t, topics.TopicNames, 2) + + query.Set("q", "database") + searchURL.RawQuery = query.Encode() + res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + DecodeJSON(t, res, &topics) + if assert.Len(t, topics.TopicNames, 1) { + assert.EqualValues(t, 2, topics.TopicNames[0].ID) + assert.EqualValues(t, "database", topics.TopicNames[0].Name) + assert.EqualValues(t, 1, topics.TopicNames[0].RepoCount) + } +} + func TestAPIRepoTopic(t *testing.T) { defer prepareTestEnv(t)() user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of repo2 diff --git a/models/access.go b/models/access.go index d35b900cfd..5d0b0b06cf 100644 --- a/models/access.go +++ b/models/access.go @@ -246,7 +246,7 @@ func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err return fmt.Errorf("refreshCollaboratorAccesses: %v", err) } - if err = repo.Owner.getTeams(e); err != nil { + if err = repo.Owner.loadTeams(e); err != nil { return err } diff --git a/models/commit_status.go b/models/commit_status.go index c275820242..1ee3ead217 100644 --- a/models/commit_status.go +++ b/models/commit_status.go @@ -159,7 +159,7 @@ func getLatestCommitStatus(e Engine, repoID int64, sha string, listOptions ListO if len(ids) == 0 { return statuses, nil } - return statuses, x.In("id", ids).Find(&statuses) + return statuses, e.In("id", ids).Find(&statuses) } // FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts diff --git a/models/gpg_key.go b/models/gpg_key.go index 74ffb82a54..1072813b1b 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -71,6 +71,11 @@ func listGPGKeys(e Engine, uid int64, listOptions ListOptions) ([]*GPGKey, error return keys, sess.Find(&keys) } +// CountUserGPGKeys return number of gpg keys a user own +func CountUserGPGKeys(userID int64) (int64, error) { + return x.Where("owner_id=? AND primary_key_id=''", userID).Count(&GPGKey{}) +} + // GetGPGKeyByID returns public key by given ID. func GetGPGKeyByID(keyID int64) (*GPGKey, error) { key := new(GPGKey) diff --git a/models/issue.go b/models/issue.go index 225dfee20f..8b83612c2a 100644 --- a/models/issue.go +++ b/models/issue.go @@ -89,7 +89,7 @@ func init() { func (issue *Issue) loadTotalTimes(e Engine) (err error) { opts := FindTrackedTimesOptions{IssueID: issue.ID} - issue.TotalTrackedTime, err = opts.ToSession(e).SumInt(&TrackedTime{}, "time") + issue.TotalTrackedTime, err = opts.toSession(e).SumInt(&TrackedTime{}, "time") if err != nil { return err } @@ -214,7 +214,7 @@ func (issue *Issue) loadCommentsByType(e Engine, tp CommentType) (err error) { if issue.Comments != nil { return nil } - issue.Comments, err = findComments(e, FindCommentsOptions{ + issue.Comments, err = findComments(e, &FindCommentsOptions{ IssueID: issue.ID, Type: tp, }) diff --git a/models/issue_comment.go b/models/issue_comment.go index 755041efd7..06b74dff85 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -999,7 +999,7 @@ func (opts *FindCommentsOptions) toConds() builder.Cond { return cond } -func findComments(e Engine, opts FindCommentsOptions) ([]*Comment, error) { +func findComments(e Engine, opts *FindCommentsOptions) ([]*Comment, error) { comments := make([]*Comment, 0, 10) sess := e.Where(opts.toConds()) if opts.RepoID > 0 { @@ -1019,10 +1019,19 @@ func findComments(e Engine, opts FindCommentsOptions) ([]*Comment, error) { } // FindComments returns all comments according options -func FindComments(opts FindCommentsOptions) ([]*Comment, error) { +func FindComments(opts *FindCommentsOptions) ([]*Comment, error) { return findComments(x, opts) } +// CountComments count all comments according options by ignoring pagination +func CountComments(opts *FindCommentsOptions) (int64, error) { + sess := x.Where(opts.toConds()) + if opts.RepoID > 0 { + sess.Join("INNER", "issue", "issue.id = comment.issue_id") + } + return sess.Count(&Comment{}) +} + // UpdateComment updates information of comment. func UpdateComment(c *Comment, doer *User) error { sess := x.NewSession() diff --git a/models/issue_label.go b/models/issue_label.go index d1ff469236..5d50203b5a 100644 --- a/models/issue_label.go +++ b/models/issue_label.go @@ -444,6 +444,11 @@ func GetLabelsByRepoID(repoID int64, sortType string, listOptions ListOptions) ( return getLabelsByRepoID(x, repoID, sortType, listOptions) } +// CountLabelsByRepoID count number of all labels that belong to given repository by ID. +func CountLabelsByRepoID(repoID int64) (int64, error) { + return x.Where("repo_id = ?", repoID).Count(&Label{}) +} + // ________ // \_____ \_______ ____ // / | \_ __ \/ ___\ @@ -556,6 +561,11 @@ func GetLabelsByOrgID(orgID int64, sortType string, listOptions ListOptions) ([] return getLabelsByOrgID(x, orgID, sortType, listOptions) } +// CountLabelsByOrgID count all labels that belong to given organization by ID. +func CountLabelsByOrgID(orgID int64) (int64, error) { + return x.Where("org_id = ?", orgID).Count(&Label{}) +} + // .___ // | | ______ ________ __ ____ // | |/ ___// ___/ | \_/ __ \ diff --git a/models/issue_milestone.go b/models/issue_milestone.go index 5e934cde0a..e6976a46c7 100644 --- a/models/issue_milestone.go +++ b/models/issue_milestone.go @@ -380,24 +380,33 @@ type GetMilestonesOption struct { SortType string } -// GetMilestones returns milestones filtered by GetMilestonesOption's -func GetMilestones(opts GetMilestonesOption) (MilestoneList, error) { - sess := x.Where("repo_id = ?", opts.RepoID) +func (opts GetMilestonesOption) toCond() builder.Cond { + cond := builder.NewCond() + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } switch opts.State { case api.StateClosed: - sess = sess.And("is_closed = ?", true) + cond = cond.And(builder.Eq{"is_closed": true}) case api.StateAll: break // api.StateOpen: default: - sess = sess.And("is_closed = ?", false) + cond = cond.And(builder.Eq{"is_closed": false}) } if len(opts.Name) != 0 { - sess = sess.And(builder.Like{"name", opts.Name}) + cond = cond.And(builder.Like{"name", opts.Name}) } + return cond +} + +// GetMilestones returns milestones filtered by GetMilestonesOption's +func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) { + sess := x.Where(opts.toCond()) + if opts.Page != 0 { sess = opts.setSessionPagination(sess) } @@ -420,7 +429,8 @@ func GetMilestones(opts GetMilestonesOption) (MilestoneList, error) { } miles := make([]*Milestone, 0, opts.PageSize) - return miles, sess.Find(&miles) + total, err := sess.FindAndCount(&miles) + return miles, total, err } // SearchMilestones search milestones diff --git a/models/issue_milestone_test.go b/models/issue_milestone_test.go index 5406129884..1472c951eb 100644 --- a/models/issue_milestone_test.go +++ b/models/issue_milestone_test.go @@ -50,7 +50,7 @@ func TestGetMilestonesByRepoID(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) test := func(repoID int64, state api.StateType) { repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository) - milestones, err := GetMilestones(GetMilestonesOption{ + milestones, _, err := GetMilestones(GetMilestonesOption{ RepoID: repo.ID, State: state, }) @@ -87,7 +87,7 @@ func TestGetMilestonesByRepoID(t *testing.T) { test(3, api.StateClosed) test(3, api.StateAll) - milestones, err := GetMilestones(GetMilestonesOption{ + milestones, _, err := GetMilestones(GetMilestonesOption{ RepoID: NonexistentID, State: api.StateOpen, }) @@ -100,7 +100,7 @@ func TestGetMilestones(t *testing.T) { repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) test := func(sortType string, sortCond func(*Milestone) int) { for _, page := range []int{0, 1} { - milestones, err := GetMilestones(GetMilestonesOption{ + milestones, _, err := GetMilestones(GetMilestonesOption{ ListOptions: ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, @@ -117,7 +117,7 @@ func TestGetMilestones(t *testing.T) { } assert.True(t, sort.IntsAreSorted(values)) - milestones, err = GetMilestones(GetMilestonesOption{ + milestones, _, err = GetMilestones(GetMilestonesOption{ ListOptions: ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go index 8cdad94fd4..9322e26bf2 100644 --- a/models/issue_stopwatch.go +++ b/models/issue_stopwatch.go @@ -55,6 +55,11 @@ func GetUserStopwatches(userID int64, listOptions ListOptions) ([]*Stopwatch, er return sws, nil } +// CountUserStopwatches return count of all stopwatches of a user +func CountUserStopwatches(userID int64) (int64, error) { + return x.Where("user_id = ?", userID).Count(&Stopwatch{}) +} + // StopwatchExists returns true if the stopwatch exists func StopwatchExists(userID, issueID int64) bool { _, exists, _ := getStopwatch(x, userID, issueID) diff --git a/models/issue_tracked_time.go b/models/issue_tracked_time.go index 43f2e13784..e7769b41dd 100644 --- a/models/issue_tracked_time.go +++ b/models/issue_tracked_time.go @@ -79,8 +79,8 @@ type FindTrackedTimesOptions struct { CreatedBeforeUnix int64 } -// ToCond will convert each condition into a xorm-Cond -func (opts *FindTrackedTimesOptions) ToCond() builder.Cond { +// toCond will convert each condition into a xorm-Cond +func (opts *FindTrackedTimesOptions) toCond() 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}) @@ -103,14 +103,14 @@ func (opts *FindTrackedTimesOptions) ToCond() builder.Cond { return cond } -// 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 Engine) Engine { +// 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 Engine) Engine { sess := e if opts.RepositoryID > 0 || opts.MilestoneID > 0 { sess = e.Join("INNER", "issue", "issue.id = tracked_time.issue_id") } - sess = sess.Where(opts.ToCond()) + sess = sess.Where(opts.toCond()) if opts.Page != 0 { sess = opts.setEnginePagination(sess) @@ -119,18 +119,27 @@ func (opts *FindTrackedTimesOptions) ToSession(e Engine) Engine { return sess } -func getTrackedTimes(e Engine, options FindTrackedTimesOptions) (trackedTimes TrackedTimeList, err error) { - err = options.ToSession(e).Find(&trackedTimes) +func getTrackedTimes(e Engine, options *FindTrackedTimesOptions) (trackedTimes TrackedTimeList, err error) { + err = options.toSession(e).Find(&trackedTimes) return } // GetTrackedTimes returns all tracked times that fit to the given options. -func GetTrackedTimes(opts FindTrackedTimesOptions) (TrackedTimeList, error) { +func GetTrackedTimes(opts *FindTrackedTimesOptions) (TrackedTimeList, error) { return getTrackedTimes(x, opts) } +// CountTrackedTimes returns count of tracked times that fit to the given options. +func CountTrackedTimes(opts *FindTrackedTimesOptions) (int64, error) { + sess := x.Where(opts.toCond()) + if opts.RepositoryID > 0 || opts.MilestoneID > 0 { + sess = sess.Join("INNER", "issue", "issue.id = tracked_time.issue_id") + } + return sess.Count(&TrackedTime{}) +} + func getTrackedSeconds(e Engine, opts FindTrackedTimesOptions) (trackedSeconds int64, err error) { - return opts.ToSession(e).SumInt(&TrackedTime{}, "time") + return opts.toSession(e).SumInt(&TrackedTime{}, "time") } // GetTrackedSeconds return sum of seconds @@ -188,7 +197,7 @@ func addTime(e Engine, user *User, issue *Issue, amount int64, created time.Time } // TotalTimes returns the spent time for each user by an issue -func TotalTimes(options FindTrackedTimesOptions) (map[*User]string, error) { +func TotalTimes(options *FindTrackedTimesOptions) (map[*User]string, error) { trackedTimes, err := GetTrackedTimes(options) if err != nil { return nil, err @@ -288,7 +297,7 @@ func deleteTimes(e Engine, opts FindTrackedTimesOptions) (removedTime int64, err return } - _, err = opts.ToSession(e).Table("tracked_time").Cols("deleted").Update(&TrackedTime{Deleted: true}) + _, err = opts.toSession(e).Table("tracked_time").Cols("deleted").Update(&TrackedTime{Deleted: true}) return } diff --git a/models/issue_tracked_time_test.go b/models/issue_tracked_time_test.go index e30de6cb69..9de1dd699d 100644 --- a/models/issue_tracked_time_test.go +++ b/models/issue_tracked_time_test.go @@ -38,27 +38,27 @@ func TestGetTrackedTimes(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) // by Issue - times, err := GetTrackedTimes(FindTrackedTimesOptions{IssueID: 1}) + times, err := GetTrackedTimes(&FindTrackedTimesOptions{IssueID: 1}) assert.NoError(t, err) assert.Len(t, times, 1) assert.Equal(t, int64(400), times[0].Time) - times, err = GetTrackedTimes(FindTrackedTimesOptions{IssueID: -1}) + times, err = GetTrackedTimes(&FindTrackedTimesOptions{IssueID: -1}) assert.NoError(t, err) assert.Len(t, times, 0) // by User - times, err = GetTrackedTimes(FindTrackedTimesOptions{UserID: 1}) + times, err = GetTrackedTimes(&FindTrackedTimesOptions{UserID: 1}) assert.NoError(t, err) assert.Len(t, times, 3) assert.Equal(t, int64(400), times[0].Time) - times, err = GetTrackedTimes(FindTrackedTimesOptions{UserID: 3}) + times, err = GetTrackedTimes(&FindTrackedTimesOptions{UserID: 3}) assert.NoError(t, err) assert.Len(t, times, 0) // by Repo - times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 2}) + times, err = GetTrackedTimes(&FindTrackedTimesOptions{RepositoryID: 2}) assert.NoError(t, err) assert.Len(t, times, 3) assert.Equal(t, int64(1), times[0].Time) @@ -66,11 +66,11 @@ func TestGetTrackedTimes(t *testing.T) { assert.NoError(t, err) assert.Equal(t, issue.RepoID, int64(2)) - times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 1}) + times, err = GetTrackedTimes(&FindTrackedTimesOptions{RepositoryID: 1}) assert.NoError(t, err) assert.Len(t, times, 5) - times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 10}) + times, err = GetTrackedTimes(&FindTrackedTimesOptions{RepositoryID: 10}) assert.NoError(t, err) assert.Len(t, times, 0) } @@ -78,7 +78,7 @@ func TestGetTrackedTimes(t *testing.T) { func TestTotalTimes(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - total, err := TotalTimes(FindTrackedTimesOptions{IssueID: 1}) + total, err := TotalTimes(&FindTrackedTimesOptions{IssueID: 1}) assert.NoError(t, err) assert.Len(t, total, 1) for user, time := range total { @@ -86,7 +86,7 @@ func TestTotalTimes(t *testing.T) { assert.Equal(t, "6min 40s", time) } - total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 2}) + total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 2}) assert.NoError(t, err) assert.Len(t, total, 2) for user, time := range total { @@ -99,7 +99,7 @@ func TestTotalTimes(t *testing.T) { } } - total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 5}) + total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 5}) assert.NoError(t, err) assert.Len(t, total, 1) for user, time := range total { @@ -107,7 +107,7 @@ func TestTotalTimes(t *testing.T) { assert.Equal(t, "1s", time) } - total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 4}) + total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 4}) assert.NoError(t, err) assert.Len(t, total, 2) } diff --git a/models/notification.go b/models/notification.go index c4c7728ad9..30bb7596a8 100644 --- a/models/notification.go +++ b/models/notification.go @@ -125,6 +125,11 @@ func GetNotifications(opts *FindNotificationOptions) (NotificationList, error) { return getNotifications(x, opts) } +// CountNotifications count all notifications that fit to the given options and ignore pagination. +func CountNotifications(opts *FindNotificationOptions) (int64, error) { + return x.Where(opts.ToCond()).Count(&Notification{}) +} + // CreateRepoTransferNotification creates notification for the user a repository was transferred to func CreateRepoTransferNotification(doer, newOwner *User, repo *Repository) error { sess := x.NewSession() diff --git a/models/oauth2_application.go b/models/oauth2_application.go index 2aa9fbd3d9..9cf236f0cb 100644 --- a/models/oauth2_application.go +++ b/models/oauth2_application.go @@ -269,7 +269,7 @@ func DeleteOAuth2Application(id, userid int64) error { } // ListOAuth2Applications returns a list of oauth2 applications belongs to given user. -func ListOAuth2Applications(uid int64, listOptions ListOptions) ([]*OAuth2Application, error) { +func ListOAuth2Applications(uid int64, listOptions ListOptions) ([]*OAuth2Application, int64, error) { sess := x. Where("uid=?", uid). Desc("id") @@ -278,11 +278,13 @@ func ListOAuth2Applications(uid int64, listOptions ListOptions) ([]*OAuth2Applic sess = listOptions.setSessionPagination(sess) apps := make([]*OAuth2Application, 0, listOptions.PageSize) - return apps, sess.Find(&apps) + total, err := sess.FindAndCount(&apps) + return apps, total, err } apps := make([]*OAuth2Application, 0, 5) - return apps, sess.Find(&apps) + total, err := sess.FindAndCount(&apps) + return apps, total, err } ////////////////////////////////////////////////////// diff --git a/models/org.go b/models/org.go index 58fb26b1bb..3de6bd14b2 100644 --- a/models/org.go +++ b/models/org.go @@ -52,7 +52,7 @@ func (org *User) GetOwnerTeam() (*Team, error) { return org.getOwnerTeam(x) } -func (org *User) getTeams(e Engine) error { +func (org *User) loadTeams(e Engine) error { if org.Teams != nil { return nil } @@ -62,13 +62,9 @@ func (org *User) getTeams(e Engine) error { Find(&org.Teams) } -// GetTeams returns paginated teams that belong to organization. -func (org *User) GetTeams(opts *SearchTeamOptions) error { - if opts.Page != 0 { - return org.getTeams(opts.getPaginatedSession()) - } - - return org.getTeams(x) +// LoadTeams load teams if not loaded. +func (org *User) LoadTeams() error { + return org.loadTeams(x) } // GetMembers returns all members of organization. @@ -87,7 +83,7 @@ type FindOrgMembersOpts struct { } // CountOrgMembers counts the organization's members -func CountOrgMembers(opts FindOrgMembersOpts) (int64, error) { +func CountOrgMembers(opts *FindOrgMembersOpts) (int64, error) { sess := x.Where("org_id=?", opts.OrgID) if opts.PublicOnly { sess.And("is_public = ?", true) diff --git a/models/org_team.go b/models/org_team.go index 6226772b9a..c380c8cd8e 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -790,16 +790,6 @@ func GetTeamMembers(teamID int64) ([]*User, error) { return getTeamMembers(x, teamID) } -func getUserTeams(e Engine, userID int64, listOptions ListOptions) (teams []*Team, err error) { - sess := e. - Join("INNER", "team_user", "team_user.team_id = team.id"). - Where("team_user.uid=?", userID) - if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) - } - return teams, sess.Find(&teams) -} - func getUserOrgTeams(e Engine, orgID, userID int64) (teams []*Team, err error) { return teams, e. Join("INNER", "team_user", "team_user.team_id = team.id"). @@ -823,11 +813,6 @@ func GetUserOrgTeams(orgID, userID int64) ([]*Team, error) { return getUserOrgTeams(x, orgID, userID) } -// GetUserTeams returns all teams that user belongs across all organizations. -func GetUserTeams(userID int64, listOptions ListOptions) ([]*Team, error) { - return getUserTeams(x, userID, listOptions) -} - // AddTeamMember adds new membership of given team to given organization, // the user will have membership to given organization automatically when needed. func AddTeamMember(team *Team, userID int64) error { diff --git a/models/org_team_test.go b/models/org_team_test.go index 38e36cf82c..9dc26fd814 100644 --- a/models/org_team_test.go +++ b/models/org_team_test.go @@ -286,7 +286,7 @@ func TestGetTeamMembers(t *testing.T) { func TestGetUserTeams(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) test := func(userID int64) { - teams, err := GetUserTeams(userID, ListOptions{}) + teams, _, err := SearchTeam(&SearchTeamOptions{UserID: userID}) assert.NoError(t, err) for _, team := range teams { AssertExistsAndLoadBean(t, &TeamUser{TeamID: team.ID, UID: userID}) diff --git a/models/org_test.go b/models/org_test.go index e494e502dd..7ba9d8ee88 100644 --- a/models/org_test.go +++ b/models/org_test.go @@ -86,7 +86,7 @@ func TestUser_GetOwnerTeam(t *testing.T) { func TestUser_GetTeams(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User) - assert.NoError(t, org.GetTeams(&SearchTeamOptions{})) + assert.NoError(t, org.LoadTeams()) if assert.Len(t, org.Teams, 4) { assert.Equal(t, int64(1), org.Teams[0].ID) assert.Equal(t, int64(2), org.Teams[1].ID) diff --git a/models/repo.go b/models/repo.go index 93827f6a88..f2e3f5b58b 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1125,8 +1125,8 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository, overwriteO // Give access to all members in teams with access to all repositories. if u.IsOrganization() { - if err := u.getTeams(ctx.e); err != nil { - return fmt.Errorf("GetTeams: %v", err) + if err := u.loadTeams(ctx.e); err != nil { + return fmt.Errorf("loadTeams: %v", err) } for _, t := range u.Teams { if t.IncludesAllRepositories { @@ -1439,7 +1439,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error { return err } if org.IsOrganization() { - if err = org.getTeams(sess); err != nil { + if err = org.loadTeams(sess); err != nil { return err } } @@ -1453,7 +1453,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error { } // Delete Deploy Keys - deployKeys, err := listDeployKeys(sess, repo.ID, ListOptions{}) + deployKeys, err := listDeployKeys(sess, &ListDeployKeysOptions{RepoID: repoID}) if err != nil { return fmt.Errorf("listDeployKeys: %v", err) } diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go index b9488f5e2e..a8b715bbcf 100644 --- a/models/repo_collaboration.go +++ b/models/repo_collaboration.go @@ -102,6 +102,11 @@ func (repo *Repository) GetCollaborators(listOptions ListOptions) ([]*Collaborat return repo.getCollaborators(x, listOptions) } +// CountCollaborators returns total number of collaborators for a repository +func (repo *Repository) CountCollaborators() (int64, error) { + return x.Where("repo_id = ? ", repo.ID).Count(&Collaboration{}) +} + func (repo *Repository) getCollaboration(e Engine, uid int64) (*Collaboration, error) { collaboration := &Collaboration{ RepoID: repo.ID, diff --git a/models/repo_generate.go b/models/repo_generate.go index 1cf73bc55e..66682903f1 100644 --- a/models/repo_generate.go +++ b/models/repo_generate.go @@ -111,7 +111,7 @@ func GenerateGitHooks(ctx DBContext, templateRepo, generateRepo *Repository) err // GenerateWebhooks generates webhooks from a template repository func GenerateWebhooks(ctx DBContext, templateRepo, generateRepo *Repository) error { - templateWebhooks, err := GetWebhooksByRepoID(templateRepo.ID, ListOptions{}) + templateWebhooks, err := ListWebhooksByOpts(&ListWebhookOptions{RepoID: templateRepo.ID}) if err != nil { return err } diff --git a/models/repo_transfer.go b/models/repo_transfer.go index d7ef0a8ca6..09b6290293 100644 --- a/models/repo_transfer.go +++ b/models/repo_transfer.go @@ -291,8 +291,8 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) (err e } if newOwner.IsOrganization() { - if err := newOwner.getTeams(sess); err != nil { - return fmt.Errorf("GetTeams: %v", err) + if err := newOwner.loadTeams(sess); err != nil { + return fmt.Errorf("LoadTeams: %v", err) } for _, t := range newOwner.Teams { if t.IncludesAllRepositories { diff --git a/models/review.go b/models/review.go index acb54d970f..1ffff8feb6 100644 --- a/models/review.go +++ b/models/review.go @@ -208,6 +208,11 @@ func FindReviews(opts FindReviewOptions) ([]*Review, error) { return findReviews(x, opts) } +// CountReviews returns count of reviews passing FindReviewOptions +func CountReviews(opts FindReviewOptions) (int64, error) { + return x.Where(opts.toCond()).Count(&Review{}) +} + // CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required. type CreateReviewOptions struct { Content string diff --git a/models/ssh_key.go b/models/ssh_key.go index 6cda4f1658..a2f4c610e4 100644 --- a/models/ssh_key.go +++ b/models/ssh_key.go @@ -205,6 +205,12 @@ func ListPublicKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) { return keys, sess.Find(&keys) } +// CountPublicKeys count public keys a user has +func CountPublicKeys(userID int64) (int64, error) { + sess := x.Where("owner_id = ? AND type != ?", userID, KeyTypePrincipal) + return sess.Count(&PublicKey{}) +} + // ListPublicKeysBySource returns a list of synchronized public keys for a given user and login source. func ListPublicKeysBySource(uid, loginSourceID int64) ([]*PublicKey, error) { keys := make([]*PublicKey, 0, 5) diff --git a/models/ssh_key_deploy.go b/models/ssh_key_deploy.go index 3189bcf456..e7d486b9f5 100644 --- a/models/ssh_key_deploy.go +++ b/models/ssh_key_deploy.go @@ -264,17 +264,40 @@ func deleteDeployKey(sess Engine, doer *User, id int64) error { return nil } -// ListDeployKeys returns all deploy keys by given repository ID. -func ListDeployKeys(repoID int64, listOptions ListOptions) ([]*DeployKey, error) { - return listDeployKeys(x, repoID, listOptions) +// ListDeployKeysOptions are options for ListDeployKeys +type ListDeployKeysOptions struct { + ListOptions + RepoID int64 + KeyID int64 + Fingerprint string } -func listDeployKeys(e Engine, repoID int64, listOptions ListOptions) ([]*DeployKey, error) { - sess := e.Where("repo_id = ?", repoID) - if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) +func (opt ListDeployKeysOptions) toCond() builder.Cond { + cond := builder.NewCond() + if opt.RepoID != 0 { + cond = cond.And(builder.Eq{"repo_id": opt.RepoID}) + } + if opt.KeyID != 0 { + cond = cond.And(builder.Eq{"key_id": opt.KeyID}) + } + if opt.Fingerprint != "" { + cond = cond.And(builder.Eq{"fingerprint": opt.Fingerprint}) + } + return cond +} - keys := make([]*DeployKey, 0, listOptions.PageSize) +// ListDeployKeys returns a list of deploy keys matching the provided arguments. +func ListDeployKeys(opts *ListDeployKeysOptions) ([]*DeployKey, error) { + return listDeployKeys(x, opts) +} + +func listDeployKeys(e Engine, opts *ListDeployKeysOptions) ([]*DeployKey, error) { + sess := e.Where(opts.toCond()) + + if opts.Page != 0 { + sess = opts.setSessionPagination(sess) + + keys := make([]*DeployKey, 0, opts.PageSize) return keys, sess.Find(&keys) } @@ -282,18 +305,7 @@ func listDeployKeys(e Engine, repoID int64, listOptions ListOptions) ([]*DeployK return keys, sess.Find(&keys) } -// SearchDeployKeys returns a list of deploy keys matching the provided arguments. -func SearchDeployKeys(repoID, keyID int64, fingerprint string) ([]*DeployKey, error) { - keys := make([]*DeployKey, 0, 5) - cond := builder.NewCond() - if repoID != 0 { - cond = cond.And(builder.Eq{"repo_id": repoID}) - } - if keyID != 0 { - cond = cond.And(builder.Eq{"key_id": keyID}) - } - if fingerprint != "" { - cond = cond.And(builder.Eq{"fingerprint": fingerprint}) - } - return keys, x.Where(cond).Find(&keys) +// CountDeployKeys returns count deploy keys matching the provided arguments. +func CountDeployKeys(opts *ListDeployKeysOptions) (int64, error) { + return x.Where(opts.toCond()).Count(&DeployKey{}) } diff --git a/models/token.go b/models/token.go index 357afe44a7..8e1f91d43f 100644 --- a/models/token.go +++ b/models/token.go @@ -122,6 +122,15 @@ func UpdateAccessToken(t *AccessToken) error { return err } +// CountAccessTokens count access tokens belongs to given user by options +func CountAccessTokens(opts ListAccessTokensOptions) (int64, error) { + sess := x.Where("uid=?", opts.UserID) + if len(opts.Name) != 0 { + sess = sess.Where("name=?", opts.Name) + } + return sess.Count(&AccessToken{}) +} + // DeleteAccessTokenByID deletes access token by given ID. func DeleteAccessTokenByID(id, userID int64) error { cnt, err := x.ID(id).Delete(&AccessToken{ diff --git a/models/topic.go b/models/topic.go index 19c572fefe..7fc34f5bef 100644 --- a/models/topic.go +++ b/models/topic.go @@ -184,7 +184,7 @@ func (opts *FindTopicOptions) toConds() builder.Cond { } // FindTopics retrieves the topics via FindTopicOptions -func FindTopics(opts *FindTopicOptions) (topics []*Topic, err error) { +func FindTopics(opts *FindTopicOptions) ([]*Topic, int64, error) { sess := x.Select("topic.*").Where(opts.toConds()) if opts.RepoID > 0 { sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") @@ -192,7 +192,18 @@ func FindTopics(opts *FindTopicOptions) (topics []*Topic, err error) { if opts.PageSize != 0 && opts.Page != 0 { sess = opts.setSessionPagination(sess) } - return topics, sess.Desc("topic.repo_count").Find(&topics) + topics := make([]*Topic, 0, 10) + total, err := sess.Desc("topic.repo_count").FindAndCount(&topics) + return topics, total, err +} + +// CountTopics counts the number of topics matching the FindTopicOptions +func CountTopics(opts *FindTopicOptions) (int64, error) { + sess := x.Where(opts.toConds()) + if opts.RepoID > 0 { + sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") + } + return sess.Count(new(Topic)) } // GetRepoTopicByName retrieves topic from name for a repo if it exist @@ -269,7 +280,7 @@ func DeleteTopic(repoID int64, topicName string) (*Topic, error) { // SaveTopics save topics to a repository func SaveTopics(repoID int64, topicNames ...string) error { - topics, err := FindTopics(&FindTopicOptions{ + topics, _, err := FindTopics(&FindTopicOptions{ RepoID: repoID, }) if err != nil { diff --git a/models/topic_test.go b/models/topic_test.go index 25232eb981..9386a71e35 100644 --- a/models/topic_test.go +++ b/models/topic_test.go @@ -17,17 +17,18 @@ func TestAddTopic(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - topics, err := FindTopics(&FindTopicOptions{}) + topics, _, err := FindTopics(&FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, err = FindTopics(&FindTopicOptions{ + topics, total, err := FindTopics(&FindTopicOptions{ ListOptions: ListOptions{Page: 1, PageSize: 2}, }) assert.NoError(t, err) assert.Len(t, topics, 2) + assert.EqualValues(t, 6, total) - topics, err = FindTopics(&FindTopicOptions{ + topics, _, err = FindTopics(&FindTopicOptions{ RepoID: 1, }) assert.NoError(t, err) @@ -35,11 +36,11 @@ func TestAddTopic(t *testing.T) { assert.NoError(t, SaveTopics(2, "golang")) repo2NrOfTopics = 1 - topics, err = FindTopics(&FindTopicOptions{}) + topics, _, err = FindTopics(&FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, err = FindTopics(&FindTopicOptions{ + topics, _, err = FindTopics(&FindTopicOptions{ RepoID: 2, }) assert.NoError(t, err) @@ -52,11 +53,11 @@ func TestAddTopic(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, 1, topic.RepoCount) - topics, err = FindTopics(&FindTopicOptions{}) + topics, _, err = FindTopics(&FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, err = FindTopics(&FindTopicOptions{ + topics, _, err = FindTopics(&FindTopicOptions{ RepoID: 2, }) assert.NoError(t, err) diff --git a/models/user.go b/models/user.go index c68417a2c3..1af9e545ad 100644 --- a/models/user.go +++ b/models/user.go @@ -1704,7 +1704,7 @@ func GetStarredRepos(userID int64, private bool, listOptions ListOptions) ([]*Re } // GetWatchedRepos returns the repos watched by a particular user -func GetWatchedRepos(userID int64, private bool, listOptions ListOptions) ([]*Repository, error) { +func GetWatchedRepos(userID int64, private bool, listOptions ListOptions) ([]*Repository, int64, error) { sess := x.Where("watch.user_id=?", userID). And("`watch`.mode<>?", RepoWatchModeDont). Join("LEFT", "watch", "`repository`.id=`watch`.repo_id") @@ -1716,11 +1716,13 @@ func GetWatchedRepos(userID int64, private bool, listOptions ListOptions) ([]*Re sess = listOptions.setSessionPagination(sess) repos := make([]*Repository, 0, listOptions.PageSize) - return repos, sess.Find(&repos) + total, err := sess.FindAndCount(&repos) + return repos, total, err } repos := make([]*Repository, 0, 10) - return repos, sess.Find(&repos) + total, err := sess.FindAndCount(&repos) + return repos, total, err } // IterateUser iterate users diff --git a/models/webhook.go b/models/webhook.go index 138ba26bde..79ce70a0de 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -16,8 +16,10 @@ 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" gouuid "github.com/google/uuid" + "xorm.io/builder" ) // HookContentType is the content type of a web hook @@ -387,53 +389,51 @@ func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) { }) } -// GetActiveWebhooksByRepoID returns all active webhooks of repository. -func GetActiveWebhooksByRepoID(repoID int64) ([]*Webhook, error) { - return getActiveWebhooksByRepoID(x, repoID) +// ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts +type ListWebhookOptions struct { + ListOptions + RepoID int64 + OrgID int64 + IsActive util.OptionalBool } -func getActiveWebhooksByRepoID(e Engine, repoID int64) ([]*Webhook, error) { - webhooks := make([]*Webhook, 0, 5) - return webhooks, e.Where("is_active=?", true). - Find(&webhooks, &Webhook{RepoID: repoID}) +func (opts *ListWebhookOptions) toCond() builder.Cond { + cond := builder.NewCond() + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"webhook.repo_id": opts.RepoID}) + } + if opts.OrgID != 0 { + cond = cond.And(builder.Eq{"webhook.org_id": opts.OrgID}) + } + if !opts.IsActive.IsNone() { + cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()}) + } + return cond } -// GetWebhooksByRepoID returns all webhooks of a repository. -func GetWebhooksByRepoID(repoID int64, listOptions ListOptions) ([]*Webhook, error) { - if listOptions.Page == 0 { - webhooks := make([]*Webhook, 0, 5) - return webhooks, x.Find(&webhooks, &Webhook{RepoID: repoID}) +func listWebhooksByOpts(e Engine, opts *ListWebhookOptions) ([]*Webhook, error) { + sess := e.Where(opts.toCond()) + + if opts.Page != 0 { + sess = opts.setSessionPagination(sess) + webhooks := make([]*Webhook, 0, opts.PageSize) + err := sess.Find(&webhooks) + return webhooks, err } - sess := listOptions.getPaginatedSession() - webhooks := make([]*Webhook, 0, listOptions.PageSize) - - return webhooks, sess.Find(&webhooks, &Webhook{RepoID: repoID}) + webhooks := make([]*Webhook, 0, 10) + err := sess.Find(&webhooks) + return webhooks, err } -// GetActiveWebhooksByOrgID returns all active webhooks for an organization. -func GetActiveWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) { - return getActiveWebhooksByOrgID(x, orgID) +// ListWebhooksByOpts return webhooks based on options +func ListWebhooksByOpts(opts *ListWebhookOptions) ([]*Webhook, error) { + return listWebhooksByOpts(x, opts) } -func getActiveWebhooksByOrgID(e Engine, orgID int64) (ws []*Webhook, err error) { - err = e. - Where("org_id=?", orgID). - And("is_active=?", true). - Find(&ws) - return ws, err -} - -// GetWebhooksByOrgID returns paginated webhooks for an organization. -func GetWebhooksByOrgID(orgID int64, listOptions ListOptions) ([]*Webhook, error) { - if listOptions.Page == 0 { - ws := make([]*Webhook, 0, 5) - return ws, x.Find(&ws, &Webhook{OrgID: orgID}) - } - - sess := listOptions.getPaginatedSession() - ws := make([]*Webhook, 0, listOptions.PageSize) - return ws, sess.Find(&ws, &Webhook{OrgID: orgID}) +// CountWebhooksByOpts count webhooks based on options and ignore pagination +func CountWebhooksByOpts(opts *ListWebhookOptions) (int64, error) { + return x.Where(opts.toCond()).Count(&Webhook{}) } // GetDefaultWebhooks returns all admin-default webhooks. diff --git a/models/webhook_test.go b/models/webhook_test.go index 84520666c0..625d643856 100644 --- a/models/webhook_test.go +++ b/models/webhook_test.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) @@ -118,7 +119,7 @@ func TestGetWebhookByOrgID(t *testing.T) { func TestGetActiveWebhooksByRepoID(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - hooks, err := GetActiveWebhooksByRepoID(1) + hooks, err := ListWebhooksByOpts(&ListWebhookOptions{RepoID: 1, IsActive: util.OptionalBoolTrue}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(1), hooks[0].ID) @@ -128,7 +129,7 @@ func TestGetActiveWebhooksByRepoID(t *testing.T) { func TestGetWebhooksByRepoID(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - hooks, err := GetWebhooksByRepoID(1, ListOptions{}) + hooks, err := ListWebhooksByOpts(&ListWebhookOptions{RepoID: 1}) assert.NoError(t, err) if assert.Len(t, hooks, 2) { assert.Equal(t, int64(1), hooks[0].ID) @@ -138,7 +139,7 @@ func TestGetWebhooksByRepoID(t *testing.T) { func TestGetActiveWebhooksByOrgID(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - hooks, err := GetActiveWebhooksByOrgID(3) + hooks, err := ListWebhooksByOpts(&ListWebhookOptions{OrgID: 3, IsActive: util.OptionalBoolTrue}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(3), hooks[0].ID) @@ -148,7 +149,7 @@ func TestGetActiveWebhooksByOrgID(t *testing.T) { func TestGetWebhooksByOrgID(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - hooks, err := GetWebhooksByOrgID(3, ListOptions{}) + hooks, err := ListWebhooksByOpts(&ListWebhookOptions{OrgID: 3}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(3), hooks[0].ID) diff --git a/modules/context/api.go b/modules/context/api.go index b543c8bac8..47ea8acfe0 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -181,6 +181,23 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) { if len(links) > 0 { ctx.Header().Set("Link", strings.Join(links, ",")) + ctx.AppendAccessControlExposeHeaders("Link") + } +} + +// SetTotalCountHeader set "X-Total-Count" header +func (ctx *APIContext) SetTotalCountHeader(total int64) { + ctx.Header().Set("X-Total-Count", fmt.Sprint(total)) + ctx.AppendAccessControlExposeHeaders("X-Total-Count") +} + +// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header +func (ctx *APIContext) AppendAccessControlExposeHeaders(names ...string) { + val := ctx.Header().Get("Access-Control-Expose-Headers") + if len(val) != 0 { + ctx.Header().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", "))) + } else { + ctx.Header().Set("Access-Control-Expose-Headers", strings.Join(names, ", ")) } } diff --git a/modules/context/org.go b/modules/context/org.go index 527ccfbcaa..fd67212a10 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -123,8 +123,8 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { // Team. if ctx.Org.IsMember { if ctx.Org.IsOwner { - if err := org.GetTeams(&models.SearchTeamOptions{}); err != nil { - ctx.ServerError("GetTeams", err) + if err := org.LoadTeams(); err != nil { + ctx.ServerError("LoadTeams", err) return } } else { diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index d91c3ca979..44d7a048bc 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) // TagPrefix tags prefix path on the repository @@ -160,24 +161,18 @@ func (repo *Repository) GetTag(name string) (*Tag, error) { } // GetTagInfos returns all tag infos of the repository. -func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, error) { +func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { // TODO this a slow implementation, makes one git command per tag stdout, err := NewCommand("tag").RunInDir(repo.Path) if err != nil { - return nil, err + return nil, 0, err } tagNames := strings.Split(strings.TrimRight(stdout, "\n"), "\n") + tagsTotal := len(tagNames) if page != 0 { - skip := (page - 1) * pageSize - if skip >= len(tagNames) { - return nil, nil - } - if (len(tagNames) - skip) < pageSize { - pageSize = len(tagNames) - skip - } - tagNames = tagNames[skip : skip+pageSize] + tagNames = util.PaginateSlice(tagNames, page, pageSize).([]string) } var tags = make([]*Tag, 0, len(tagNames)) @@ -189,13 +184,13 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, error) { tag, err := repo.GetTag(tagName) if err != nil { - return nil, err + return nil, tagsTotal, err } tag.Name = tagName tags = append(tags, tag) } sortTagsByTime(tags) - return tags, nil + return tags, tagsTotal, nil } // GetTagType gets the type of the tag, either commit (simple) or tag (annotated) diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index 6feae8d913..cfab9edd8f 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -18,9 +18,10 @@ func TestRepository_GetTags(t *testing.T) { assert.NoError(t, err) defer bareRepo1.Close() - tags, err := bareRepo1.GetTagInfos(0, 0) + tags, total, err := bareRepo1.GetTagInfos(0, 0) assert.NoError(t, err) assert.Len(t, tags, 1) + assert.Equal(t, len(tags), total) assert.EqualValues(t, "test", tags[0].Name) assert.EqualValues(t, "3ad28a9149a2864384548f3d17ed7f38014c9e8a", tags[0].ID.String()) assert.EqualValues(t, "tag", tags[0].Type) diff --git a/modules/migrations/gitea_uploader_test.go b/modules/migrations/gitea_uploader_test.go index 5f36d54584..2f854ba368 100644 --- a/modules/migrations/gitea_uploader_test.go +++ b/modules/migrations/gitea_uploader_test.go @@ -54,14 +54,14 @@ func TestGiteaUploadRepo(t *testing.T) { assert.True(t, repo.HasWiki()) assert.EqualValues(t, models.RepositoryReady, repo.Status) - milestones, err := models.GetMilestones(models.GetMilestonesOption{ + milestones, _, err := models.GetMilestones(models.GetMilestonesOption{ RepoID: repo.ID, State: structs.StateOpen, }) assert.NoError(t, err) assert.Len(t, milestones, 1) - milestones, err = models.GetMilestones(models.GetMilestonesOption{ + milestones, _, err = models.GetMilestones(models.GetMilestonesOption{ RepoID: repo.ID, State: structs.StateClosed, }) diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index 062cee6283..9c1b9fc0f8 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -5,7 +5,6 @@ package admin import ( - "fmt" "net/http" "code.gitea.io/gitea/models" @@ -47,8 +46,7 @@ func ListUnadoptedRepositories(ctx *context.APIContext) { ctx.InternalServerError(err) } - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", count)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count") + ctx.SetTotalCountHeader(int64(count)) ctx.JSON(http.StatusOK, repoNames) } diff --git a/routers/api/v1/admin/cron.go b/routers/api/v1/admin/cron.go index 2531346fcb..970fad8388 100644 --- a/routers/api/v1/admin/cron.go +++ b/routers/api/v1/admin/cron.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/cron" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/v1/utils" ) @@ -36,12 +37,10 @@ func ListCronTasks(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" tasks := cron.ListTasks() - listOpts := utils.GetListOptions(ctx) - start, end := listOpts.GetStartEnd() + count := len(tasks) - if len(tasks) > listOpts.PageSize { - tasks = tasks[start:end] - } + listOpts := utils.GetListOptions(ctx) + tasks = util.PaginateSlice(tasks, listOpts.Page, listOpts.PageSize).(cron.TaskTable) res := make([]structs.Cron, len(tasks)) for i, task := range tasks { @@ -53,6 +52,8 @@ func ListCronTasks(ctx *context.APIContext) { ExecTimes: task.ExecTimes, } } + + ctx.SetTotalCountHeader(int64(count)) ctx.JSON(http.StatusOK, res) } diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index 1356276f07..f1a766d544 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -6,7 +6,6 @@ package admin import ( - "fmt" "net/http" "code.gitea.io/gitea/models" @@ -121,7 +120,6 @@ func GetAllOrgs(ctx *context.APIContext) { } ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", maxResults)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &orgs) } diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 6bc9b849b1..e5a75da759 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -423,7 +423,6 @@ func GetAllUsers(ctx *context.APIContext) { } ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", maxResults)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &results) } diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go index af4507f896..1a36642b6a 100644 --- a/routers/api/v1/notify/repo.go +++ b/routers/api/v1/notify/repo.go @@ -108,6 +108,12 @@ func ListRepoNotifications(ctx *context.APIContext) { } opts.RepoID = ctx.Repo.Repository.ID + totalCount, err := models.CountNotifications(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + nl, err := models.GetNotifications(opts) if err != nil { ctx.InternalServerError(err) @@ -119,6 +125,8 @@ func ListRepoNotifications(ctx *context.APIContext) { return } + ctx.SetTotalCountHeader(totalCount) + ctx.JSON(http.StatusOK, convert.ToNotifications(nl)) } diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go index c2178b4dee..e4626cb719 100644 --- a/routers/api/v1/notify/user.go +++ b/routers/api/v1/notify/user.go @@ -68,6 +68,12 @@ func ListNotifications(ctx *context.APIContext) { return } + totalCount, err := models.CountNotifications(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + nl, err := models.GetNotifications(opts) if err != nil { ctx.InternalServerError(err) @@ -79,6 +85,7 @@ func ListNotifications(ctx *context.APIContext) { return } + ctx.SetTotalCountHeader(totalCount) ctx.JSON(http.StatusOK, convert.ToNotifications(nl)) } diff --git a/routers/api/v1/org/hook.go b/routers/api/v1/org/hook.go index ed827c48d4..c5982300eb 100644 --- a/routers/api/v1/org/hook.go +++ b/routers/api/v1/org/hook.go @@ -40,16 +40,29 @@ func ListHooks(ctx *context.APIContext) { // "200": // "$ref": "#/responses/HookList" - org := ctx.Org.Organization - orgHooks, err := models.GetWebhooksByOrgID(org.ID, utils.GetListOptions(ctx)) + opts := &models.ListWebhookOptions{ + ListOptions: utils.GetListOptions(ctx), + OrgID: ctx.Org.Organization.ID, + } + + count, err := models.CountWebhooksByOpts(opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetWebhooksByOrgID", err) + ctx.InternalServerError(err) return } + + orgHooks, err := models.ListWebhooksByOpts(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + hooks := make([]*api.Hook, len(orgHooks)) for i, hook := range orgHooks { - hooks[i] = convert.ToHook(org.HomeLink(), hook) + hooks[i] = convert.ToHook(ctx.Org.Organization.HomeLink(), hook) } + + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, hooks) } diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index c70252158e..09acb0bf04 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -49,6 +49,13 @@ func ListLabels(ctx *context.APIContext) { return } + count, err := models.CountLabelsByOrgID(ctx.Org.Organization.ID) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, convert.ToLabelList(labels)) } diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 09abad2557..97940d5925 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -18,15 +18,21 @@ import ( // listMembers list an organization's members func listMembers(ctx *context.APIContext, publicOnly bool) { - var members []*models.User - - members, _, err := models.FindOrgMembers(&models.FindOrgMembersOpts{ + opts := &models.FindOrgMembersOpts{ OrgID: ctx.Org.Organization.ID, PublicOnly: publicOnly, ListOptions: utils.GetListOptions(ctx), - }) + } + + count, err := models.CountOrgMembers(opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUsersByIDs", err) + ctx.InternalServerError(err) + return + } + + members, _, err := models.FindOrgMembers(opts) + if err != nil { + ctx.InternalServerError(err) return } @@ -35,6 +41,7 @@ func listMembers(ctx *context.APIContext, publicOnly bool) { apiMembers[i] = convert.ToUser(member, ctx.User) } + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiMembers) } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 5c16594f89..39ce896cd6 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -6,7 +6,6 @@ package org import ( - "fmt" "net/http" "code.gitea.io/gitea/models" @@ -38,9 +37,8 @@ func listUserOrgs(ctx *context.APIContext, u *models.User) { apiOrgs[i] = convert.ToOrganization(orgs[i]) } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", maxResults)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetLinkHeader(maxResults, listOptions.PageSize) + ctx.SetTotalCountHeader(int64(maxResults)) ctx.JSON(http.StatusOK, &apiOrgs) } @@ -145,8 +143,7 @@ func GetAll(ctx *context.APIContext) { } ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", maxResults)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &orgs) } diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 7998bb249f..a84763d6f6 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -6,7 +6,6 @@ package org import ( - "fmt" "net/http" "code.gitea.io/gitea/models" @@ -44,23 +43,27 @@ func ListTeams(ctx *context.APIContext) { // "200": // "$ref": "#/responses/TeamList" - org := ctx.Org.Organization - if err := org.GetTeams(&models.SearchTeamOptions{ + teams, count, err := models.SearchTeam(&models.SearchTeamOptions{ ListOptions: utils.GetListOptions(ctx), - }); err != nil { - ctx.Error(http.StatusInternalServerError, "GetTeams", err) + OrgID: ctx.Org.Organization.ID, + }) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "LoadTeams", err) return } - apiTeams := make([]*api.Team, len(org.Teams)) - for i := range org.Teams { - if err := org.Teams[i].GetUnits(); err != nil { + apiTeams := make([]*api.Team, len(teams)) + for i := range teams { + if err := teams[i].GetUnits(); err != nil { ctx.Error(http.StatusInternalServerError, "GetUnits", err) return } - apiTeams[i] = convert.ToTeam(org.Teams[i]) + apiTeams[i] = convert.ToTeam(teams[i]) } + + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiTeams) } @@ -84,7 +87,10 @@ func ListUserTeams(ctx *context.APIContext) { // "200": // "$ref": "#/responses/TeamList" - teams, err := models.GetUserTeams(ctx.User.ID, utils.GetListOptions(ctx)) + teams, count, err := models.SearchTeam(&models.SearchTeamOptions{ + ListOptions: utils.GetListOptions(ctx), + UserID: ctx.User.ID, + }) if err != nil { ctx.Error(http.StatusInternalServerError, "GetUserTeams", err) return @@ -106,6 +112,8 @@ func ListUserTeams(ctx *context.APIContext) { apiTeams[i] = convert.ToTeam(teams[i]) apiTeams[i].Organization = apiOrg } + + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiTeams) } @@ -327,17 +335,19 @@ func GetTeamMembers(ctx *context.APIContext) { ctx.NotFound() return } - team := ctx.Org.Team - if err := team.GetMembers(&models.SearchMembersOptions{ + + if err := ctx.Org.Team.GetMembers(&models.SearchMembersOptions{ ListOptions: utils.GetListOptions(ctx), }); err != nil { ctx.Error(http.StatusInternalServerError, "GetTeamMembers", err) return } - members := make([]*api.User, len(team.Members)) - for i, member := range team.Members { + members := make([]*api.User, len(ctx.Org.Team.Members)) + for i, member := range ctx.Org.Team.Members { members[i] = convert.ToUser(member, ctx.User) } + + ctx.SetTotalCountHeader(int64(ctx.Org.Team.NumMembers)) ctx.JSON(http.StatusOK, members) } @@ -687,8 +697,7 @@ func SearchTeam(ctx *context.APIContext) { } ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", maxResults)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, map[string]interface{}{ "ok": true, "data": apiTeams, diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 85c1681dfe..8653b0bc80 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -282,9 +282,8 @@ func ListBranches(ctx *context.APIContext) { } } - ctx.SetLinkHeader(int(totalNumOfBranches), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", totalNumOfBranches)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetLinkHeader(totalNumOfBranches, listOptions.PageSize) + ctx.SetTotalCountHeader(int64(totalNumOfBranches)) ctx.JSON(http.StatusOK, &apiBranches) } diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 078af1f6ff..d636220f62 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -47,15 +47,24 @@ func ListCollaborators(ctx *context.APIContext) { // "200": // "$ref": "#/responses/UserList" + count, err := ctx.Repo.Repository.CountCollaborators() + if err != nil { + ctx.InternalServerError(err) + return + } + collaborators, err := ctx.Repo.Repository.GetCollaborators(utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "ListCollaborators", err) return } + users := make([]*api.User, len(collaborators)) for i, collaborator := range collaborators { users[i] = convert.ToUser(collaborator.User, ctx.User) } + + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, users) } diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index f2fd901efa..975b9cab2a 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -200,16 +200,16 @@ func GetAllCommits(ctx *context.APIContext) { } } + ctx.SetLinkHeader(int(commitsCountTotal), listOptions.PageSize) + ctx.SetTotalCountHeader(commitsCountTotal) + // kept for backwards compatibility ctx.Header().Set("X-Page", strconv.Itoa(listOptions.Page)) ctx.Header().Set("X-PerPage", strconv.Itoa(listOptions.PageSize)) ctx.Header().Set("X-Total", strconv.FormatInt(commitsCountTotal, 10)) ctx.Header().Set("X-PageCount", strconv.Itoa(pageCount)) ctx.Header().Set("X-HasMore", strconv.FormatBool(listOptions.Page < pageCount)) - - ctx.SetLinkHeader(int(commitsCountTotal), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", commitsCountTotal)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, X-PerPage, X-Total, X-PageCount, X-HasMore, Link") + ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-Total", "X-PageCount", "X-HasMore") ctx.JSON(http.StatusOK, &apiCommits) } diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 55fcefaccc..a3f9aa14f9 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -62,6 +62,8 @@ func ListForks(ctx *context.APIContext) { } apiForks[i] = convert.ToRepo(fork, access) } + + ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks)) ctx.JSON(http.StatusOK, apiForks) } diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index da0a2c501c..74860fd72f 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -48,9 +48,20 @@ func ListHooks(ctx *context.APIContext) { // "200": // "$ref": "#/responses/HookList" - hooks, err := models.GetWebhooksByRepoID(ctx.Repo.Repository.ID, utils.GetListOptions(ctx)) + opts := &models.ListWebhookOptions{ + ListOptions: utils.GetListOptions(ctx), + RepoID: ctx.Repo.Repository.ID, + } + + count, err := models.CountWebhooksByOpts(opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetWebhooksByRepoID", err) + ctx.InternalServerError(err) + return + } + + hooks, err := models.ListWebhooksByOpts(opts) + if err != nil { + ctx.InternalServerError(err) return } @@ -58,6 +69,8 @@ func ListHooks(ctx *context.APIContext) { for i := range hooks { apiHooks[i] = convert.ToHook(ctx.Repo.RepoLink, hooks[i]) } + + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, &apiHooks) } diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 7395d4fdd4..ff003b840b 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -232,8 +232,7 @@ func SearchIssues(ctx *context.APIContext) { } ctx.SetLinkHeader(int(filteredCount), setting.UI.IssuePagingNum) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", filteredCount)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(filteredCount) ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues)) } @@ -442,8 +441,7 @@ func ListIssues(ctx *context.APIContext) { } ctx.SetLinkHeader(int(filteredCount), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", filteredCount)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(filteredCount) ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues)) } diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index d62ca81314..13e7de46b1 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -68,17 +68,25 @@ func ListIssueComments(ctx *context.APIContext) { } issue.Repo = ctx.Repo.Repository - comments, err := models.FindComments(models.FindCommentsOptions{ + opts := &models.FindCommentsOptions{ IssueID: issue.ID, Since: since, Before: before, Type: models.CommentTypeComment, - }) + } + + comments, err := models.FindComments(opts) if err != nil { ctx.Error(http.StatusInternalServerError, "FindComments", err) return } + totalCount, err := models.CountComments(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + if err := models.CommentList(comments).LoadPosters(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadPosters", err) return @@ -89,6 +97,8 @@ func ListIssueComments(ctx *context.APIContext) { comment.Issue = issue apiComments[i] = convert.ToComment(comments[i]) } + + ctx.SetTotalCountHeader(totalCount) ctx.JSON(http.StatusOK, &apiComments) } @@ -138,18 +148,26 @@ func ListRepoIssueComments(ctx *context.APIContext) { return } - comments, err := models.FindComments(models.FindCommentsOptions{ + opts := &models.FindCommentsOptions{ ListOptions: utils.GetListOptions(ctx), RepoID: ctx.Repo.Repository.ID, Type: models.CommentTypeComment, Since: since, Before: before, - }) + } + + comments, err := models.FindComments(opts) if err != nil { ctx.Error(http.StatusInternalServerError, "FindComments", err) return } + totalCount, err := models.CountComments(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + if err = models.CommentList(comments).LoadPosters(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadPosters", err) return @@ -171,6 +189,8 @@ func ListRepoIssueComments(ctx *context.APIContext) { for i := range comments { apiComments[i] = convert.ToComment(comments[i]) } + + ctx.SetTotalCountHeader(totalCount) ctx.JSON(http.StatusOK, &apiComments) } diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go index a4a2261b9a..82a9ffe10b 100644 --- a/routers/api/v1/repo/issue_stopwatch.go +++ b/routers/api/v1/repo/issue_stopwatch.go @@ -225,11 +225,18 @@ func GetStopwatches(ctx *context.APIContext) { return } + count, err := models.CountUserStopwatches(ctx.User.ID) + if err != nil { + ctx.InternalServerError(err) + return + } + apiSWs, err := convert.ToStopWatches(sws) if err != nil { ctx.Error(http.StatusInternalServerError, "APIFormat", err) return } + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiSWs) } diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index b27b746eb2..e9d8fbab26 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -83,7 +83,7 @@ func ListTrackedTimes(ctx *context.APIContext) { return } - opts := models.FindTrackedTimesOptions{ + opts := &models.FindTrackedTimesOptions{ ListOptions: utils.GetListOptions(ctx), RepositoryID: ctx.Repo.Repository.ID, IssueID: issue.ID, @@ -119,6 +119,12 @@ func ListTrackedTimes(ctx *context.APIContext) { } } + count, err := models.CountTrackedTimes(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + trackedTimes, err := models.GetTrackedTimes(opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) @@ -128,6 +134,8 @@ func ListTrackedTimes(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) return } + + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes)) } @@ -423,7 +431,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { return } - opts := models.FindTrackedTimesOptions{ + opts := &models.FindTrackedTimesOptions{ UserID: user.ID, RepositoryID: ctx.Repo.Repository.ID, } @@ -493,7 +501,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { return } - opts := models.FindTrackedTimesOptions{ + opts := &models.FindTrackedTimesOptions{ ListOptions: utils.GetListOptions(ctx), RepositoryID: ctx.Repo.Repository.ID, } @@ -530,6 +538,12 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { } } + count, err := models.CountTrackedTimes(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + trackedTimes, err := models.GetTrackedTimes(opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) @@ -539,6 +553,8 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) return } + + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes)) } @@ -573,7 +589,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) { // "200": // "$ref": "#/responses/TrackedTimeList" - opts := models.FindTrackedTimesOptions{ + opts := &models.FindTrackedTimesOptions{ ListOptions: utils.GetListOptions(ctx), UserID: ctx.User.ID, } @@ -584,6 +600,12 @@ func ListMyTrackedTimes(ctx *context.APIContext) { return } + count, err := models.CountTrackedTimes(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + trackedTimes, err := models.GetTrackedTimes(opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err) @@ -595,5 +617,6 @@ func ListMyTrackedTimes(ctx *context.APIContext) { return } + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes)) } diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index 903cef7104..98ee2b4de5 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -75,26 +75,29 @@ func ListDeployKeys(ctx *context.APIContext) { // "200": // "$ref": "#/responses/DeployKeyList" - var keys []*models.DeployKey - var err error - - fingerprint := ctx.FormString("fingerprint") - keyID := ctx.FormInt64("key_id") - if fingerprint != "" || keyID != 0 { - keys, err = models.SearchDeployKeys(ctx.Repo.Repository.ID, keyID, fingerprint) - } else { - keys, err = models.ListDeployKeys(ctx.Repo.Repository.ID, utils.GetListOptions(ctx)) + opts := &models.ListDeployKeysOptions{ + ListOptions: utils.GetListOptions(ctx), + RepoID: ctx.Repo.Repository.ID, + KeyID: ctx.FormInt64("key_id"), + Fingerprint: ctx.FormString("fingerprint"), } + keys, err := models.ListDeployKeys(opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "ListDeployKeys", err) + ctx.InternalServerError(err) + return + } + + count, err := models.CountDeployKeys(opts) + if err != nil { + ctx.InternalServerError(err) return } apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name) apiKeys := make([]*api.DeployKey, len(keys)) for i := range keys { - if err = keys[i].GetContent(); err != nil { + if err := keys[i].GetContent(); err != nil { ctx.Error(http.StatusInternalServerError, "GetContent", err) return } @@ -104,6 +107,7 @@ func ListDeployKeys(ctx *context.APIContext) { } } + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, &apiKeys) } diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index ca0d8392b8..1de5705aa2 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -55,6 +55,13 @@ func ListLabels(ctx *context.APIContext) { return } + count, err := models.CountLabelsByRepoID(ctx.Repo.Repository.ID) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, convert.ToLabelList(labels)) } diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go index 07b3897bc1..be1da18c5d 100644 --- a/routers/api/v1/repo/milestone.go +++ b/routers/api/v1/repo/milestone.go @@ -57,7 +57,7 @@ func ListMilestones(ctx *context.APIContext) { // "200": // "$ref": "#/responses/MilestoneList" - milestones, err := models.GetMilestones(models.GetMilestonesOption{ + milestones, total, err := models.GetMilestones(models.GetMilestonesOption{ ListOptions: utils.GetListOptions(ctx), RepoID: ctx.Repo.Repository.ID, State: api.StateType(ctx.FormString("state")), @@ -72,6 +72,8 @@ func ListMilestones(ctx *context.APIContext) { for i := range milestones { apiMilestones[i] = convert.ToAPIMilestone(milestones[i]) } + + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, &apiMilestones) } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 9be6228bfd..0a903101c7 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -119,8 +119,7 @@ func ListPullRequests(ctx *context.APIContext) { } ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", maxResults)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &apiPrs) } @@ -1232,13 +1231,14 @@ func GetPullRequestCommits(ctx *context.APIContext) { apiCommits = append(apiCommits, apiCommit) } - ctx.SetLinkHeader(int(totalNumberOfCommits), listOptions.PageSize) + ctx.SetLinkHeader(totalNumberOfCommits, listOptions.PageSize) + ctx.SetTotalCountHeader(int64(totalNumberOfCommits)) ctx.Header().Set("X-Page", strconv.Itoa(listOptions.Page)) ctx.Header().Set("X-PerPage", strconv.Itoa(listOptions.PageSize)) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", totalNumberOfCommits)) ctx.Header().Set("X-PageCount", strconv.Itoa(totalNumberOfPages)) ctx.Header().Set("X-HasMore", strconv.FormatBool(listOptions.Page < totalNumberOfPages)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, X-PerPage, X-Total, X-PageCount, X-HasMore, Link") + ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-PageCount", "X-HasMore") + ctx.JSON(http.StatusOK, &apiCommits) } diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 323904f45c..55b5178305 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -78,14 +78,21 @@ func ListPullReviews(ctx *context.APIContext) { return } - allReviews, err := models.FindReviews(models.FindReviewOptions{ + opts := models.FindReviewOptions{ ListOptions: utils.GetListOptions(ctx), Type: models.ReviewTypeUnknown, IssueID: pr.IssueID, - }) + } + allReviews, err := models.FindReviews(opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindReviews", err) + ctx.InternalServerError(err) + return + } + + count, err := models.CountReviews(opts) + if err != nil { + ctx.InternalServerError(err) return } @@ -95,6 +102,7 @@ func ListPullReviews(ctx *context.APIContext) { return } + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, &apiReviews) } diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index 97b90079f2..6438c6e9b4 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -5,7 +5,6 @@ package repo import ( - "fmt" "net/http" "code.gitea.io/gitea/models" @@ -142,8 +141,7 @@ func ListReleases(ctx *context.APIContext) { } ctx.SetLinkHeader(int(filteredCount), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprint(filteredCount)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(filteredCount) ctx.JSON(http.StatusOK, rels) } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index d222c9b080..0f85d917d1 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -230,8 +230,7 @@ func Search(ctx *context.APIContext) { } ctx.SetLinkHeader(int(count), opts.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", count)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, api.SearchResults{ OK: true, Data: results, diff --git a/routers/api/v1/repo/star.go b/routers/api/v1/repo/star.go index 3af0a4ac12..5fa42c3244 100644 --- a/routers/api/v1/repo/star.go +++ b/routers/api/v1/repo/star.go @@ -52,5 +52,7 @@ func ListStargazers(ctx *context.APIContext) { for i, stargazer := range stargazers { users[i] = convert.ToUser(stargazer, ctx.User) } + + ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumStars)) ctx.JSON(http.StatusOK, users) } diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index 841f60bb56..b884432f73 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -204,8 +204,7 @@ func getCommitStatuses(ctx *context.APIContext, sha string) { } ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", maxResults)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, apiStatuses) } @@ -267,5 +266,6 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) { combiStatus := convert.ToCombinedStatus(statuses, convert.ToRepo(repo, ctx.Repo.AccessMode)) + // TODO: ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, combiStatus) } diff --git a/routers/api/v1/repo/subscriber.go b/routers/api/v1/repo/subscriber.go index 37bf3c29d4..dae92969ba 100644 --- a/routers/api/v1/repo/subscriber.go +++ b/routers/api/v1/repo/subscriber.go @@ -52,5 +52,7 @@ func ListSubscribers(ctx *context.APIContext) { for i, subscriber := range subscribers { users[i] = convert.ToUser(subscriber, ctx.User) } + + ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumWatches)) ctx.JSON(http.StatusOK, users) } diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index c95fb63f85..8d42e63a48 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -50,7 +50,7 @@ func ListTags(ctx *context.APIContext) { listOpts := utils.GetListOptions(ctx) - tags, err := ctx.Repo.GitRepo.GetTagInfos(listOpts.Page, listOpts.PageSize) + tags, total, err := ctx.Repo.GitRepo.GetTagInfos(listOpts.Page, listOpts.PageSize) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTags", err) return @@ -61,6 +61,7 @@ func ListTags(ctx *context.APIContext) { apiTags[i] = convert.ToTag(ctx.Repo.Repository, tags[i]) } + ctx.SetTotalCountHeader(int64(total)) ctx.JSON(http.StatusOK, &apiTags) } diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go index a7c52e0bcc..fc277cb3fe 100644 --- a/routers/api/v1/repo/topic.go +++ b/routers/api/v1/repo/topic.go @@ -47,12 +47,13 @@ func ListTopics(ctx *context.APIContext) { // "200": // "$ref": "#/responses/TopicNames" - topics, err := models.FindTopics(&models.FindTopicOptions{ + opts := &models.FindTopicOptions{ ListOptions: utils.GetListOptions(ctx), RepoID: ctx.Repo.Repository.ID, - }) + } + + topics, total, err := models.FindTopics(opts) if err != nil { - log.Error("ListTopics failed: %v", err) ctx.InternalServerError(err) return } @@ -61,6 +62,8 @@ func ListTopics(ctx *context.APIContext) { for i, topic := range topics { topicNames[i] = topic.Name } + + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, map[string]interface{}{ "topics": topicNames, }) @@ -164,15 +167,15 @@ func AddTopic(ctx *context.APIContext) { } // Prevent adding more topics than allowed to repo - topics, err := models.FindTopics(&models.FindTopicOptions{ + count, err := models.CountTopics(&models.FindTopicOptions{ RepoID: ctx.Repo.Repository.ID, }) if err != nil { - log.Error("AddTopic failed: %v", err) + log.Error("CountTopics failed: %v", err) ctx.InternalServerError(err) return } - if len(topics) >= 25 { + if count >= 25 { ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{ "message": "Exceeding maximum allowed topics per repo.", }) @@ -269,21 +272,13 @@ func TopicSearch(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - if ctx.User == nil { - ctx.Error(http.StatusForbidden, "UserIsNil", "Only owners could change the topics.") - return + opts := &models.FindTopicOptions{ + Keyword: ctx.FormString("q"), + ListOptions: utils.GetListOptions(ctx), } - kw := ctx.FormString("q") - - listOptions := utils.GetListOptions(ctx) - - topics, err := models.FindTopics(&models.FindTopicOptions{ - Keyword: kw, - ListOptions: listOptions, - }) + topics, total, err := models.FindTopics(opts) if err != nil { - log.Error("SearchTopics failed: %v", err) ctx.InternalServerError(err) return } @@ -292,6 +287,8 @@ func TopicSearch(ctx *context.APIContext) { for i, topic := range topics { topicResponses[i] = convert.ToTopicResponse(topic) } + + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, map[string]interface{}{ "topics": topicResponses, }) diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index afd209f2f0..f0f7cb4159 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -44,9 +44,16 @@ func ListAccessTokens(ctx *context.APIContext) { // "200": // "$ref": "#/responses/AccessTokenList" - tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.User.ID, ListOptions: utils.GetListOptions(ctx)}) + opts := models.ListAccessTokensOptions{UserID: ctx.User.ID, ListOptions: utils.GetListOptions(ctx)} + + count, err := models.CountAccessTokens(opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "ListAccessTokens", err) + ctx.InternalServerError(err) + return + } + tokens, err := models.ListAccessTokens(opts) + if err != nil { + ctx.InternalServerError(err) return } @@ -58,6 +65,8 @@ func ListAccessTokens(ctx *context.APIContext) { TokenLastEight: tokens[i].TokenLastEight, } } + + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, &apiTokens) } @@ -242,7 +251,7 @@ func ListOauth2Applications(ctx *context.APIContext) { // "200": // "$ref": "#/responses/OAuth2ApplicationList" - apps, err := models.ListOAuth2Applications(ctx.User.ID, utils.GetListOptions(ctx)) + apps, total, err := models.ListOAuth2Applications(ctx.User.ID, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "ListOAuth2Applications", err) return @@ -253,6 +262,8 @@ func ListOauth2Applications(ctx *context.APIContext) { apiApps[i] = convert.ToOAuth2Application(apps[i]) apiApps[i].ClientSecret = "" // Hide secret on application list } + + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, &apiApps) } diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 4d316425cd..e273ac6a02 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -29,6 +29,8 @@ func listUserFollowers(ctx *context.APIContext, u *models.User) { ctx.Error(http.StatusInternalServerError, "GetUserFollowers", err) return } + + ctx.SetTotalCountHeader(int64(u.NumFollowers)) responseAPIUsers(ctx, users) } @@ -93,6 +95,8 @@ func listUserFollowing(ctx *context.APIContext, u *models.User) { ctx.Error(http.StatusInternalServerError, "GetFollowing", err) return } + + ctx.SetTotalCountHeader(int64(u.NumFollowing)) responseAPIUsers(ctx, users) } diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index ec03e305ba..f32d60d038 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -28,6 +28,13 @@ func listGPGKeys(ctx *context.APIContext, uid int64, listOptions models.ListOpti apiKeys[i] = convert.ToGPGKey(keys[i]) } + total, err := models.CountUserGPGKeys(uid) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, &apiKeys) } diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index 04252524b7..36b1c17b89 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -47,6 +47,7 @@ func composePublicKeysAPILink() string { func listPublicKeys(ctx *context.APIContext, user *models.User) { var keys []*models.PublicKey var err error + var count int fingerprint := ctx.FormString("fingerprint") username := ctx.Params("username") @@ -60,7 +61,15 @@ func listPublicKeys(ctx *context.APIContext, user *models.User) { // Unrestricted keys, err = models.SearchPublicKey(0, fingerprint) } + count = len(keys) } else { + total, err2 := models.CountPublicKeys(user.ID) + if err2 != nil { + ctx.InternalServerError(err) + return + } + count = int(total) + // Use ListPublicKeys keys, err = models.ListPublicKeys(user.ID, utils.GetListOptions(ctx)) } @@ -79,6 +88,7 @@ func listPublicKeys(ctx *context.APIContext, user *models.User) { } } + ctx.SetTotalCountHeader(int64(count)) ctx.JSON(http.StatusOK, &apiKeys) } diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go index 0331abef10..5116c17ab2 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -6,7 +6,6 @@ package user import ( "net/http" - "strconv" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" @@ -43,8 +42,7 @@ func listUserRepos(ctx *context.APIContext, u *models.User, private bool) { } ctx.SetLinkHeader(int(count), opts.PageSize) - ctx.Header().Set("X-Total-Count", strconv.FormatInt(count, 10)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, &apiRepos) } @@ -130,8 +128,7 @@ func ListMyRepos(ctx *context.APIContext) { } ctx.SetLinkHeader(int(count), opts.ListOptions.PageSize) - ctx.Header().Set("X-Total-Count", strconv.FormatInt(count, 10)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, &results) } diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index 937dcb477f..8ee1676856 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -92,6 +92,8 @@ func GetMyStarredRepos(ctx *context.APIContext) { if err != nil { ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) } + + ctx.SetTotalCountHeader(int64(ctx.User.NumStars)) ctx.JSON(http.StatusOK, &repos) } diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index e00c8d476d..a5e70de548 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -6,7 +6,6 @@ package user import ( - "fmt" "net/http" "code.gitea.io/gitea/models" @@ -73,8 +72,7 @@ func Search(ctx *context.APIContext) { } ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) - ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", maxResults)) - ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, map[string]interface{}{ "ok": true, diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index ab656f3229..f32ce73598 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -14,23 +14,22 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" ) -// getWatchedRepos returns the repos that the user with the specified userID is -// watching -func getWatchedRepos(user *models.User, private bool, listOptions models.ListOptions) ([]*api.Repository, error) { - watchedRepos, err := models.GetWatchedRepos(user.ID, private, listOptions) +// getWatchedRepos returns the repos that the user with the specified userID is watching +func getWatchedRepos(user *models.User, private bool, listOptions models.ListOptions) ([]*api.Repository, int64, error) { + watchedRepos, total, err := models.GetWatchedRepos(user.ID, private, listOptions) if err != nil { - return nil, err + return nil, 0, err } repos := make([]*api.Repository, len(watchedRepos)) for i, watched := range watchedRepos { access, err := models.AccessLevel(user, watched) if err != nil { - return nil, err + return nil, 0, err } repos[i] = convert.ToRepo(watched, access) } - return repos, nil + return repos, total, nil } // GetWatchedRepos returns the repos that the user specified in ctx is watching @@ -60,10 +59,12 @@ func GetWatchedRepos(ctx *context.APIContext) { user := GetUserByParams(ctx) private := user.ID == ctx.User.ID - repos, err := getWatchedRepos(user, private, utils.GetListOptions(ctx)) + repos, total, err := getWatchedRepos(user, private, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) } + + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, &repos) } @@ -87,10 +88,12 @@ func GetMyWatchedRepos(ctx *context.APIContext) { // "200": // "$ref": "#/responses/RepositoryList" - repos, err := getWatchedRepos(ctx.User, true, utils.GetListOptions(ctx)) + repos, total, err := getWatchedRepos(ctx.User, true, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) } + + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, &repos) } diff --git a/routers/web/org/home.go b/routers/web/org/home.go index da53053c7f..0c1f381d30 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -107,7 +107,7 @@ func Home(ctx *context.Context) { return } - var opts = models.FindOrgMembersOpts{ + var opts = &models.FindOrgMembersOpts{ OrgID: org.ID, PublicOnly: true, ListOptions: models.ListOptions{Page: 1, PageSize: 25}, @@ -122,7 +122,7 @@ func Home(ctx *context.Context) { opts.PublicOnly = !isMember && !ctx.User.IsAdmin } - members, _, err := models.FindOrgMembers(&opts) + members, _, err := models.FindOrgMembers(opts) if err != nil { ctx.ServerError("FindOrgMembers", err) return diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 2989637a61..84aaa28f60 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -31,7 +31,7 @@ func Members(ctx *context.Context) { page = 1 } - var opts = models.FindOrgMembersOpts{ + var opts = &models.FindOrgMembersOpts{ OrgID: org.ID, PublicOnly: true, } @@ -54,7 +54,7 @@ func Members(ctx *context.Context) { pager := context.NewPagination(int(total), setting.UI.MembersPagingNum, page, 5) opts.ListOptions.Page = page opts.ListOptions.PageSize = setting.UI.MembersPagingNum - members, membersIsPublic, err := models.FindOrgMembers(&opts) + members, membersIsPublic, err := models.FindOrgMembers(opts) if err != nil { ctx.ServerError("GetMembers", err) return diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index b21b94d64d..7e6fc5bf4c 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -186,7 +186,7 @@ func Webhooks(ctx *context.Context) { ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks" ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc") - ws, err := models.GetWebhooksByOrgID(ctx.Org.Organization.ID, models.ListOptions{}) + ws, err := models.ListWebhooksByOpts(&models.ListWebhookOptions{OrgID: ctx.Org.Organization.ID}) if err != nil { ctx.ServerError("GetWebhooksByOrgId", err) return diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 6050bb5c23..ff83be4410 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -378,7 +378,7 @@ func Issues(ctx *context.Context) { var err error // Get milestones - ctx.Data["Milestones"], err = models.GetMilestones(models.GetMilestonesOption{ + ctx.Data["Milestones"], _, err = models.GetMilestones(models.GetMilestonesOption{ RepoID: ctx.Repo.Repository.ID, State: api.StateType(ctx.FormString("state")), }) @@ -395,7 +395,7 @@ func Issues(ctx *context.Context) { // RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *models.Repository) { var err error - ctx.Data["OpenMilestones"], err = models.GetMilestones(models.GetMilestonesOption{ + ctx.Data["OpenMilestones"], _, err = models.GetMilestones(models.GetMilestonesOption{ RepoID: repo.ID, State: api.StateOpen, }) @@ -403,7 +403,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *models.Repos ctx.ServerError("GetMilestones", err) return } - ctx.Data["ClosedMilestones"], err = models.GetMilestones(models.GetMilestonesOption{ + ctx.Data["ClosedMilestones"], _, err = models.GetMilestones(models.GetMilestonesOption{ RepoID: repo.ID, State: api.StateClosed, }) @@ -1265,7 +1265,7 @@ func ViewIssue(ctx *context.Context) { } else { ctx.Data["CanUseTimetracker"] = false } - if ctx.Data["WorkingUsers"], err = models.TotalTimes(models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil { + if ctx.Data["WorkingUsers"], err = models.TotalTimes(&models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil { ctx.ServerError("TotalTimes", err) return } @@ -2584,8 +2584,8 @@ func handleTeamMentions(ctx *context.Context) { } if isAdmin { - if err := ctx.Repo.Owner.GetTeams(&models.SearchTeamOptions{}); err != nil { - ctx.ServerError("GetTeams", err) + if err := ctx.Repo.Owner.LoadTeams(); err != nil { + ctx.ServerError("LoadTeams", err) return } } else { diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index ca77e0976e..3ba27dd19a 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -53,17 +53,12 @@ func Milestones(ctx *context.Context) { page = 1 } - var total int - var state structs.StateType - if !isShowClosed { - total = int(stats.OpenCount) - state = structs.StateOpen - } else { - total = int(stats.ClosedCount) + state := structs.StateOpen + if isShowClosed { state = structs.StateClosed } - miles, err := models.GetMilestones(models.GetMilestonesOption{ + miles, total, err := models.GetMilestones(models.GetMilestonesOption{ ListOptions: models.ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, @@ -106,7 +101,7 @@ func Milestones(ctx *context.Context) { ctx.Data["Keyword"] = keyword ctx.Data["IsShowClosed"] = isShowClosed - pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5) + pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5) pager.AddParam(ctx, "state", "State") pager.AddParam(ctx, "q", "Keyword") ctx.Data["Page"] = pager diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index 2f15610737..20253a7cae 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -1002,7 +1002,7 @@ func DeployKeys(ctx *context.Context) { ctx.Data["PageIsSettingsKeys"] = true ctx.Data["DisableSSH"] = setting.SSH.Disabled - keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{}) + keys, err := models.ListDeployKeys(&models.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID}) if err != nil { ctx.ServerError("ListDeployKeys", err) return @@ -1018,7 +1018,7 @@ func DeployKeysPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") ctx.Data["PageIsSettingsKeys"] = true - keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{}) + keys, err := models.ListDeployKeys(&models.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID}) if err != nil { ctx.ServerError("ListDeployKeys", err) return diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 6c8645226f..2c703fc1af 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -674,7 +674,7 @@ func renderLanguageStats(ctx *context.Context) { } func renderRepoTopics(ctx *context.Context) { - topics, err := models.FindTopics(&models.FindTopicOptions{ + topics, _, err := models.FindTopics(&models.FindTopicOptions{ RepoID: ctx.Repo.Repository.ID, }) if err != nil { diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index 79f47470f2..0ceb9bc1dc 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -41,7 +41,7 @@ func Webhooks(ctx *context.Context) { ctx.Data["BaseLinkNew"] = ctx.Repo.RepoLink + "/settings/hooks" ctx.Data["Description"] = ctx.Tr("repo.settings.hooks_desc", "https://docs.gitea.io/en-us/webhooks/") - ws, err := models.GetWebhooksByRepoID(ctx.Repo.Repository.ID, models.ListOptions{}) + ws, err := models.ListWebhooksByOpts(&models.ListWebhookOptions{RepoID: ctx.Repo.Repository.ID}) if err != nil { ctx.ServerError("GetWebhooksByRepoID", err) return diff --git a/services/pull/review.go b/services/pull/review.go index b07e21fad9..3aa4570620 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -132,7 +132,7 @@ func createCodeComment(doer *models.User, repo *models.Repository, issue *models head := pr.GetGitRefName() if line > 0 { if reviewID != 0 { - first, err := models.FindComments(models.FindCommentsOptions{ + first, err := models.FindComments(&models.FindCommentsOptions{ ReviewID: reviewID, Line: line, TreePath: treePath, diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 46002c895c..00b2ef05b8 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -14,6 +14,8 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/sync" + "code.gitea.io/gitea/modules/util" + "github.com/gobwas/glob" ) @@ -187,7 +189,10 @@ func PrepareWebhooks(repo *models.Repository, event models.HookEventType, p api. } func prepareWebhooks(repo *models.Repository, event models.HookEventType, p api.Payloader) error { - ws, err := models.GetActiveWebhooksByRepoID(repo.ID) + ws, err := models.ListWebhooksByOpts(&models.ListWebhookOptions{ + RepoID: repo.ID, + IsActive: util.OptionalBoolTrue, + }) if err != nil { return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err) } @@ -195,7 +200,10 @@ func prepareWebhooks(repo *models.Repository, event models.HookEventType, p api. // check if repo belongs to org and append additional webhooks if repo.MustOwner().IsOrganization() { // get hooks for org - orgHooks, err := models.GetActiveWebhooksByOrgID(repo.OwnerID) + orgHooks, err := models.ListWebhooksByOpts(&models.ListWebhookOptions{ + OrgID: repo.OwnerID, + IsActive: util.OptionalBoolTrue, + }) if err != nil { return fmt.Errorf("GetActiveWebhooksByOrgID: %v", err) }