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 }