gitea源码

repo_list.go 25KB

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