mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 22:13:26 +09:00
fix: avoid enumerating every public repository in issue search (#38992)
Both issue search endpoints resolve their repository filter with `SearchRepositoryIDs` and pass the result to the indexer as `RepoIDs`. They mean to leave public repositories to the indexer, but `SearchRepoOptions.AllPublic` is only read when `OwnerID > 0`, so without an `owner` filter the flag does nothing and every public repository is enumerated, without a `LIMIT`, into `repo_id IN (...)`. Those IDs are redundant, as `allPublic` is passed to the indexer, which already matches every public repository. On a large instance this binds tens of thousands of parameters and can fail in the driver, making the endpoint return 500 for every filter. Admins are worst hit, as `SearchRepositoryCondition` skips their accessible-repository condition and enumerates the whole table. Restrict the enumeration to private repositories. The result set is unchanged, as the dropped IDs are a subset of what `allPublic` matches. Both endpoints held copies of this block, so it moves to `routers/common`. --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
co-authored by
silverwind
wxiaoguang
parent
943f026844
commit
475e51c7e0
@@ -768,6 +768,15 @@ func PublicRepoUnderPublicOwnerCond() builder.Cond {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotPublicRepoUnderPublicOwnerCond complements PublicRepoUnderPublicOwnerCond. Spelled positively so
|
||||||
|
// the owner subquery hashes the limited/private minority, not every public user.
|
||||||
|
func NotPublicRepoUnderPublicOwnerCond() builder.Cond {
|
||||||
|
return builder.Or(
|
||||||
|
builder.Eq{"`repository`.is_private": true},
|
||||||
|
builder.In("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Neq{"visibility": structs.VisibleTypePublic})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// UserActionsAccessibleOwnerRepoCond selects the repos owned by ownerID whose Actions `user` may read.
|
// UserActionsAccessibleOwnerRepoCond selects the repos owned by ownerID whose Actions `user` may read.
|
||||||
// It is used to list an org/user's Actions runs and jobs (see the callers in routers/api/v1/shared).
|
// It is used to list an org/user's Actions runs and jobs (see the callers in routers/api/v1/shared).
|
||||||
// - owner_id = ownerID: only that owner's repos.
|
// - owner_id = ownerID: only that owner's repos.
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
issues_model "gitea.dev/models/issues"
|
issues_model "gitea.dev/models/issues"
|
||||||
"gitea.dev/models/organization"
|
|
||||||
access_model "gitea.dev/models/perm/access"
|
access_model "gitea.dev/models/perm/access"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unit"
|
"gitea.dev/models/unit"
|
||||||
@@ -33,61 +32,6 @@ import (
|
|||||||
issue_service "gitea.dev/services/issue"
|
issue_service "gitea.dev/services/issue"
|
||||||
)
|
)
|
||||||
|
|
||||||
// buildSearchIssuesRepoIDs builds the list of repository IDs for issue search based on query parameters.
|
|
||||||
// It returns repoIDs, allPublic flag, and any error that occurred.
|
|
||||||
func buildSearchIssuesRepoIDs(ctx *context.APIContext) (repoIDs []int64, allPublic bool, err error) {
|
|
||||||
opts := repo_model.SearchRepoOptions{
|
|
||||||
Private: false,
|
|
||||||
AllPublic: true,
|
|
||||||
TopicOnly: false,
|
|
||||||
Collaborate: optional.None[bool](),
|
|
||||||
// This needs to be a column that is not nil in fixtures or
|
|
||||||
// MySQL will return different results when sorting by null in some cases
|
|
||||||
OrderBy: db.SearchOrderByAlphabetically,
|
|
||||||
Actor: ctx.Doer,
|
|
||||||
}
|
|
||||||
if ctx.IsSigned {
|
|
||||||
opts.Private = true
|
|
||||||
opts.AllLimited = true
|
|
||||||
}
|
|
||||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
|
||||||
if ctx.FormString("owner") != "" {
|
|
||||||
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
opts.OwnerID = owner.ID
|
|
||||||
opts.AllLimited = false
|
|
||||||
opts.AllPublic = false
|
|
||||||
opts.Collaborate = optional.Some(false)
|
|
||||||
}
|
|
||||||
if ctx.FormString("team") != "" {
|
|
||||||
if ctx.FormString("owner") == "" {
|
|
||||||
return nil, false, util.NewInvalidArgumentErrorf("owner organisation is required for filtering on team")
|
|
||||||
}
|
|
||||||
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
opts.TeamID = team.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.AllPublic {
|
|
||||||
allPublic = true
|
|
||||||
opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
|
|
||||||
}
|
|
||||||
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
if len(repoIDs) == 0 {
|
|
||||||
// no repos found, don't let the indexer return all repos
|
|
||||||
repoIDs = []int64{0}
|
|
||||||
}
|
|
||||||
|
|
||||||
return repoIDs, allPublic, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchIssues searches for issues across the repositories that the user has access to
|
// SearchIssues searches for issues across the repositories that the user has access to
|
||||||
func SearchIssues(ctx *context.APIContext) {
|
func SearchIssues(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /repos/issues/search issue issueSearchIssues
|
// swagger:operation GET /repos/issues/search issue issueSearchIssues
|
||||||
@@ -193,7 +137,12 @@ func SearchIssues(ctx *context.APIContext) {
|
|||||||
|
|
||||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||||
|
|
||||||
repoIDs, allPublic, err := buildSearchIssuesRepoIDs(ctx)
|
repoIDs, allPublic, err := common.SearchIssuesRepoIDs(ctx, common.SearchIssuesRepoIDsOptions{
|
||||||
|
Doer: ctx.Doer,
|
||||||
|
PublicOnly: ctx.PublicOnly,
|
||||||
|
OwnerName: ctx.FormString("owner"),
|
||||||
|
TeamName: ctx.FormString("team"),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
|
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
|
||||||
ctx.APIError(http.StatusBadRequest, err.Error())
|
ctx.APIError(http.StatusBadRequest, err.Error())
|
||||||
@@ -204,10 +153,6 @@ func SearchIssues(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyword := ctx.FormTrim("q")
|
keyword := ctx.FormTrim("q")
|
||||||
if strings.IndexByte(keyword, 0) >= 0 {
|
|
||||||
keyword = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
|
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
|
||||||
|
|
||||||
var includedAnyLabels []int64
|
var includedAnyLabels []int64
|
||||||
@@ -390,9 +335,6 @@ func ListIssues(ctx *context.APIContext) {
|
|||||||
|
|
||||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||||
keyword := ctx.FormTrim("q")
|
keyword := ctx.FormTrim("q")
|
||||||
if strings.IndexByte(keyword, 0) >= 0 {
|
|
||||||
keyword = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var labelIDs []int64
|
var labelIDs []int64
|
||||||
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
|
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
|
||||||
@@ -435,13 +377,7 @@ func ListIssues(ctx *context.APIContext) {
|
|||||||
|
|
||||||
listOptions := utils.GetListOptions(ctx)
|
listOptions := utils.GetListOptions(ctx)
|
||||||
|
|
||||||
isPull := optional.None[bool]()
|
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
|
||||||
switch ctx.FormString("type") {
|
|
||||||
case "pulls":
|
|
||||||
isPull = optional.Some(true)
|
|
||||||
case "issues":
|
|
||||||
isPull = optional.Some(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isPull.Has() && !ctx.Repo.Permission.CanReadIssuesOrPulls(isPull.Value()) {
|
if isPull.Has() && !ctx.Repo.Permission.CanReadIssuesOrPulls(isPull.Value()) {
|
||||||
ctx.APIErrorNotFound()
|
ctx.APIErrorNotFound()
|
||||||
|
|||||||
@@ -4,7 +4,13 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.dev/models/organization"
|
||||||
|
repo_model "gitea.dev/models/repo"
|
||||||
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/optional"
|
"gitea.dev/modules/optional"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseIssueFilterStateIsClosed(state string) optional.Option[bool] {
|
func ParseIssueFilterStateIsClosed(state string) optional.Option[bool] {
|
||||||
@@ -23,3 +29,59 @@ func ParseIssueFilterStateIsClosed(state string) optional.Option[bool] {
|
|||||||
func ParseIssueFilterTypeIsPull(typ string) optional.Option[bool] {
|
func ParseIssueFilterTypeIsPull(typ string) optional.Option[bool] {
|
||||||
return optional.FromMapLookup(map[string]bool{"pulls": true, "issues": false}, typ)
|
return optional.FromMapLookup(map[string]bool{"pulls": true, "issues": false}, typ)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SearchIssuesRepoIDsOptions struct {
|
||||||
|
Doer *user_model.User
|
||||||
|
PublicOnly bool
|
||||||
|
OwnerName string
|
||||||
|
TeamName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchIssuesRepoIDs resolves the repository filter of an issue search. allPublic makes the indexer
|
||||||
|
// match everything its own is_public covers (modules/indexer/issues/util.go), so repoIDs omits those.
|
||||||
|
func SearchIssuesRepoIDs(ctx context.Context, opts SearchIssuesRepoIDsOptions) (repoIDs []int64, allPublic bool, err error) {
|
||||||
|
searchOpts := repo_model.SearchRepoOptions{
|
||||||
|
Private: opts.Doer != nil,
|
||||||
|
Collaborate: optional.None[bool](),
|
||||||
|
Actor: opts.Doer,
|
||||||
|
}
|
||||||
|
searchOpts.ApplyPublicOnly(opts.PublicOnly)
|
||||||
|
if opts.OwnerName != "" {
|
||||||
|
owner, err := user_model.GetUserByName(ctx, opts.OwnerName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
searchOpts.OwnerID = owner.ID
|
||||||
|
searchOpts.Collaborate = optional.Some(false)
|
||||||
|
}
|
||||||
|
if opts.TeamName != "" {
|
||||||
|
if opts.OwnerName == "" {
|
||||||
|
return nil, false, util.NewInvalidArgumentErrorf("owner organisation is required for filtering on team")
|
||||||
|
}
|
||||||
|
team, err := organization.GetTeam(ctx, searchOpts.OwnerID, opts.TeamName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
searchOpts.TeamID = team.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchRepoOptions.AllPublic and AllLimited only apply under an owner filter, so the indexer covers them
|
||||||
|
allPublic = opts.OwnerName == ""
|
||||||
|
cond := repo_model.SearchRepositoryCondition(searchOpts)
|
||||||
|
if allPublic {
|
||||||
|
if !searchOpts.Private {
|
||||||
|
return []int64{0}, allPublic, nil // sees nothing beyond is_public, so skip the query
|
||||||
|
}
|
||||||
|
cond = cond.And(repo_model.NotPublicRepoUnderPublicOwnerCond()) // enumerating them scales with the instance
|
||||||
|
}
|
||||||
|
repoIDs, err = repo_model.SearchRepositoryIDsByCondition(ctx, cond)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if len(repoIDs) == 0 {
|
||||||
|
// no repos found, don't let the indexer return all repos
|
||||||
|
repoIDs = []int64{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
return repoIDs, allPublic, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/models/unittest"
|
||||||
|
user_model "gitea.dev/models/user"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSearchIssuesRepoIDs(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
// the indexer's is_public covers repo 1 (public under a public owner) but misses repo 38 (public
|
||||||
|
// under a limited org) and repo 40 (public under a private org)
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
doerID int64
|
||||||
|
opts SearchIssuesRepoIDsOptions
|
||||||
|
allPublic bool
|
||||||
|
want []int64
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "site admin", // admins skip the accessible repository condition entirely
|
||||||
|
doerID: 1,
|
||||||
|
allPublic: true,
|
||||||
|
want: []int64{2, 38, 40},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regular user",
|
||||||
|
doerID: 2,
|
||||||
|
allPublic: true,
|
||||||
|
want: []int64{2, 38},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "private org member",
|
||||||
|
doerID: 5,
|
||||||
|
allPublic: true,
|
||||||
|
want: []int64{38, 40},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "anonymous",
|
||||||
|
allPublic: true,
|
||||||
|
want: []int64{0}, // the placeholder keeps the indexer off "every repository"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "public-only token",
|
||||||
|
doerID: 2,
|
||||||
|
opts: SearchIssuesRepoIDsOptions{PublicOnly: true},
|
||||||
|
allPublic: true,
|
||||||
|
want: []int64{0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "owner filter", // turns allPublic off, so public repos must still be enumerated
|
||||||
|
doerID: 2,
|
||||||
|
opts: SearchIssuesRepoIDsOptions{OwnerName: "user2"},
|
||||||
|
want: []int64{1, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "team without owner",
|
||||||
|
opts: SearchIssuesRepoIDsOptions{TeamName: "team1"},
|
||||||
|
wantErr: util.ErrInvalidArgument,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
opts := tc.opts
|
||||||
|
if tc.doerID != 0 {
|
||||||
|
opts.Doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.doerID})
|
||||||
|
}
|
||||||
|
|
||||||
|
repoIDs, allPublic, err := SearchIssuesRepoIDs(t.Context(), opts)
|
||||||
|
if tc.wantErr != nil {
|
||||||
|
assert.ErrorIs(t, err, tc.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.allPublic, allPublic)
|
||||||
|
assert.Subset(t, repoIDs, tc.want)
|
||||||
|
if allPublic {
|
||||||
|
assert.NotContains(t, repoIDs, int64(1), "already matched by the indexer's is_public")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -14,7 +15,6 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
issues_model "gitea.dev/models/issues"
|
issues_model "gitea.dev/models/issues"
|
||||||
"gitea.dev/models/organization"
|
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
issue_indexer "gitea.dev/modules/indexer/issues"
|
issue_indexer "gitea.dev/modules/indexer/issues"
|
||||||
@@ -53,85 +53,22 @@ func SearchIssues(ctx *context.Context) {
|
|||||||
|
|
||||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||||
|
|
||||||
var (
|
repoIDs, allPublic, err := common.SearchIssuesRepoIDs(ctx, common.SearchIssuesRepoIDsOptions{
|
||||||
repoIDs []int64
|
Doer: ctx.Doer,
|
||||||
allPublic bool
|
OwnerName: ctx.FormString("owner"),
|
||||||
)
|
TeamName: ctx.FormString("team"),
|
||||||
{
|
})
|
||||||
// find repos user can access (for issue search)
|
if err != nil {
|
||||||
opts := repo_model.SearchRepoOptions{
|
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
|
||||||
Private: false,
|
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||||
AllPublic: true,
|
} else {
|
||||||
TopicOnly: false,
|
ctx.ServerError("SearchIssuesRepoIDs", err)
|
||||||
Collaborate: optional.None[bool](),
|
|
||||||
// This needs to be a column that is not nil in fixtures or
|
|
||||||
// MySQL will return different results when sorting by null in some cases
|
|
||||||
OrderBy: db.SearchOrderByAlphabetically,
|
|
||||||
Actor: ctx.Doer,
|
|
||||||
}
|
|
||||||
if ctx.IsSigned {
|
|
||||||
opts.Private = true
|
|
||||||
opts.AllLimited = true
|
|
||||||
}
|
|
||||||
if ctx.FormString("owner") != "" {
|
|
||||||
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
|
|
||||||
if err != nil {
|
|
||||||
if user_model.IsErrUserNotExist(err) {
|
|
||||||
ctx.HTTPError(http.StatusBadRequest, "Owner not found", err.Error())
|
|
||||||
} else {
|
|
||||||
ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
opts.OwnerID = owner.ID
|
|
||||||
opts.AllLimited = false
|
|
||||||
opts.AllPublic = false
|
|
||||||
opts.Collaborate = optional.Some(false)
|
|
||||||
}
|
|
||||||
if ctx.FormString("team") != "" {
|
|
||||||
if ctx.FormString("owner") == "" {
|
|
||||||
ctx.HTTPError(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
|
|
||||||
if err != nil {
|
|
||||||
if organization.IsErrTeamNotExist(err) {
|
|
||||||
ctx.HTTPError(http.StatusBadRequest, "Team not found", err.Error())
|
|
||||||
} else {
|
|
||||||
ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
opts.TeamID = team.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.AllPublic {
|
|
||||||
allPublic = true
|
|
||||||
opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
|
|
||||||
}
|
|
||||||
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
|
|
||||||
if err != nil {
|
|
||||||
ctx.HTTPError(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(repoIDs) == 0 {
|
|
||||||
// no repos found, don't let the indexer return all repos
|
|
||||||
repoIDs = []int64{0}
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
keyword := ctx.FormTrim("q")
|
keyword := ctx.FormTrim("q")
|
||||||
if strings.IndexByte(keyword, 0) >= 0 {
|
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
|
||||||
keyword = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
isPull := optional.None[bool]()
|
|
||||||
switch ctx.FormString("type") {
|
|
||||||
case "pulls":
|
|
||||||
isPull = optional.Some(true)
|
|
||||||
case "issues":
|
|
||||||
isPull = optional.Some(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
var includedAnyLabels []int64
|
var includedAnyLabels []int64
|
||||||
{
|
{
|
||||||
@@ -142,7 +79,7 @@ func SearchIssues(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
|
includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.HTTPError(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error())
|
ctx.ServerError("GetLabelIDsByNames", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +93,7 @@ func SearchIssues(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
|
includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.HTTPError(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error())
|
ctx.ServerError("GetMilestoneIDsByNames", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,12 +157,12 @@ func SearchIssues(ctx *context.Context) {
|
|||||||
|
|
||||||
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error())
|
ctx.ServerError("SearchIssues", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
|
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
|
ctx.ServerError("FindIssuesByIDs", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,11 +201,7 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||||
|
|
||||||
keyword := ctx.FormTrim("q")
|
keyword := ctx.FormTrim("q")
|
||||||
if strings.IndexByte(keyword, 0) >= 0 {
|
|
||||||
keyword = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var mileIDs []int64
|
var mileIDs []int64
|
||||||
if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
|
if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
|
||||||
@@ -300,13 +233,7 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isPull := optional.None[bool]()
|
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
|
||||||
switch ctx.FormString("type") {
|
|
||||||
case "pulls":
|
|
||||||
isPull = optional.Some(true)
|
|
||||||
case "issues":
|
|
||||||
isPull = optional.Some(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: we should be more efficient here
|
// FIXME: we should be more efficient here
|
||||||
createdByID := getUserIDForFilter(ctx, "created_by")
|
createdByID := getUserIDForFilter(ctx, "created_by")
|
||||||
|
|||||||
Reference in New Issue
Block a user