code.gitea.io/gitea@v1.22.3/models/repo/repo_list.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repo
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"strings"
    10  
    11  	"code.gitea.io/gitea/models/db"
    12  	"code.gitea.io/gitea/models/perm"
    13  	"code.gitea.io/gitea/models/unit"
    14  	user_model "code.gitea.io/gitea/models/user"
    15  	"code.gitea.io/gitea/modules/container"
    16  	"code.gitea.io/gitea/modules/optional"
    17  	"code.gitea.io/gitea/modules/setting"
    18  	"code.gitea.io/gitea/modules/structs"
    19  	"code.gitea.io/gitea/modules/util"
    20  
    21  	"xorm.io/builder"
    22  )
    23  
    24  // FindReposMapByIDs find repos as map
    25  func FindReposMapByIDs(ctx context.Context, repoIDs []int64, res map[int64]*Repository) error {
    26  	return db.GetEngine(ctx).In("id", repoIDs).Find(&res)
    27  }
    28  
    29  // RepositoryListDefaultPageSize is the default number of repositories
    30  // to load in memory when running administrative tasks on all (or almost
    31  // all) of them.
    32  // The number should be low enough to avoid filling up all RAM with
    33  // repository data...
    34  const RepositoryListDefaultPageSize = 64
    35  
    36  // RepositoryList contains a list of repositories
    37  type RepositoryList []*Repository
    38  
    39  func (repos RepositoryList) Len() int {
    40  	return len(repos)
    41  }
    42  
    43  func (repos RepositoryList) Less(i, j int) bool {
    44  	return repos[i].FullName() < repos[j].FullName()
    45  }
    46  
    47  func (repos RepositoryList) Swap(i, j int) {
    48  	repos[i], repos[j] = repos[j], repos[i]
    49  }
    50  
    51  // ValuesRepository converts a repository map to a list
    52  // FIXME: Remove in favor of maps.values when MIN_GO_VERSION >= 1.18
    53  func ValuesRepository(m map[int64]*Repository) []*Repository {
    54  	values := make([]*Repository, 0, len(m))
    55  	for _, v := range m {
    56  		values = append(values, v)
    57  	}
    58  	return values
    59  }
    60  
    61  // RepositoryListOfMap make list from values of map
    62  func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
    63  	return RepositoryList(ValuesRepository(repoMap))
    64  }
    65  
    66  func (repos RepositoryList) LoadUnits(ctx context.Context) error {
    67  	if len(repos) == 0 {
    68  		return nil
    69  	}
    70  
    71  	// Load units.
    72  	units := make([]*RepoUnit, 0, len(repos)*6)
    73  	if err := db.GetEngine(ctx).
    74  		In("repo_id", repos.IDs()).
    75  		Find(&units); err != nil {
    76  		return fmt.Errorf("find units: %w", err)
    77  	}
    78  
    79  	unitsMap := make(map[int64][]*RepoUnit, len(repos))
    80  	for _, unit := range units {
    81  		if !unit.Type.UnitGlobalDisabled() {
    82  			unitsMap[unit.RepoID] = append(unitsMap[unit.RepoID], unit)
    83  		}
    84  	}
    85  
    86  	for _, repo := range repos {
    87  		repo.Units = unitsMap[repo.ID]
    88  	}
    89  
    90  	return nil
    91  }
    92  
    93  func (repos RepositoryList) IDs() []int64 {
    94  	repoIDs := make([]int64, len(repos))
    95  	for i := range repos {
    96  		repoIDs[i] = repos[i].ID
    97  	}
    98  	return repoIDs
    99  }
   100  
   101  // LoadAttributes loads the attributes for the given RepositoryList
   102  func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
   103  	if len(repos) == 0 {
   104  		return nil
   105  	}
   106  
   107  	userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) {
   108  		return repo.OwnerID, true
   109  	})
   110  	repoIDs := make([]int64, len(repos))
   111  	for i := range repos {
   112  		repoIDs[i] = repos[i].ID
   113  	}
   114  
   115  	// Load owners.
   116  	users := make(map[int64]*user_model.User, len(userIDs))
   117  	if err := db.GetEngine(ctx).
   118  		Where("id > 0").
   119  		In("id", userIDs).
   120  		Find(&users); err != nil {
   121  		return fmt.Errorf("find users: %w", err)
   122  	}
   123  	for i := range repos {
   124  		repos[i].Owner = users[repos[i].OwnerID]
   125  	}
   126  
   127  	// Load primary language.
   128  	stats := make(LanguageStatList, 0, len(repos))
   129  	if err := db.GetEngine(ctx).
   130  		Where("`is_primary` = ? AND `language` != ?", true, "other").
   131  		In("`repo_id`", repoIDs).
   132  		Find(&stats); err != nil {
   133  		return fmt.Errorf("find primary languages: %w", err)
   134  	}
   135  	stats.LoadAttributes()
   136  	for i := range repos {
   137  		for _, st := range stats {
   138  			if st.RepoID == repos[i].ID {
   139  				repos[i].PrimaryLanguage = st
   140  				break
   141  			}
   142  		}
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  // SearchRepoOptions holds the search options
   149  type SearchRepoOptions struct {
   150  	db.ListOptions
   151  	Actor           *user_model.User
   152  	Keyword         string
   153  	OwnerID         int64
   154  	PriorityOwnerID int64
   155  	TeamID          int64
   156  	OrderBy         db.SearchOrderBy
   157  	Private         bool // Include private repositories in results
   158  	StarredByID     int64
   159  	WatchedByID     int64
   160  	AllPublic       bool // Include also all public repositories of users and public organisations
   161  	AllLimited      bool // Include also all public repositories of limited organisations
   162  	// None -> include public and private
   163  	// True -> include just private
   164  	// False -> include just public
   165  	IsPrivate optional.Option[bool]
   166  	// None -> include collaborative AND non-collaborative
   167  	// True -> include just collaborative
   168  	// False -> include just non-collaborative
   169  	Collaborate optional.Option[bool]
   170  	// What type of unit the user can be collaborative in,
   171  	// it is ignored if Collaborate is False.
   172  	// TypeInvalid means any unit type.
   173  	UnitType unit.Type
   174  	// None -> include forks AND non-forks
   175  	// True -> include just forks
   176  	// False -> include just non-forks
   177  	Fork optional.Option[bool]
   178  	// If Fork option is True, you can use this option to limit the forks of a special repo by repo id.
   179  	ForkFrom int64
   180  	// None -> include templates AND non-templates
   181  	// True -> include just templates
   182  	// False -> include just non-templates
   183  	Template optional.Option[bool]
   184  	// None -> include mirrors AND non-mirrors
   185  	// True -> include just mirrors
   186  	// False -> include just non-mirrors
   187  	Mirror optional.Option[bool]
   188  	// None -> include archived AND non-archived
   189  	// True -> include just archived
   190  	// False -> include just non-archived
   191  	Archived optional.Option[bool]
   192  	// only search topic name
   193  	TopicOnly bool
   194  	// only search repositories with specified primary language
   195  	Language string
   196  	// include description in keyword search
   197  	IncludeDescription bool
   198  	// None -> include has milestones AND has no milestone
   199  	// True -> include just has milestones
   200  	// False -> include just has no milestone
   201  	HasMilestones optional.Option[bool]
   202  	// LowerNames represents valid lower names to restrict to
   203  	LowerNames []string
   204  	// When specified true, apply some filters over the conditions:
   205  	// - Don't show forks, when opts.Fork is OptionalBoolNone.
   206  	// - Do not display repositories that don't have a description, an icon and topics.
   207  	OnlyShowRelevant bool
   208  }
   209  
   210  // SearchOrderBy is used to sort the result
   211  type SearchOrderBy string
   212  
   213  func (s SearchOrderBy) String() string {
   214  	return string(s)
   215  }
   216  
   217  // Strings for sorting result
   218  const (
   219  	SearchOrderByAlphabetically        SearchOrderBy = "name ASC"
   220  	SearchOrderByAlphabeticallyReverse SearchOrderBy = "name DESC"
   221  	SearchOrderByLeastUpdated          SearchOrderBy = "updated_unix ASC"
   222  	SearchOrderByRecentUpdated         SearchOrderBy = "updated_unix DESC"
   223  	SearchOrderByOldest                SearchOrderBy = "created_unix ASC"
   224  	SearchOrderByNewest                SearchOrderBy = "created_unix DESC"
   225  	SearchOrderBySize                  SearchOrderBy = "size ASC"
   226  	SearchOrderBySizeReverse           SearchOrderBy = "size DESC"
   227  	SearchOrderByID                    SearchOrderBy = "id ASC"
   228  	SearchOrderByIDReverse             SearchOrderBy = "id DESC"
   229  	SearchOrderByStars                 SearchOrderBy = "num_stars ASC"
   230  	SearchOrderByStarsReverse          SearchOrderBy = "num_stars DESC"
   231  	SearchOrderByForks                 SearchOrderBy = "num_forks ASC"
   232  	SearchOrderByForksReverse          SearchOrderBy = "num_forks DESC"
   233  )
   234  
   235  // UserOwnedRepoCond returns user ownered repositories
   236  func UserOwnedRepoCond(userID int64) builder.Cond {
   237  	return builder.Eq{
   238  		"repository.owner_id": userID,
   239  	}
   240  }
   241  
   242  // UserAssignedRepoCond return user as assignee repositories list
   243  func UserAssignedRepoCond(id string, userID int64) builder.Cond {
   244  	return builder.And(
   245  		builder.Eq{
   246  			"repository.is_private": false,
   247  		},
   248  		builder.In(id,
   249  			builder.Select("issue.repo_id").From("issue_assignees").
   250  				InnerJoin("issue", "issue.id = issue_assignees.issue_id").
   251  				Where(builder.Eq{
   252  					"issue_assignees.assignee_id": userID,
   253  				}),
   254  		),
   255  	)
   256  }
   257  
   258  // UserCreateIssueRepoCond return user created issues repositories list
   259  func UserCreateIssueRepoCond(id string, userID int64, isPull bool) builder.Cond {
   260  	return builder.And(
   261  		builder.Eq{
   262  			"repository.is_private": false,
   263  		},
   264  		builder.In(id,
   265  			builder.Select("issue.repo_id").From("issue").
   266  				Where(builder.Eq{
   267  					"issue.poster_id": userID,
   268  					"issue.is_pull":   isPull,
   269  				}),
   270  		),
   271  	)
   272  }
   273  
   274  // UserMentionedRepoCond return user metinoed repositories list
   275  func UserMentionedRepoCond(id string, userID int64) builder.Cond {
   276  	return builder.And(
   277  		builder.Eq{
   278  			"repository.is_private": false,
   279  		},
   280  		builder.In(id,
   281  			builder.Select("issue.repo_id").From("issue_user").
   282  				InnerJoin("issue", "issue.id = issue_user.issue_id").
   283  				Where(builder.Eq{
   284  					"issue_user.is_mentioned": true,
   285  					"issue_user.uid":          userID,
   286  				}),
   287  		),
   288  	)
   289  }
   290  
   291  // UserAccessRepoCond returns a condition for selecting all repositories a user has unit independent access to
   292  func UserAccessRepoCond(idStr string, userID int64) builder.Cond {
   293  	return builder.In(idStr, builder.Select("repo_id").
   294  		From("`access`").
   295  		Where(builder.And(
   296  			builder.Eq{"`access`.user_id": userID},
   297  			builder.Gt{"`access`.mode": int(perm.AccessModeNone)},
   298  		)),
   299  	)
   300  }
   301  
   302  // userCollaborationRepoCond returns a condition for selecting all repositories a user is collaborator in
   303  func UserCollaborationRepoCond(idStr string, userID int64) builder.Cond {
   304  	return builder.In(idStr, builder.Select("repo_id").
   305  		From("`collaboration`").
   306  		Where(builder.And(
   307  			builder.Eq{"`collaboration`.user_id": userID},
   308  		)),
   309  	)
   310  }
   311  
   312  // UserOrgTeamRepoCond selects repos that the given user has access to through team membership
   313  func UserOrgTeamRepoCond(idStr string, userID int64) builder.Cond {
   314  	return builder.In(idStr, userOrgTeamRepoBuilder(userID))
   315  }
   316  
   317  // userOrgTeamRepoBuilder returns repo ids where user's teams can access.
   318  func userOrgTeamRepoBuilder(userID int64) *builder.Builder {
   319  	return builder.Select("`team_repo`.repo_id").
   320  		From("team_repo").
   321  		Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id").
   322  		Where(builder.Eq{"`team_user`.uid": userID})
   323  }
   324  
   325  // userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit.
   326  func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder {
   327  	return userOrgTeamRepoBuilder(userID).
   328  		Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id").
   329  		Where(builder.Eq{"`team_unit`.`type`": unitType}).
   330  		And(builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)})
   331  }
   332  
   333  // userOrgTeamUnitRepoCond returns a condition to select repo ids where user's teams can access the special unit.
   334  func userOrgTeamUnitRepoCond(idStr string, userID int64, unitType unit.Type) builder.Cond {
   335  	return builder.In(idStr, userOrgTeamUnitRepoBuilder(userID, unitType))
   336  }
   337  
   338  // UserOrgUnitRepoCond selects repos that the given user has access to through org and the special unit
   339  func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond {
   340  	return builder.In(idStr,
   341  		userOrgTeamUnitRepoBuilder(userID, unitType).
   342  			And(builder.Eq{"`team_unit`.org_id": orgID}),
   343  	)
   344  }
   345  
   346  // userOrgPublicRepoCond returns the condition that one user could access all public repositories in organizations
   347  func userOrgPublicRepoCond(userID int64) builder.Cond {
   348  	return builder.And(
   349  		builder.Eq{"`repository`.is_private": false},
   350  		builder.In("`repository`.owner_id",
   351  			builder.Select("`org_user`.org_id").
   352  				From("org_user").
   353  				Where(builder.Eq{"`org_user`.uid": userID}),
   354  		),
   355  	)
   356  }
   357  
   358  // userOrgPublicRepoCondPrivate returns the condition that one user could access all public repositories in private organizations
   359  func userOrgPublicRepoCondPrivate(userID int64) builder.Cond {
   360  	return builder.And(
   361  		builder.Eq{"`repository`.is_private": false},
   362  		builder.In("`repository`.owner_id",
   363  			builder.Select("`org_user`.org_id").
   364  				From("org_user").
   365  				Join("INNER", "`user`", "`user`.id = `org_user`.org_id").
   366  				Where(builder.Eq{
   367  					"`org_user`.uid":    userID,
   368  					"`user`.`type`":     user_model.UserTypeOrganization,
   369  					"`user`.visibility": structs.VisibleTypePrivate,
   370  				}),
   371  		),
   372  	)
   373  }
   374  
   375  // UserOrgPublicUnitRepoCond returns the condition that one user could access all public repositories in the special organization
   376  func UserOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond {
   377  	return userOrgPublicRepoCond(userID).
   378  		And(builder.Eq{"`repository`.owner_id": orgID})
   379  }
   380  
   381  // SearchRepositoryCondition creates a query condition according search repository options
   382  func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
   383  	cond := builder.NewCond()
   384  
   385  	if opts.Private {
   386  		if opts.Actor != nil && !opts.Actor.IsAdmin && opts.Actor.ID != opts.OwnerID {
   387  			// OK we're in the context of a User
   388  			cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
   389  		}
   390  	} else {
   391  		// Not looking at private organisations and users
   392  		// We should be able to see all non-private repositories that
   393  		// isn't in a private or limited organisation.
   394  		cond = cond.And(
   395  			builder.Eq{"is_private": false},
   396  			builder.NotIn("owner_id", builder.Select("id").From("`user`").Where(
   397  				builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}),
   398  			)))
   399  	}
   400  
   401  	if opts.IsPrivate.Has() {
   402  		cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.Value()})
   403  	}
   404  
   405  	if opts.Template.Has() {
   406  		cond = cond.And(builder.Eq{"is_template": opts.Template.Value()})
   407  	}
   408  
   409  	// Restrict to starred repositories
   410  	if opts.StarredByID > 0 {
   411  		cond = cond.And(builder.In("id", builder.Select("repo_id").From("star").Where(builder.Eq{"uid": opts.StarredByID})))
   412  	}
   413  
   414  	// Restrict to watched repositories
   415  	if opts.WatchedByID > 0 {
   416  		cond = cond.And(builder.In("id", builder.Select("repo_id").From("watch").Where(builder.Eq{"user_id": opts.WatchedByID})))
   417  	}
   418  
   419  	// Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate
   420  	if opts.OwnerID > 0 {
   421  		accessCond := builder.NewCond()
   422  		if !opts.Collaborate.Value() {
   423  			accessCond = builder.Eq{"owner_id": opts.OwnerID}
   424  		}
   425  
   426  		if opts.Collaborate.ValueOrDefault(true) {
   427  			// A Collaboration is:
   428  
   429  			collaborateCond := builder.NewCond()
   430  			// 1. Repository we don't own
   431  			collaborateCond = collaborateCond.And(builder.Neq{"owner_id": opts.OwnerID})
   432  			// 2. But we can see because of:
   433  			{
   434  				userAccessCond := builder.NewCond()
   435  				// A. We have unit independent access
   436  				userAccessCond = userAccessCond.Or(UserAccessRepoCond("`repository`.id", opts.OwnerID))
   437  				// B. We are in a team for
   438  				if opts.UnitType == unit.TypeInvalid {
   439  					userAccessCond = userAccessCond.Or(UserOrgTeamRepoCond("`repository`.id", opts.OwnerID))
   440  				} else {
   441  					userAccessCond = userAccessCond.Or(userOrgTeamUnitRepoCond("`repository`.id", opts.OwnerID, opts.UnitType))
   442  				}
   443  				// C. Public repositories in organizations that we are member of
   444  				userAccessCond = userAccessCond.Or(userOrgPublicRepoCondPrivate(opts.OwnerID))
   445  				collaborateCond = collaborateCond.And(userAccessCond)
   446  			}
   447  			if !opts.Private {
   448  				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))
   449  			}
   450  
   451  			accessCond = accessCond.Or(collaborateCond)
   452  		}
   453  
   454  		if opts.AllPublic {
   455  			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}))))
   456  		}
   457  
   458  		if opts.AllLimited {
   459  			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}))))
   460  		}
   461  
   462  		cond = cond.And(accessCond)
   463  	}
   464  
   465  	if opts.TeamID > 0 {
   466  		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})))
   467  	}
   468  
   469  	if opts.Keyword != "" {
   470  		// separate keyword
   471  		subQueryCond := builder.NewCond()
   472  		for _, v := range strings.Split(opts.Keyword, ",") {
   473  			if opts.TopicOnly {
   474  				subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)})
   475  			} else {
   476  				subQueryCond = subQueryCond.Or(builder.Like{"topic.name", strings.ToLower(v)})
   477  			}
   478  		}
   479  		subQuery := builder.Select("repo_topic.repo_id").From("repo_topic").
   480  			Join("INNER", "topic", "topic.id = repo_topic.topic_id").
   481  			Where(subQueryCond).
   482  			GroupBy("repo_topic.repo_id")
   483  
   484  		keywordCond := builder.In("id", subQuery)
   485  		if !opts.TopicOnly {
   486  			likes := builder.NewCond()
   487  			for _, v := range strings.Split(opts.Keyword, ",") {
   488  				likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)})
   489  
   490  				// If the string looks like "org/repo", match against that pattern too
   491  				if opts.TeamID == 0 && strings.Count(opts.Keyword, "/") == 1 {
   492  					pieces := strings.Split(opts.Keyword, "/")
   493  					ownerName := pieces[0]
   494  					repoName := pieces[1]
   495  					likes = likes.Or(builder.And(builder.Like{"owner_name", strings.ToLower(ownerName)}, builder.Like{"lower_name", strings.ToLower(repoName)}))
   496  				}
   497  
   498  				if opts.IncludeDescription {
   499  					likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)})
   500  				}
   501  			}
   502  			keywordCond = keywordCond.Or(likes)
   503  		}
   504  		cond = cond.And(keywordCond)
   505  	}
   506  
   507  	if opts.Language != "" {
   508  		cond = cond.And(builder.In("id", builder.
   509  			Select("repo_id").
   510  			From("language_stat").
   511  			Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true})))
   512  	}
   513  
   514  	if opts.Fork.Has() || opts.OnlyShowRelevant {
   515  		if opts.OnlyShowRelevant && !opts.Fork.Has() {
   516  			cond = cond.And(builder.Eq{"is_fork": false})
   517  		} else {
   518  			cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()})
   519  
   520  			if opts.ForkFrom > 0 && opts.Fork.Value() {
   521  				cond = cond.And(builder.Eq{"fork_id": opts.ForkFrom})
   522  			}
   523  		}
   524  	}
   525  
   526  	if opts.Mirror.Has() {
   527  		cond = cond.And(builder.Eq{"is_mirror": opts.Mirror.Value()})
   528  	}
   529  
   530  	if opts.Actor != nil && opts.Actor.IsRestricted {
   531  		cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
   532  	}
   533  
   534  	if opts.Archived.Has() {
   535  		cond = cond.And(builder.Eq{"is_archived": opts.Archived.Value()})
   536  	}
   537  
   538  	if opts.HasMilestones.Has() {
   539  		if opts.HasMilestones.Value() {
   540  			cond = cond.And(builder.Gt{"num_milestones": 0})
   541  		} else {
   542  			cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"}))
   543  		}
   544  	}
   545  
   546  	if opts.OnlyShowRelevant {
   547  		// Only show a repo that has at least a topic, an icon, or a description
   548  		subQueryCond := builder.NewCond()
   549  
   550  		// Topic checking. Topics are present.
   551  		if setting.Database.Type.IsPostgreSQL() { // postgres stores the topics as json and not as text
   552  			subQueryCond = subQueryCond.Or(builder.And(builder.NotNull{"topics"}, builder.Neq{"(topics)::text": "[]"}))
   553  		} else {
   554  			subQueryCond = subQueryCond.Or(builder.And(builder.Neq{"topics": "null"}, builder.Neq{"topics": "[]"}))
   555  		}
   556  
   557  		// Description checking. Description not empty
   558  		subQueryCond = subQueryCond.Or(builder.Neq{"description": ""})
   559  
   560  		// Repo has a avatar
   561  		subQueryCond = subQueryCond.Or(builder.Neq{"avatar": ""})
   562  
   563  		// Always hide repo's that are empty
   564  		subQueryCond = subQueryCond.And(builder.Eq{"is_empty": false})
   565  
   566  		cond = cond.And(subQueryCond)
   567  	}
   568  
   569  	return cond
   570  }
   571  
   572  // SearchRepository returns repositories based on search options,
   573  // it returns results in given range and number of total results.
   574  func SearchRepository(ctx context.Context, opts *SearchRepoOptions) (RepositoryList, int64, error) {
   575  	cond := SearchRepositoryCondition(opts)
   576  	return SearchRepositoryByCondition(ctx, opts, cond, true)
   577  }
   578  
   579  // CountRepository counts repositories based on search options,
   580  func CountRepository(ctx context.Context, opts *SearchRepoOptions) (int64, error) {
   581  	return db.GetEngine(ctx).Where(SearchRepositoryCondition(opts)).Count(new(Repository))
   582  }
   583  
   584  // SearchRepositoryByCondition search repositories by condition
   585  func SearchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) {
   586  	sess, count, err := searchRepositoryByCondition(ctx, opts, cond)
   587  	if err != nil {
   588  		return nil, 0, err
   589  	}
   590  
   591  	defaultSize := 50
   592  	if opts.PageSize > 0 {
   593  		defaultSize = opts.PageSize
   594  	}
   595  	repos := make(RepositoryList, 0, defaultSize)
   596  	if err := sess.Find(&repos); err != nil {
   597  		return nil, 0, fmt.Errorf("Repo: %w", err)
   598  	}
   599  
   600  	if opts.PageSize <= 0 {
   601  		count = int64(len(repos))
   602  	}
   603  
   604  	if loadAttributes {
   605  		if err := repos.LoadAttributes(ctx); err != nil {
   606  			return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
   607  		}
   608  	}
   609  
   610  	return repos, count, nil
   611  }
   612  
   613  func searchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, cond builder.Cond) (db.Engine, int64, error) {
   614  	if opts.Page <= 0 {
   615  		opts.Page = 1
   616  	}
   617  
   618  	if len(opts.OrderBy) == 0 {
   619  		opts.OrderBy = db.SearchOrderByAlphabetically
   620  	}
   621  
   622  	args := make([]any, 0)
   623  	if opts.PriorityOwnerID > 0 {
   624  		opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", opts.OrderBy))
   625  		args = append(args, opts.PriorityOwnerID)
   626  	} else if strings.Count(opts.Keyword, "/") == 1 {
   627  		// With "owner/repo" search times, prioritise results which match the owner field
   628  		orgName := strings.Split(opts.Keyword, "/")[0]
   629  		opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", opts.OrderBy))
   630  		args = append(args, orgName)
   631  	}
   632  
   633  	sess := db.GetEngine(ctx)
   634  
   635  	var count int64
   636  	if opts.PageSize > 0 {
   637  		var err error
   638  		count, err = sess.
   639  			Where(cond).
   640  			Count(new(Repository))
   641  		if err != nil {
   642  			return nil, 0, fmt.Errorf("Count: %w", err)
   643  		}
   644  	}
   645  
   646  	sess = sess.Where(cond).OrderBy(opts.OrderBy.String(), args...)
   647  	if opts.PageSize > 0 {
   648  		sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
   649  	}
   650  	return sess, count, nil
   651  }
   652  
   653  // SearchRepositoryIDsByCondition search repository IDs by given condition.
   654  func SearchRepositoryIDsByCondition(ctx context.Context, cond builder.Cond) ([]int64, error) {
   655  	repoIDs := make([]int64, 0, 10)
   656  	return repoIDs, db.GetEngine(ctx).
   657  		Table("repository").
   658  		Cols("id").
   659  		Where(cond).
   660  		Find(&repoIDs)
   661  }
   662  
   663  // AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
   664  func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond {
   665  	cond := builder.NewCond()
   666  
   667  	if user == nil || !user.IsRestricted || user.ID <= 0 {
   668  		orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate}
   669  		if user == nil || user.ID <= 0 {
   670  			orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited)
   671  		}
   672  		// 1. Be able to see all non-private repositories that either:
   673  		cond = cond.Or(builder.And(
   674  			builder.Eq{"`repository`.is_private": false},
   675  			// 2. Aren't in an private organisation or limited organisation if we're not logged in
   676  			builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(
   677  				builder.And(
   678  					builder.Eq{"type": user_model.UserTypeOrganization},
   679  					builder.In("visibility", orgVisibilityLimit)),
   680  			))))
   681  	}
   682  
   683  	if user != nil {
   684  		// 2. Be able to see all repositories that we have unit independent access to
   685  		// 3. Be able to see all repositories through team membership(s)
   686  		if unitType == unit.TypeInvalid {
   687  			// Regardless of UnitType
   688  			cond = cond.Or(
   689  				UserAccessRepoCond("`repository`.id", user.ID),
   690  				UserOrgTeamRepoCond("`repository`.id", user.ID),
   691  			)
   692  		} else {
   693  			// For a specific UnitType
   694  			cond = cond.Or(
   695  				UserCollaborationRepoCond("`repository`.id", user.ID),
   696  				userOrgTeamUnitRepoCond("`repository`.id", user.ID, unitType),
   697  			)
   698  		}
   699  		// 4. Repositories that we directly own
   700  		cond = cond.Or(builder.Eq{"`repository`.owner_id": user.ID})
   701  		if !user.IsRestricted {
   702  			// 5. Be able to see all public repos in private organizations that we are an org_user of
   703  			cond = cond.Or(userOrgPublicRepoCond(user.ID))
   704  		}
   705  	}
   706  
   707  	return cond
   708  }
   709  
   710  // SearchRepositoryByName takes keyword and part of repository name to search,
   711  // it returns results in given range and number of total results.
   712  func SearchRepositoryByName(ctx context.Context, opts *SearchRepoOptions) (RepositoryList, int64, error) {
   713  	opts.IncludeDescription = false
   714  	return SearchRepository(ctx, opts)
   715  }
   716  
   717  // SearchRepositoryIDs takes keyword and part of repository name to search,
   718  // it returns results in given range and number of total results.
   719  func SearchRepositoryIDs(ctx context.Context, opts *SearchRepoOptions) ([]int64, int64, error) {
   720  	opts.IncludeDescription = false
   721  
   722  	cond := SearchRepositoryCondition(opts)
   723  
   724  	sess, count, err := searchRepositoryByCondition(ctx, opts, cond)
   725  	if err != nil {
   726  		return nil, 0, err
   727  	}
   728  
   729  	defaultSize := 50
   730  	if opts.PageSize > 0 {
   731  		defaultSize = opts.PageSize
   732  	}
   733  
   734  	ids := make([]int64, 0, defaultSize)
   735  	err = sess.Select("id").Table("repository").Find(&ids)
   736  	if opts.PageSize <= 0 {
   737  		count = int64(len(ids))
   738  	}
   739  
   740  	return ids, count, err
   741  }
   742  
   743  // AccessibleRepoIDsQuery queries accessible repository ids. Usable as a subquery wherever repo ids need to be filtered.
   744  func AccessibleRepoIDsQuery(user *user_model.User) *builder.Builder {
   745  	// NB: Please note this code needs to still work if user is nil
   746  	return builder.Select("id").From("repository").Where(AccessibleRepositoryCondition(user, unit.TypeInvalid))
   747  }
   748  
   749  // FindUserCodeAccessibleRepoIDs finds all at Code level accessible repositories' ID by the user's id
   750  func FindUserCodeAccessibleRepoIDs(ctx context.Context, user *user_model.User) ([]int64, error) {
   751  	return SearchRepositoryIDsByCondition(ctx, AccessibleRepositoryCondition(user, unit.TypeCode))
   752  }
   753  
   754  // FindUserCodeAccessibleOwnerRepoIDs finds all repository IDs for the given owner whose code the user can see.
   755  func FindUserCodeAccessibleOwnerRepoIDs(ctx context.Context, ownerID int64, user *user_model.User) ([]int64, error) {
   756  	return SearchRepositoryIDsByCondition(ctx, builder.NewCond().And(
   757  		builder.Eq{"owner_id": ownerID},
   758  		AccessibleRepositoryCondition(user, unit.TypeCode),
   759  	))
   760  }
   761  
   762  // GetUserRepositories returns a list of repositories of given user.
   763  func GetUserRepositories(ctx context.Context, opts *SearchRepoOptions) (RepositoryList, int64, error) {
   764  	if len(opts.OrderBy) == 0 {
   765  		opts.OrderBy = "updated_unix DESC"
   766  	}
   767  
   768  	cond := builder.NewCond()
   769  	if opts.Actor == nil {
   770  		return nil, 0, util.NewInvalidArgumentErrorf("GetUserRepositories: Actor is needed but not given")
   771  	}
   772  	cond = cond.And(builder.Eq{"owner_id": opts.Actor.ID})
   773  	if !opts.Private {
   774  		cond = cond.And(builder.Eq{"is_private": false})
   775  	}
   776  
   777  	if opts.LowerNames != nil && len(opts.LowerNames) > 0 {
   778  		cond = cond.And(builder.In("lower_name", opts.LowerNames))
   779  	}
   780  
   781  	sess := db.GetEngine(ctx)
   782  
   783  	count, err := sess.Where(cond).Count(new(Repository))
   784  	if err != nil {
   785  		return nil, 0, fmt.Errorf("Count: %w", err)
   786  	}
   787  
   788  	sess = sess.Where(cond).OrderBy(opts.OrderBy.String())
   789  	repos := make(RepositoryList, 0, opts.PageSize)
   790  	return repos, count, db.SetSessionPagination(sess, opts).Find(&repos)
   791  }