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