mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-30 12:21:06 +00:00
c1ca4a8313
/api/v1/repos/issues/search is a highly inefficient search which is unfortunately the basis for our dependency searching algorithm. In particular it currently loads all of the repositories and their owners and their primary coding language all of which is immediately thrown away. This PR makes one simple change - just get the IDs. Related #14560 Related #12827 Signed-off-by: Andrew Thornton <art27@cantab.net>
527 lines
16 KiB
Go
527 lines
16 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/util"
|
|
|
|
"xorm.io/builder"
|
|
"xorm.io/xorm"
|
|
)
|
|
|
|
// RepositoryListDefaultPageSize is the default number of repositories
|
|
// to load in memory when running administrative tasks on all (or almost
|
|
// all) of them.
|
|
// The number should be low enough to avoid filling up all RAM with
|
|
// repository data...
|
|
const RepositoryListDefaultPageSize = 64
|
|
|
|
// RepositoryList contains a list of repositories
|
|
type RepositoryList []*Repository
|
|
|
|
func (repos RepositoryList) Len() int {
|
|
return len(repos)
|
|
}
|
|
|
|
func (repos RepositoryList) Less(i, j int) bool {
|
|
return repos[i].FullName() < repos[j].FullName()
|
|
}
|
|
|
|
func (repos RepositoryList) Swap(i, j int) {
|
|
repos[i], repos[j] = repos[j], repos[i]
|
|
}
|
|
|
|
// RepositoryListOfMap make list from values of map
|
|
func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
|
|
return RepositoryList(valuesRepository(repoMap))
|
|
}
|
|
|
|
func (repos RepositoryList) loadAttributes(e Engine) error {
|
|
if len(repos) == 0 {
|
|
return nil
|
|
}
|
|
|
|
set := make(map[int64]struct{})
|
|
repoIDs := make([]int64, len(repos))
|
|
for i := range repos {
|
|
set[repos[i].OwnerID] = struct{}{}
|
|
repoIDs[i] = repos[i].ID
|
|
}
|
|
|
|
// Load owners.
|
|
users := make(map[int64]*User, len(set))
|
|
if err := e.
|
|
Where("id > 0").
|
|
In("id", keysInt64(set)).
|
|
Find(&users); err != nil {
|
|
return fmt.Errorf("find users: %v", err)
|
|
}
|
|
for i := range repos {
|
|
repos[i].Owner = users[repos[i].OwnerID]
|
|
}
|
|
|
|
// Load primary language.
|
|
stats := make(LanguageStatList, 0, len(repos))
|
|
if err := e.
|
|
Where("`is_primary` = ? AND `language` != ?", true, "other").
|
|
In("`repo_id`", repoIDs).
|
|
Find(&stats); err != nil {
|
|
return fmt.Errorf("find primary languages: %v", err)
|
|
}
|
|
stats.loadAttributes()
|
|
for i := range repos {
|
|
for _, st := range stats {
|
|
if st.RepoID == repos[i].ID {
|
|
repos[i].PrimaryLanguage = st
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadAttributes loads the attributes for the given RepositoryList
|
|
func (repos RepositoryList) LoadAttributes() error {
|
|
return repos.loadAttributes(x)
|
|
}
|
|
|
|
// MirrorRepositoryList contains the mirror repositories
|
|
type MirrorRepositoryList []*Repository
|
|
|
|
func (repos MirrorRepositoryList) loadAttributes(e Engine) error {
|
|
if len(repos) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Load mirrors.
|
|
repoIDs := make([]int64, 0, len(repos))
|
|
for i := range repos {
|
|
if !repos[i].IsMirror {
|
|
continue
|
|
}
|
|
|
|
repoIDs = append(repoIDs, repos[i].ID)
|
|
}
|
|
mirrors := make([]*Mirror, 0, len(repoIDs))
|
|
if err := e.
|
|
Where("id > 0").
|
|
In("repo_id", repoIDs).
|
|
Find(&mirrors); err != nil {
|
|
return fmt.Errorf("find mirrors: %v", err)
|
|
}
|
|
|
|
set := make(map[int64]*Mirror)
|
|
for i := range mirrors {
|
|
set[mirrors[i].RepoID] = mirrors[i]
|
|
}
|
|
for i := range repos {
|
|
repos[i].Mirror = set[repos[i].ID]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LoadAttributes loads the attributes for the given MirrorRepositoryList
|
|
func (repos MirrorRepositoryList) LoadAttributes() error {
|
|
return repos.loadAttributes(x)
|
|
}
|
|
|
|
// SearchRepoOptions holds the search options
|
|
type SearchRepoOptions struct {
|
|
ListOptions
|
|
Actor *User
|
|
Keyword string
|
|
OwnerID int64
|
|
PriorityOwnerID int64
|
|
TeamID int64
|
|
OrderBy SearchOrderBy
|
|
Private bool // Include private repositories in results
|
|
StarredByID int64
|
|
AllPublic bool // Include also all public repositories of users and public organisations
|
|
AllLimited bool // Include also all public repositories of limited organisations
|
|
// None -> include public and private
|
|
// True -> include just private
|
|
// False -> incude just public
|
|
IsPrivate util.OptionalBool
|
|
// None -> include collaborative AND non-collaborative
|
|
// True -> include just collaborative
|
|
// False -> incude just non-collaborative
|
|
Collaborate util.OptionalBool
|
|
// None -> include forks AND non-forks
|
|
// True -> include just forks
|
|
// False -> include just non-forks
|
|
Fork util.OptionalBool
|
|
// None -> include templates AND non-templates
|
|
// True -> include just templates
|
|
// False -> include just non-templates
|
|
Template util.OptionalBool
|
|
// None -> include mirrors AND non-mirrors
|
|
// True -> include just mirrors
|
|
// False -> include just non-mirrors
|
|
Mirror util.OptionalBool
|
|
// None -> include archived AND non-archived
|
|
// True -> include just archived
|
|
// False -> include just non-archived
|
|
Archived util.OptionalBool
|
|
// only search topic name
|
|
TopicOnly bool
|
|
// include description in keyword search
|
|
IncludeDescription bool
|
|
// None -> include has milestones AND has no milestone
|
|
// True -> include just has milestones
|
|
// False -> include just has no milestone
|
|
HasMilestones util.OptionalBool
|
|
// LowerNames represents valid lower names to restrict to
|
|
LowerNames []string
|
|
}
|
|
|
|
// SearchOrderBy is used to sort the result
|
|
type SearchOrderBy string
|
|
|
|
func (s SearchOrderBy) String() string {
|
|
return string(s)
|
|
}
|
|
|
|
// Strings for sorting result
|
|
const (
|
|
SearchOrderByAlphabetically SearchOrderBy = "name ASC"
|
|
SearchOrderByAlphabeticallyReverse SearchOrderBy = "name DESC"
|
|
SearchOrderByLeastUpdated SearchOrderBy = "updated_unix ASC"
|
|
SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC"
|
|
SearchOrderByOldest SearchOrderBy = "created_unix ASC"
|
|
SearchOrderByNewest SearchOrderBy = "created_unix DESC"
|
|
SearchOrderBySize SearchOrderBy = "size ASC"
|
|
SearchOrderBySizeReverse SearchOrderBy = "size DESC"
|
|
SearchOrderByID SearchOrderBy = "id ASC"
|
|
SearchOrderByIDReverse SearchOrderBy = "id DESC"
|
|
SearchOrderByStars SearchOrderBy = "num_stars ASC"
|
|
SearchOrderByStarsReverse SearchOrderBy = "num_stars DESC"
|
|
SearchOrderByForks SearchOrderBy = "num_forks ASC"
|
|
SearchOrderByForksReverse SearchOrderBy = "num_forks DESC"
|
|
)
|
|
|
|
// SearchRepositoryCondition creates a query condition according search repository options
|
|
func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
|
|
cond := builder.NewCond()
|
|
|
|
if opts.Private {
|
|
if opts.Actor != nil && !opts.Actor.IsAdmin && opts.Actor.ID != opts.OwnerID {
|
|
// OK we're in the context of a User
|
|
cond = cond.And(accessibleRepositoryCondition(opts.Actor))
|
|
}
|
|
} else {
|
|
// Not looking at private organisations
|
|
// We should be able to see all non-private repositories that
|
|
// isn't in a private or limited organisation.
|
|
cond = cond.And(
|
|
builder.Eq{"is_private": false},
|
|
builder.NotIn("owner_id", builder.Select("id").From("`user`").Where(
|
|
builder.And(
|
|
builder.Eq{"type": UserTypeOrganization},
|
|
builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}),
|
|
))))
|
|
}
|
|
|
|
if opts.IsPrivate != util.OptionalBoolNone {
|
|
cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.IsTrue()})
|
|
}
|
|
|
|
if opts.Template != util.OptionalBoolNone {
|
|
cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue})
|
|
}
|
|
|
|
// Restrict to starred repositories
|
|
if opts.StarredByID > 0 {
|
|
cond = cond.And(builder.In("id", builder.Select("repo_id").From("star").Where(builder.Eq{"uid": opts.StarredByID})))
|
|
}
|
|
|
|
// Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate
|
|
if opts.OwnerID > 0 {
|
|
accessCond := builder.NewCond()
|
|
if opts.Collaborate != util.OptionalBoolTrue {
|
|
accessCond = builder.Eq{"owner_id": opts.OwnerID}
|
|
}
|
|
|
|
if opts.Collaborate != util.OptionalBoolFalse {
|
|
// A Collaboration is:
|
|
collaborateCond := builder.And(
|
|
// 1. Repository we don't own
|
|
builder.Neq{"owner_id": opts.OwnerID},
|
|
// 2. But we can see because of:
|
|
builder.Or(
|
|
// A. We have access
|
|
builder.In("`repository`.id",
|
|
builder.Select("`access`.repo_id").
|
|
From("access").
|
|
Where(builder.Eq{"`access`.user_id": opts.OwnerID})),
|
|
// B. We are in a team for
|
|
builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").
|
|
From("team_repo").
|
|
Where(builder.Eq{"`team_user`.uid": opts.OwnerID}).
|
|
Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id")),
|
|
// C. Public repositories in private organizations that we are member of
|
|
builder.And(
|
|
builder.Eq{"`repository`.is_private": false},
|
|
builder.In("`repository`.owner_id",
|
|
builder.Select("`org_user`.org_id").
|
|
From("org_user").
|
|
Join("INNER", "`user`", "`user`.id = `org_user`.org_id").
|
|
Where(builder.Eq{
|
|
"`org_user`.uid": opts.OwnerID,
|
|
"`user`.type": UserTypeOrganization,
|
|
"`user`.visibility": structs.VisibleTypePrivate,
|
|
})))),
|
|
)
|
|
if !opts.Private {
|
|
collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false))
|
|
}
|
|
|
|
accessCond = accessCond.Or(collaborateCond)
|
|
}
|
|
|
|
if opts.AllPublic {
|
|
accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}))))
|
|
}
|
|
|
|
if opts.AllLimited {
|
|
accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypeLimited}))))
|
|
}
|
|
|
|
cond = cond.And(accessCond)
|
|
}
|
|
|
|
if opts.TeamID > 0 {
|
|
cond = cond.And(builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").From("team_repo").Where(builder.Eq{"`team_repo`.team_id": opts.TeamID})))
|
|
}
|
|
|
|
if opts.Keyword != "" {
|
|
// separate keyword
|
|
subQueryCond := builder.NewCond()
|
|
for _, v := range strings.Split(opts.Keyword, ",") {
|
|
if opts.TopicOnly {
|
|
subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)})
|
|
} else {
|
|
subQueryCond = subQueryCond.Or(builder.Like{"topic.name", strings.ToLower(v)})
|
|
}
|
|
}
|
|
subQuery := builder.Select("repo_topic.repo_id").From("repo_topic").
|
|
Join("INNER", "topic", "topic.id = repo_topic.topic_id").
|
|
Where(subQueryCond).
|
|
GroupBy("repo_topic.repo_id")
|
|
|
|
keywordCond := builder.In("id", subQuery)
|
|
if !opts.TopicOnly {
|
|
likes := builder.NewCond()
|
|
for _, v := range strings.Split(opts.Keyword, ",") {
|
|
likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)})
|
|
if opts.IncludeDescription {
|
|
likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)})
|
|
}
|
|
}
|
|
keywordCond = keywordCond.Or(likes)
|
|
}
|
|
cond = cond.And(keywordCond)
|
|
}
|
|
|
|
if opts.Fork != util.OptionalBoolNone {
|
|
cond = cond.And(builder.Eq{"is_fork": opts.Fork == util.OptionalBoolTrue})
|
|
}
|
|
|
|
if opts.Mirror != util.OptionalBoolNone {
|
|
cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue})
|
|
}
|
|
|
|
if opts.Actor != nil && opts.Actor.IsRestricted {
|
|
cond = cond.And(accessibleRepositoryCondition(opts.Actor))
|
|
}
|
|
|
|
if opts.Archived != util.OptionalBoolNone {
|
|
cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue})
|
|
}
|
|
|
|
switch opts.HasMilestones {
|
|
case util.OptionalBoolTrue:
|
|
cond = cond.And(builder.Gt{"num_milestones": 0})
|
|
case util.OptionalBoolFalse:
|
|
cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"}))
|
|
}
|
|
|
|
return cond
|
|
}
|
|
|
|
// SearchRepository returns repositories based on search options,
|
|
// it returns results in given range and number of total results.
|
|
func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) {
|
|
cond := SearchRepositoryCondition(opts)
|
|
return SearchRepositoryByCondition(opts, cond, true)
|
|
}
|
|
|
|
// SearchRepositoryByCondition search repositories by condition
|
|
func SearchRepositoryByCondition(opts *SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) {
|
|
sess, count, err := searchRepositoryByCondition(opts, cond)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer sess.Close()
|
|
|
|
defaultSize := 50
|
|
if opts.PageSize > 0 {
|
|
defaultSize = opts.PageSize
|
|
}
|
|
repos := make(RepositoryList, 0, defaultSize)
|
|
if err := sess.Find(&repos); err != nil {
|
|
return nil, 0, fmt.Errorf("Repo: %v", err)
|
|
}
|
|
|
|
if opts.PageSize <= 0 {
|
|
count = int64(len(repos))
|
|
}
|
|
|
|
if loadAttributes {
|
|
if err := repos.loadAttributes(sess); err != nil {
|
|
return nil, 0, fmt.Errorf("LoadAttributes: %v", err)
|
|
}
|
|
}
|
|
|
|
return repos, count, nil
|
|
}
|
|
|
|
func searchRepositoryByCondition(opts *SearchRepoOptions, cond builder.Cond) (*xorm.Session, int64, error) {
|
|
if opts.Page <= 0 {
|
|
opts.Page = 1
|
|
}
|
|
|
|
if len(opts.OrderBy) == 0 {
|
|
opts.OrderBy = SearchOrderByAlphabetically
|
|
}
|
|
|
|
if opts.PriorityOwnerID > 0 {
|
|
opts.OrderBy = SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = %d THEN 0 ELSE owner_id END, %s", opts.PriorityOwnerID, opts.OrderBy))
|
|
}
|
|
|
|
sess := x.NewSession()
|
|
|
|
var count int64
|
|
if opts.PageSize > 0 {
|
|
var err error
|
|
count, err = sess.
|
|
Where(cond).
|
|
Count(new(Repository))
|
|
if err != nil {
|
|
_ = sess.Close()
|
|
return nil, 0, fmt.Errorf("Count: %v", err)
|
|
}
|
|
}
|
|
|
|
sess.Where(cond).OrderBy(opts.OrderBy.String())
|
|
if opts.PageSize > 0 {
|
|
sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
|
}
|
|
return sess, count, nil
|
|
}
|
|
|
|
// accessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
|
|
func accessibleRepositoryCondition(user *User) builder.Cond {
|
|
cond := builder.NewCond()
|
|
|
|
if user == nil || !user.IsRestricted || user.ID <= 0 {
|
|
orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate}
|
|
if user == nil || user.ID <= 0 {
|
|
orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited)
|
|
}
|
|
// 1. Be able to see all non-private repositories that either:
|
|
cond = cond.Or(builder.And(
|
|
builder.Eq{"`repository`.is_private": false},
|
|
// 2. Aren't in an private organisation or limited organisation if we're not logged in
|
|
builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(
|
|
builder.And(
|
|
builder.Eq{"type": UserTypeOrganization},
|
|
builder.In("visibility", orgVisibilityLimit)),
|
|
))))
|
|
}
|
|
|
|
if user != nil {
|
|
cond = cond.Or(
|
|
// 2. Be able to see all repositories that we have access to
|
|
builder.In("`repository`.id", builder.Select("repo_id").
|
|
From("`access`").
|
|
Where(builder.And(
|
|
builder.Eq{"user_id": user.ID},
|
|
builder.Gt{"mode": int(AccessModeNone)}))),
|
|
// 3. Repositories that we directly own
|
|
builder.Eq{"`repository`.owner_id": user.ID},
|
|
// 4. Be able to see all repositories that we are in a team
|
|
builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").
|
|
From("team_repo").
|
|
Where(builder.Eq{"`team_user`.uid": user.ID}).
|
|
Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id")),
|
|
// 5. Be able to see all public repos in private organizations that we are an org_user of
|
|
builder.And(builder.Eq{"`repository`.is_private": false},
|
|
builder.In("`repository`.owner_id",
|
|
builder.Select("`org_user`.org_id").
|
|
From("org_user").
|
|
Where(builder.Eq{"`org_user`.uid": user.ID}))))
|
|
}
|
|
|
|
return cond
|
|
}
|
|
|
|
// SearchRepositoryByName takes keyword and part of repository name to search,
|
|
// it returns results in given range and number of total results.
|
|
func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, error) {
|
|
opts.IncludeDescription = false
|
|
return SearchRepository(opts)
|
|
}
|
|
|
|
// SearchRepositoryIDs takes keyword and part of repository name to search,
|
|
// it returns results in given range and number of total results.
|
|
func SearchRepositoryIDs(opts *SearchRepoOptions) ([]int64, int64, error) {
|
|
opts.IncludeDescription = false
|
|
|
|
cond := SearchRepositoryCondition(opts)
|
|
|
|
sess, count, err := searchRepositoryByCondition(opts, cond)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer sess.Close()
|
|
|
|
defaultSize := 50
|
|
if opts.PageSize > 0 {
|
|
defaultSize = opts.PageSize
|
|
}
|
|
|
|
ids := make([]int64, 0, defaultSize)
|
|
err = sess.Select("id").Table("repository").Find(&ids)
|
|
if opts.PageSize <= 0 {
|
|
count = int64(len(ids))
|
|
}
|
|
|
|
return ids, count, err
|
|
}
|
|
|
|
// AccessibleRepoIDsQuery queries accessible repository ids. Usable as a subquery wherever repo ids need to be filtered.
|
|
func AccessibleRepoIDsQuery(user *User) *builder.Builder {
|
|
// NB: Please note this code needs to still work if user is nil
|
|
return builder.Select("id").From("repository").Where(accessibleRepositoryCondition(user))
|
|
}
|
|
|
|
// FindUserAccessibleRepoIDs find all accessible repositories' ID by user's id
|
|
func FindUserAccessibleRepoIDs(user *User) ([]int64, error) {
|
|
repoIDs := make([]int64, 0, 10)
|
|
if err := x.
|
|
Table("repository").
|
|
Cols("id").
|
|
Where(accessibleRepositoryCondition(user)).
|
|
Find(&repoIDs); err != nil {
|
|
return nil, fmt.Errorf("FindUserAccesibleRepoIDs: %v", err)
|
|
}
|
|
return repoIDs, nil
|
|
}
|