code.gitea.io/gitea@v1.22.3/models/issues/issue_search.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package issues 5 6 import ( 7 "context" 8 "fmt" 9 "strings" 10 11 "code.gitea.io/gitea/models/db" 12 "code.gitea.io/gitea/models/organization" 13 repo_model "code.gitea.io/gitea/models/repo" 14 "code.gitea.io/gitea/models/unit" 15 user_model "code.gitea.io/gitea/models/user" 16 "code.gitea.io/gitea/modules/optional" 17 18 "xorm.io/builder" 19 "xorm.io/xorm" 20 ) 21 22 // IssuesOptions represents options of an issue. 23 type IssuesOptions struct { //nolint 24 Paginator *db.ListOptions 25 RepoIDs []int64 // overwrites RepoCond if the length is not 0 26 AllPublic bool // include also all public repositories 27 RepoCond builder.Cond 28 AssigneeID int64 29 PosterID int64 30 MentionedID int64 31 ReviewRequestedID int64 32 ReviewedID int64 33 SubscriberID int64 34 MilestoneIDs []int64 35 ProjectID int64 36 ProjectBoardID int64 37 IsClosed optional.Option[bool] 38 IsPull optional.Option[bool] 39 LabelIDs []int64 40 IncludedLabelNames []string 41 ExcludedLabelNames []string 42 IncludeMilestones []string 43 SortType string 44 IssueIDs []int64 45 UpdatedAfterUnix int64 46 UpdatedBeforeUnix int64 47 // prioritize issues from this repo 48 PriorityRepoID int64 49 IsArchived optional.Option[bool] 50 Org *organization.Organization // issues permission scope 51 Team *organization.Team // issues permission scope 52 User *user_model.User // issues permission scope 53 } 54 55 // applySorts sort an issues-related session based on the provided 56 // sortType string 57 func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) { 58 switch sortType { 59 case "oldest": 60 sess.Asc("issue.created_unix").Asc("issue.id") 61 case "recentupdate": 62 sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id") 63 case "leastupdate": 64 sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id") 65 case "mostcomment": 66 sess.Desc("issue.num_comments").Desc("issue.created_unix").Desc("issue.id") 67 case "leastcomment": 68 sess.Asc("issue.num_comments").Desc("issue.created_unix").Desc("issue.id") 69 case "priority": 70 sess.Desc("issue.priority").Desc("issue.created_unix").Desc("issue.id") 71 case "nearduedate": 72 // 253370764800 is 01/01/9999 @ 12:00am (UTC) 73 sess.Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). 74 OrderBy("CASE " + 75 "WHEN issue.deadline_unix = 0 AND (milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL) THEN 253370764800 " + 76 "WHEN milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL THEN issue.deadline_unix " + 77 "WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " + 78 "ELSE issue.deadline_unix END ASC"). 79 Desc("issue.created_unix"). 80 Desc("issue.id") 81 case "farduedate": 82 sess.Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). 83 OrderBy("CASE " + 84 "WHEN milestone.deadline_unix IS NULL THEN issue.deadline_unix " + 85 "WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " + 86 "ELSE issue.deadline_unix END DESC"). 87 Desc("issue.created_unix"). 88 Desc("issue.id") 89 case "priorityrepo": 90 sess.OrderBy("CASE "+ 91 "WHEN issue.repo_id = ? THEN 1 "+ 92 "ELSE 2 END ASC", priorityRepoID). 93 Desc("issue.created_unix"). 94 Desc("issue.id") 95 case "project-column-sorting": 96 sess.Asc("project_issue.sorting").Desc("issue.created_unix").Desc("issue.id") 97 default: 98 sess.Desc("issue.created_unix").Desc("issue.id") 99 } 100 } 101 102 func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { 103 if opts.Paginator == nil || opts.Paginator.IsListAll() { 104 return sess 105 } 106 107 start := 0 108 if opts.Paginator.Page > 1 { 109 start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize 110 } 111 sess.Limit(opts.Paginator.PageSize, start) 112 113 return sess 114 } 115 116 func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { 117 if len(opts.LabelIDs) > 0 { 118 if opts.LabelIDs[0] == 0 { 119 sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)") 120 } else { 121 for i, labelID := range opts.LabelIDs { 122 if labelID > 0 { 123 sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), 124 fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) 125 } else if labelID < 0 { // 0 is not supported here, so just ignore it 126 sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID) 127 } 128 } 129 } 130 } 131 132 if len(opts.IncludedLabelNames) > 0 { 133 sess.In("issue.id", BuildLabelNamesIssueIDsCondition(opts.IncludedLabelNames)) 134 } 135 136 if len(opts.ExcludedLabelNames) > 0 { 137 sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames))) 138 } 139 140 return sess 141 } 142 143 func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { 144 if len(opts.MilestoneIDs) == 1 && opts.MilestoneIDs[0] == db.NoConditionID { 145 sess.And("issue.milestone_id = 0") 146 } else if len(opts.MilestoneIDs) > 0 { 147 sess.In("issue.milestone_id", opts.MilestoneIDs) 148 } 149 150 if len(opts.IncludeMilestones) > 0 { 151 sess.In("issue.milestone_id", 152 builder.Select("id"). 153 From("milestone"). 154 Where(builder.In("name", opts.IncludeMilestones))) 155 } 156 157 return sess 158 } 159 160 func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { 161 if opts.ProjectID > 0 { // specific project 162 sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). 163 And("project_issue.project_id=?", opts.ProjectID) 164 } else if opts.ProjectID == db.NoConditionID { // show those that are in no project 165 sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue").And(builder.Neq{"project_id": 0}))) 166 } 167 // opts.ProjectID == 0 means all projects, 168 // do not need to apply any condition 169 return sess 170 } 171 172 func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { 173 // opts.ProjectBoardID == 0 means all project boards, 174 // do not need to apply any condition 175 if opts.ProjectBoardID > 0 { 176 sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) 177 } else if opts.ProjectBoardID == db.NoConditionID { 178 sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) 179 } 180 return sess 181 } 182 183 func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { 184 if len(opts.RepoIDs) == 1 { 185 opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]} 186 } else if len(opts.RepoIDs) > 1 { 187 opts.RepoCond = builder.In("issue.repo_id", opts.RepoIDs) 188 } 189 if opts.AllPublic { 190 if opts.RepoCond == nil { 191 opts.RepoCond = builder.NewCond() 192 } 193 opts.RepoCond = opts.RepoCond.Or(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"is_private": false}))) 194 } 195 if opts.RepoCond != nil { 196 sess.And(opts.RepoCond) 197 } 198 return sess 199 } 200 201 func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { 202 if len(opts.IssueIDs) > 0 { 203 sess.In("issue.id", opts.IssueIDs) 204 } 205 206 applyRepoConditions(sess, opts) 207 208 if opts.IsClosed.Has() { 209 sess.And("issue.is_closed=?", opts.IsClosed.Value()) 210 } 211 212 if opts.AssigneeID > 0 { 213 applyAssigneeCondition(sess, opts.AssigneeID) 214 } else if opts.AssigneeID == db.NoConditionID { 215 sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") 216 } 217 218 if opts.PosterID > 0 { 219 applyPosterCondition(sess, opts.PosterID) 220 } 221 222 if opts.MentionedID > 0 { 223 applyMentionedCondition(sess, opts.MentionedID) 224 } 225 226 if opts.ReviewRequestedID > 0 { 227 applyReviewRequestedCondition(sess, opts.ReviewRequestedID) 228 } 229 230 if opts.ReviewedID > 0 { 231 applyReviewedCondition(sess, opts.ReviewedID) 232 } 233 234 if opts.SubscriberID > 0 { 235 applySubscribedCondition(sess, opts.SubscriberID) 236 } 237 238 applyMilestoneCondition(sess, opts) 239 240 if opts.UpdatedAfterUnix != 0 { 241 sess.And(builder.Gte{"issue.updated_unix": opts.UpdatedAfterUnix}) 242 } 243 if opts.UpdatedBeforeUnix != 0 { 244 sess.And(builder.Lte{"issue.updated_unix": opts.UpdatedBeforeUnix}) 245 } 246 247 applyProjectCondition(sess, opts) 248 249 applyProjectBoardCondition(sess, opts) 250 251 if opts.IsPull.Has() { 252 sess.And("issue.is_pull=?", opts.IsPull.Value()) 253 } 254 255 if opts.IsArchived.Has() { 256 sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.Value()}) 257 } 258 259 applyLabelsCondition(sess, opts) 260 261 if opts.User != nil { 262 sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value())) 263 } 264 265 return sess 266 } 267 268 // teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access 269 func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond { 270 return builder.In(id, 271 builder.Select("repo_id").From("team_repo").Where( 272 builder.Eq{ 273 "team_id": teamID, 274 }.And( 275 builder.Or( 276 // Check if the user is member of the team. 277 builder.In( 278 "team_id", builder.Select("team_id").From("team_user").Where( 279 builder.Eq{ 280 "uid": userID, 281 }, 282 ), 283 ), 284 // Check if the user is in the owner team of the organisation. 285 builder.Exists(builder.Select("team_id").From("team_user"). 286 Where(builder.Eq{ 287 "org_id": orgID, 288 "team_id": builder.Select("id").From("team").Where( 289 builder.Eq{ 290 "org_id": orgID, 291 "lower_name": strings.ToLower(organization.OwnerTeamName), 292 }), 293 "uid": userID, 294 }), 295 ), 296 )).And( 297 builder.In( 298 "team_id", builder.Select("team_id").From("team_unit").Where( 299 builder.Eq{ 300 "`team_unit`.org_id": orgID, 301 }.And( 302 builder.In("`team_unit`.type", units), 303 ), 304 ), 305 ), 306 ), 307 )) 308 } 309 310 // issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table 311 func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond { 312 cond := builder.NewCond() 313 unitType := unit.TypeIssues 314 if isPull { 315 unitType = unit.TypePullRequests 316 } 317 if org != nil { 318 if team != nil { 319 cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos 320 } else { 321 cond = cond.And( 322 builder.Or( 323 repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos 324 repo_model.UserOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues 325 ), 326 ) 327 } 328 } else { 329 cond = cond.And( 330 builder.Or( 331 repo_model.UserOwnedRepoCond(userID), // owned repos 332 repo_model.UserAccessRepoCond(repoIDstr, userID), // user can access repo in a unit independent way 333 repo_model.UserAssignedRepoCond(repoIDstr, userID), // user has been assigned accessible public repos 334 repo_model.UserMentionedRepoCond(repoIDstr, userID), // user has been mentioned accessible public repos 335 repo_model.UserCreateIssueRepoCond(repoIDstr, userID, isPull), // user has created issue/pr accessible public repos 336 ), 337 ) 338 } 339 return cond 340 } 341 342 func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) *xorm.Session { 343 return sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). 344 And("issue_assignees.assignee_id = ?", assigneeID) 345 } 346 347 func applyPosterCondition(sess *xorm.Session, posterID int64) *xorm.Session { 348 return sess.And("issue.poster_id=?", posterID) 349 } 350 351 func applyMentionedCondition(sess *xorm.Session, mentionedID int64) *xorm.Session { 352 return sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id"). 353 And("issue_user.is_mentioned = ?", true). 354 And("issue_user.uid = ?", mentionedID) 355 } 356 357 func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) *xorm.Session { 358 existInTeamQuery := builder.Select("team_user.team_id"). 359 From("team_user"). 360 Where(builder.Eq{"team_user.uid": reviewRequestedID}) 361 362 // if the review is approved or rejected, it should not be shown in the review requested list 363 maxReview := builder.Select("MAX(r.id)"). 364 From("review as r"). 365 Where(builder.In("r.type", []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest})). 366 GroupBy("r.issue_id, r.reviewer_id, r.reviewer_team_id") 367 368 subQuery := builder.Select("review.issue_id"). 369 From("review"). 370 Where(builder.And( 371 builder.Eq{"review.type": ReviewTypeRequest}, 372 builder.Or( 373 builder.Eq{"review.reviewer_id": reviewRequestedID}, 374 builder.In("review.reviewer_team_id", existInTeamQuery), 375 ), 376 builder.In("review.id", maxReview), 377 )) 378 return sess.Where("issue.poster_id <> ?", reviewRequestedID). 379 And(builder.In("issue.id", subQuery)) 380 } 381 382 func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session { 383 // Query for pull requests where you are a reviewer or commenter, excluding 384 // any pull requests already returned by the review requested filter. 385 notPoster := builder.Neq{"issue.poster_id": reviewedID} 386 reviewed := builder.In("issue.id", builder. 387 Select("issue_id"). 388 From("review"). 389 Where(builder.And( 390 builder.Neq{"type": ReviewTypeRequest}, 391 builder.Or( 392 builder.Eq{"reviewer_id": reviewedID}, 393 builder.In("reviewer_team_id", builder. 394 Select("team_id"). 395 From("team_user"). 396 Where(builder.Eq{"uid": reviewedID}), 397 ), 398 ), 399 )), 400 ) 401 commented := builder.In("issue.id", builder. 402 Select("issue_id"). 403 From("comment"). 404 Where(builder.And( 405 builder.Eq{"poster_id": reviewedID}, 406 builder.In("type", CommentTypeComment, CommentTypeCode, CommentTypeReview), 407 )), 408 ) 409 return sess.And(notPoster, builder.Or(reviewed, commented)) 410 } 411 412 func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Session { 413 return sess.And( 414 builder. 415 NotIn("issue.id", 416 builder.Select("issue_id"). 417 From("issue_watch"). 418 Where(builder.Eq{"is_watching": false, "user_id": subscriberID}), 419 ), 420 ).And( 421 builder.Or( 422 builder.In("issue.id", builder. 423 Select("issue_id"). 424 From("issue_watch"). 425 Where(builder.Eq{"is_watching": true, "user_id": subscriberID}), 426 ), 427 builder.In("issue.id", builder. 428 Select("issue_id"). 429 From("comment"). 430 Where(builder.Eq{"poster_id": subscriberID}), 431 ), 432 builder.Eq{"issue.poster_id": subscriberID}, 433 builder.In("issue.repo_id", builder. 434 Select("id"). 435 From("watch"). 436 Where(builder.And(builder.Eq{"user_id": subscriberID}, 437 builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))), 438 ), 439 ), 440 ) 441 } 442 443 // Issues returns a list of issues by given conditions. 444 func Issues(ctx context.Context, opts *IssuesOptions) (IssueList, error) { 445 sess := db.GetEngine(ctx). 446 Join("INNER", "repository", "`issue`.repo_id = `repository`.id") 447 applyLimit(sess, opts) 448 applyConditions(sess, opts) 449 applySorts(sess, opts.SortType, opts.PriorityRepoID) 450 451 issues := IssueList{} 452 if err := sess.Find(&issues); err != nil { 453 return nil, fmt.Errorf("unable to query Issues: %w", err) 454 } 455 456 if err := issues.LoadAttributes(ctx); err != nil { 457 return nil, fmt.Errorf("unable to LoadAttributes for Issues: %w", err) 458 } 459 460 return issues, nil 461 } 462 463 // IssueIDs returns a list of issue ids by given conditions. 464 func IssueIDs(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Cond) ([]int64, int64, error) { 465 sess := db.GetEngine(ctx). 466 Join("INNER", "repository", "`issue`.repo_id = `repository`.id") 467 applyConditions(sess, opts) 468 for _, cond := range otherConds { 469 sess.And(cond) 470 } 471 472 applyLimit(sess, opts) 473 applySorts(sess, opts.SortType, opts.PriorityRepoID) 474 475 var res []int64 476 total, err := sess.Select("`issue`.id").Table(&Issue{}).FindAndCount(&res) 477 if err != nil { 478 return nil, 0, err 479 } 480 481 return res, total, nil 482 }