code.gitea.io/gitea@v1.21.7/models/issues/issue_list.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package issues
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	"code.gitea.io/gitea/models/db"
    11  	project_model "code.gitea.io/gitea/models/project"
    12  	repo_model "code.gitea.io/gitea/models/repo"
    13  	user_model "code.gitea.io/gitea/models/user"
    14  	"code.gitea.io/gitea/modules/container"
    15  
    16  	"xorm.io/builder"
    17  )
    18  
    19  // IssueList defines a list of issues
    20  type IssueList []*Issue
    21  
    22  // get the repo IDs to be loaded later, these IDs are for issue.Repo and issue.PullRequest.HeadRepo
    23  func (issues IssueList) getRepoIDs() []int64 {
    24  	repoIDs := make(container.Set[int64], len(issues))
    25  	for _, issue := range issues {
    26  		if issue.Repo == nil {
    27  			repoIDs.Add(issue.RepoID)
    28  		}
    29  		if issue.PullRequest != nil && issue.PullRequest.HeadRepo == nil {
    30  			repoIDs.Add(issue.PullRequest.HeadRepoID)
    31  		}
    32  	}
    33  	return repoIDs.Values()
    34  }
    35  
    36  // LoadRepositories loads issues' all repositories
    37  func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.RepositoryList, error) {
    38  	if len(issues) == 0 {
    39  		return nil, nil
    40  	}
    41  
    42  	repoIDs := issues.getRepoIDs()
    43  	repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs))
    44  	left := len(repoIDs)
    45  	for left > 0 {
    46  		limit := db.DefaultMaxInSize
    47  		if left < limit {
    48  			limit = left
    49  		}
    50  		err := db.GetEngine(ctx).
    51  			In("id", repoIDs[:limit]).
    52  			Find(&repoMaps)
    53  		if err != nil {
    54  			return nil, fmt.Errorf("find repository: %w", err)
    55  		}
    56  		left -= limit
    57  		repoIDs = repoIDs[limit:]
    58  	}
    59  
    60  	for _, issue := range issues {
    61  		if issue.Repo == nil {
    62  			issue.Repo = repoMaps[issue.RepoID]
    63  		} else {
    64  			repoMaps[issue.RepoID] = issue.Repo
    65  		}
    66  		if issue.PullRequest != nil {
    67  			issue.PullRequest.BaseRepo = issue.Repo
    68  			if issue.PullRequest.HeadRepo == nil {
    69  				issue.PullRequest.HeadRepo = repoMaps[issue.PullRequest.HeadRepoID]
    70  			}
    71  		}
    72  	}
    73  	return repo_model.ValuesRepository(repoMaps), nil
    74  }
    75  
    76  func (issues IssueList) getPosterIDs() []int64 {
    77  	posterIDs := make(container.Set[int64], len(issues))
    78  	for _, issue := range issues {
    79  		posterIDs.Add(issue.PosterID)
    80  	}
    81  	return posterIDs.Values()
    82  }
    83  
    84  func (issues IssueList) loadPosters(ctx context.Context) error {
    85  	if len(issues) == 0 {
    86  		return nil
    87  	}
    88  
    89  	posterMaps, err := getPosters(ctx, issues.getPosterIDs())
    90  	if err != nil {
    91  		return err
    92  	}
    93  
    94  	for _, issue := range issues {
    95  		issue.Poster = getPoster(issue.PosterID, posterMaps)
    96  	}
    97  	return nil
    98  }
    99  
   100  func getPosters(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
   101  	posterMaps := make(map[int64]*user_model.User, len(posterIDs))
   102  	left := len(posterIDs)
   103  	for left > 0 {
   104  		limit := db.DefaultMaxInSize
   105  		if left < limit {
   106  			limit = left
   107  		}
   108  		err := db.GetEngine(ctx).
   109  			In("id", posterIDs[:limit]).
   110  			Find(&posterMaps)
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  		left -= limit
   115  		posterIDs = posterIDs[limit:]
   116  	}
   117  	return posterMaps, nil
   118  }
   119  
   120  func getPoster(posterID int64, posterMaps map[int64]*user_model.User) *user_model.User {
   121  	if posterID == user_model.ActionsUserID {
   122  		return user_model.NewActionsUser()
   123  	}
   124  	if posterID <= 0 {
   125  		return nil
   126  	}
   127  	poster, ok := posterMaps[posterID]
   128  	if !ok {
   129  		return user_model.NewGhostUser()
   130  	}
   131  	return poster
   132  }
   133  
   134  func (issues IssueList) getIssueIDs() []int64 {
   135  	ids := make([]int64, 0, len(issues))
   136  	for _, issue := range issues {
   137  		ids = append(ids, issue.ID)
   138  	}
   139  	return ids
   140  }
   141  
   142  func (issues IssueList) loadLabels(ctx context.Context) error {
   143  	if len(issues) == 0 {
   144  		return nil
   145  	}
   146  
   147  	type LabelIssue struct {
   148  		Label      *Label      `xorm:"extends"`
   149  		IssueLabel *IssueLabel `xorm:"extends"`
   150  	}
   151  
   152  	issueLabels := make(map[int64][]*Label, len(issues)*3)
   153  	issueIDs := issues.getIssueIDs()
   154  	left := len(issueIDs)
   155  	for left > 0 {
   156  		limit := db.DefaultMaxInSize
   157  		if left < limit {
   158  			limit = left
   159  		}
   160  		rows, err := db.GetEngine(ctx).Table("label").
   161  			Join("LEFT", "issue_label", "issue_label.label_id = label.id").
   162  			In("issue_label.issue_id", issueIDs[:limit]).
   163  			Asc("label.name").
   164  			Rows(new(LabelIssue))
   165  		if err != nil {
   166  			return err
   167  		}
   168  
   169  		for rows.Next() {
   170  			var labelIssue LabelIssue
   171  			err = rows.Scan(&labelIssue)
   172  			if err != nil {
   173  				if err1 := rows.Close(); err1 != nil {
   174  					return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
   175  				}
   176  				return err
   177  			}
   178  			issueLabels[labelIssue.IssueLabel.IssueID] = append(issueLabels[labelIssue.IssueLabel.IssueID], labelIssue.Label)
   179  		}
   180  		// When there are no rows left and we try to close it.
   181  		// Since that is not relevant for us, we can safely ignore it.
   182  		if err1 := rows.Close(); err1 != nil {
   183  			return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
   184  		}
   185  		left -= limit
   186  		issueIDs = issueIDs[limit:]
   187  	}
   188  
   189  	for _, issue := range issues {
   190  		issue.Labels = issueLabels[issue.ID]
   191  	}
   192  	return nil
   193  }
   194  
   195  func (issues IssueList) getMilestoneIDs() []int64 {
   196  	ids := make(container.Set[int64], len(issues))
   197  	for _, issue := range issues {
   198  		ids.Add(issue.MilestoneID)
   199  	}
   200  	return ids.Values()
   201  }
   202  
   203  func (issues IssueList) loadMilestones(ctx context.Context) error {
   204  	milestoneIDs := issues.getMilestoneIDs()
   205  	if len(milestoneIDs) == 0 {
   206  		return nil
   207  	}
   208  
   209  	milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
   210  	left := len(milestoneIDs)
   211  	for left > 0 {
   212  		limit := db.DefaultMaxInSize
   213  		if left < limit {
   214  			limit = left
   215  		}
   216  		err := db.GetEngine(ctx).
   217  			In("id", milestoneIDs[:limit]).
   218  			Find(&milestoneMaps)
   219  		if err != nil {
   220  			return err
   221  		}
   222  		left -= limit
   223  		milestoneIDs = milestoneIDs[limit:]
   224  	}
   225  
   226  	for _, issue := range issues {
   227  		issue.Milestone = milestoneMaps[issue.MilestoneID]
   228  	}
   229  	return nil
   230  }
   231  
   232  func (issues IssueList) LoadProjects(ctx context.Context) error {
   233  	issueIDs := issues.getIssueIDs()
   234  	projectMaps := make(map[int64]*project_model.Project, len(issues))
   235  	left := len(issueIDs)
   236  
   237  	type projectWithIssueID struct {
   238  		*project_model.Project `xorm:"extends"`
   239  		IssueID                int64
   240  	}
   241  
   242  	for left > 0 {
   243  		limit := db.DefaultMaxInSize
   244  		if left < limit {
   245  			limit = left
   246  		}
   247  
   248  		projects := make([]*projectWithIssueID, 0, limit)
   249  		err := db.GetEngine(ctx).
   250  			Table("project").
   251  			Select("project.*, project_issue.issue_id").
   252  			Join("INNER", "project_issue", "project.id = project_issue.project_id").
   253  			In("project_issue.issue_id", issueIDs[:limit]).
   254  			Find(&projects)
   255  		if err != nil {
   256  			return err
   257  		}
   258  		for _, project := range projects {
   259  			projectMaps[project.IssueID] = project.Project
   260  		}
   261  		left -= limit
   262  		issueIDs = issueIDs[limit:]
   263  	}
   264  
   265  	for _, issue := range issues {
   266  		issue.Project = projectMaps[issue.ID]
   267  	}
   268  	return nil
   269  }
   270  
   271  func (issues IssueList) loadAssignees(ctx context.Context) error {
   272  	if len(issues) == 0 {
   273  		return nil
   274  	}
   275  
   276  	type AssigneeIssue struct {
   277  		IssueAssignee *IssueAssignees  `xorm:"extends"`
   278  		Assignee      *user_model.User `xorm:"extends"`
   279  	}
   280  
   281  	assignees := make(map[int64][]*user_model.User, len(issues))
   282  	issueIDs := issues.getIssueIDs()
   283  	left := len(issueIDs)
   284  	for left > 0 {
   285  		limit := db.DefaultMaxInSize
   286  		if left < limit {
   287  			limit = left
   288  		}
   289  		rows, err := db.GetEngine(ctx).Table("issue_assignees").
   290  			Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id").
   291  			In("`issue_assignees`.issue_id", issueIDs[:limit]).OrderBy(user_model.GetOrderByName()).
   292  			Rows(new(AssigneeIssue))
   293  		if err != nil {
   294  			return err
   295  		}
   296  
   297  		for rows.Next() {
   298  			var assigneeIssue AssigneeIssue
   299  			err = rows.Scan(&assigneeIssue)
   300  			if err != nil {
   301  				if err1 := rows.Close(); err1 != nil {
   302  					return fmt.Errorf("IssueList.loadAssignees: Close: %w", err1)
   303  				}
   304  				return err
   305  			}
   306  
   307  			assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
   308  		}
   309  		if err1 := rows.Close(); err1 != nil {
   310  			return fmt.Errorf("IssueList.loadAssignees: Close: %w", err1)
   311  		}
   312  		left -= limit
   313  		issueIDs = issueIDs[limit:]
   314  	}
   315  
   316  	for _, issue := range issues {
   317  		issue.Assignees = assignees[issue.ID]
   318  	}
   319  	return nil
   320  }
   321  
   322  func (issues IssueList) getPullIssueIDs() []int64 {
   323  	ids := make([]int64, 0, len(issues))
   324  	for _, issue := range issues {
   325  		if issue.IsPull && issue.PullRequest == nil {
   326  			ids = append(ids, issue.ID)
   327  		}
   328  	}
   329  	return ids
   330  }
   331  
   332  // LoadPullRequests loads pull requests
   333  func (issues IssueList) LoadPullRequests(ctx context.Context) error {
   334  	issuesIDs := issues.getPullIssueIDs()
   335  	if len(issuesIDs) == 0 {
   336  		return nil
   337  	}
   338  
   339  	pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs))
   340  	left := len(issuesIDs)
   341  	for left > 0 {
   342  		limit := db.DefaultMaxInSize
   343  		if left < limit {
   344  			limit = left
   345  		}
   346  		rows, err := db.GetEngine(ctx).
   347  			In("issue_id", issuesIDs[:limit]).
   348  			Rows(new(PullRequest))
   349  		if err != nil {
   350  			return err
   351  		}
   352  
   353  		for rows.Next() {
   354  			var pr PullRequest
   355  			err = rows.Scan(&pr)
   356  			if err != nil {
   357  				if err1 := rows.Close(); err1 != nil {
   358  					return fmt.Errorf("IssueList.loadPullRequests: Close: %w", err1)
   359  				}
   360  				return err
   361  			}
   362  			pullRequestMaps[pr.IssueID] = &pr
   363  		}
   364  		if err1 := rows.Close(); err1 != nil {
   365  			return fmt.Errorf("IssueList.loadPullRequests: Close: %w", err1)
   366  		}
   367  		left -= limit
   368  		issuesIDs = issuesIDs[limit:]
   369  	}
   370  
   371  	for _, issue := range issues {
   372  		issue.PullRequest = pullRequestMaps[issue.ID]
   373  	}
   374  	return nil
   375  }
   376  
   377  // LoadAttachments loads attachments
   378  func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
   379  	if len(issues) == 0 {
   380  		return nil
   381  	}
   382  
   383  	attachments := make(map[int64][]*repo_model.Attachment, len(issues))
   384  	issuesIDs := issues.getIssueIDs()
   385  	left := len(issuesIDs)
   386  	for left > 0 {
   387  		limit := db.DefaultMaxInSize
   388  		if left < limit {
   389  			limit = left
   390  		}
   391  		rows, err := db.GetEngine(ctx).Table("attachment").
   392  			Join("INNER", "issue", "issue.id = attachment.issue_id").
   393  			In("issue.id", issuesIDs[:limit]).
   394  			Rows(new(repo_model.Attachment))
   395  		if err != nil {
   396  			return err
   397  		}
   398  
   399  		for rows.Next() {
   400  			var attachment repo_model.Attachment
   401  			err = rows.Scan(&attachment)
   402  			if err != nil {
   403  				if err1 := rows.Close(); err1 != nil {
   404  					return fmt.Errorf("IssueList.loadAttachments: Close: %w", err1)
   405  				}
   406  				return err
   407  			}
   408  			attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment)
   409  		}
   410  		if err1 := rows.Close(); err1 != nil {
   411  			return fmt.Errorf("IssueList.loadAttachments: Close: %w", err1)
   412  		}
   413  		left -= limit
   414  		issuesIDs = issuesIDs[limit:]
   415  	}
   416  
   417  	for _, issue := range issues {
   418  		issue.Attachments = attachments[issue.ID]
   419  	}
   420  	return nil
   421  }
   422  
   423  func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (err error) {
   424  	if len(issues) == 0 {
   425  		return nil
   426  	}
   427  
   428  	comments := make(map[int64][]*Comment, len(issues))
   429  	issuesIDs := issues.getIssueIDs()
   430  	left := len(issuesIDs)
   431  	for left > 0 {
   432  		limit := db.DefaultMaxInSize
   433  		if left < limit {
   434  			limit = left
   435  		}
   436  		rows, err := db.GetEngine(ctx).Table("comment").
   437  			Join("INNER", "issue", "issue.id = comment.issue_id").
   438  			In("issue.id", issuesIDs[:limit]).
   439  			Where(cond).
   440  			Rows(new(Comment))
   441  		if err != nil {
   442  			return err
   443  		}
   444  
   445  		for rows.Next() {
   446  			var comment Comment
   447  			err = rows.Scan(&comment)
   448  			if err != nil {
   449  				if err1 := rows.Close(); err1 != nil {
   450  					return fmt.Errorf("IssueList.loadComments: Close: %w", err1)
   451  				}
   452  				return err
   453  			}
   454  			comments[comment.IssueID] = append(comments[comment.IssueID], &comment)
   455  		}
   456  		if err1 := rows.Close(); err1 != nil {
   457  			return fmt.Errorf("IssueList.loadComments: Close: %w", err1)
   458  		}
   459  		left -= limit
   460  		issuesIDs = issuesIDs[limit:]
   461  	}
   462  
   463  	for _, issue := range issues {
   464  		issue.Comments = comments[issue.ID]
   465  	}
   466  	return nil
   467  }
   468  
   469  func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
   470  	type totalTimesByIssue struct {
   471  		IssueID int64
   472  		Time    int64
   473  	}
   474  	if len(issues) == 0 {
   475  		return nil
   476  	}
   477  	trackedTimes := make(map[int64]int64, len(issues))
   478  
   479  	ids := make([]int64, 0, len(issues))
   480  	for _, issue := range issues {
   481  		if issue.Repo.IsTimetrackerEnabled(ctx) {
   482  			ids = append(ids, issue.ID)
   483  		}
   484  	}
   485  
   486  	left := len(ids)
   487  	for left > 0 {
   488  		limit := db.DefaultMaxInSize
   489  		if left < limit {
   490  			limit = left
   491  		}
   492  
   493  		// select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
   494  		rows, err := db.GetEngine(ctx).Table("tracked_time").
   495  			Where("deleted = ?", false).
   496  			Select("issue_id, sum(time) as time").
   497  			In("issue_id", ids[:limit]).
   498  			GroupBy("issue_id").
   499  			Rows(new(totalTimesByIssue))
   500  		if err != nil {
   501  			return err
   502  		}
   503  
   504  		for rows.Next() {
   505  			var totalTime totalTimesByIssue
   506  			err = rows.Scan(&totalTime)
   507  			if err != nil {
   508  				if err1 := rows.Close(); err1 != nil {
   509  					return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %w", err1)
   510  				}
   511  				return err
   512  			}
   513  			trackedTimes[totalTime.IssueID] = totalTime.Time
   514  		}
   515  		if err1 := rows.Close(); err1 != nil {
   516  			return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %w", err1)
   517  		}
   518  		left -= limit
   519  		ids = ids[limit:]
   520  	}
   521  
   522  	for _, issue := range issues {
   523  		issue.TotalTrackedTime = trackedTimes[issue.ID]
   524  	}
   525  	return nil
   526  }
   527  
   528  // loadAttributes loads all attributes, expect for attachments and comments
   529  func (issues IssueList) LoadAttributes(ctx context.Context) error {
   530  	if _, err := issues.LoadRepositories(ctx); err != nil {
   531  		return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
   532  	}
   533  
   534  	if err := issues.loadPosters(ctx); err != nil {
   535  		return fmt.Errorf("issue.loadAttributes: loadPosters: %w", err)
   536  	}
   537  
   538  	if err := issues.loadLabels(ctx); err != nil {
   539  		return fmt.Errorf("issue.loadAttributes: loadLabels: %w", err)
   540  	}
   541  
   542  	if err := issues.loadMilestones(ctx); err != nil {
   543  		return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
   544  	}
   545  
   546  	if err := issues.LoadProjects(ctx); err != nil {
   547  		return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
   548  	}
   549  
   550  	if err := issues.loadAssignees(ctx); err != nil {
   551  		return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err)
   552  	}
   553  
   554  	if err := issues.LoadPullRequests(ctx); err != nil {
   555  		return fmt.Errorf("issue.loadAttributes: loadPullRequests: %w", err)
   556  	}
   557  
   558  	if err := issues.loadTotalTrackedTimes(ctx); err != nil {
   559  		return fmt.Errorf("issue.loadAttributes: loadTotalTrackedTimes: %w", err)
   560  	}
   561  
   562  	return nil
   563  }
   564  
   565  // LoadComments loads comments
   566  func (issues IssueList) LoadComments(ctx context.Context) error {
   567  	return issues.loadComments(ctx, builder.NewCond())
   568  }
   569  
   570  // LoadDiscussComments loads discuss comments
   571  func (issues IssueList) LoadDiscussComments(ctx context.Context) error {
   572  	return issues.loadComments(ctx, builder.Eq{"comment.type": CommentTypeComment})
   573  }
   574  
   575  // GetApprovalCounts returns a map of issue ID to slice of approval counts
   576  // FIXME: only returns official counts due to double counting of non-official approvals
   577  func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*ReviewCount, error) {
   578  	rCounts := make([]*ReviewCount, 0, 2*len(issues))
   579  	ids := make([]int64, len(issues))
   580  	for i, issue := range issues {
   581  		ids[i] = issue.ID
   582  	}
   583  	sess := db.GetEngine(ctx).In("issue_id", ids)
   584  	err := sess.Select("issue_id, type, count(id) as `count`").
   585  		Where("official = ? AND dismissed = ?", true, false).
   586  		GroupBy("issue_id, type").
   587  		OrderBy("issue_id").
   588  		Table("review").
   589  		Find(&rCounts)
   590  	if err != nil {
   591  		return nil, err
   592  	}
   593  
   594  	approvalCountMap := make(map[int64][]*ReviewCount, len(issues))
   595  
   596  	for _, c := range rCounts {
   597  		approvalCountMap[c.IssueID] = append(approvalCountMap[c.IssueID], c)
   598  	}
   599  
   600  	return approvalCountMap, nil
   601  }