code.gitea.io/gitea@v1.22.3/models/issues/issue_update.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  	"code.gitea.io/gitea/models/perm"
    14  	access_model "code.gitea.io/gitea/models/perm/access"
    15  	project_model "code.gitea.io/gitea/models/project"
    16  	repo_model "code.gitea.io/gitea/models/repo"
    17  	system_model "code.gitea.io/gitea/models/system"
    18  	"code.gitea.io/gitea/models/unit"
    19  	user_model "code.gitea.io/gitea/models/user"
    20  	"code.gitea.io/gitea/modules/git"
    21  	"code.gitea.io/gitea/modules/references"
    22  	api "code.gitea.io/gitea/modules/structs"
    23  	"code.gitea.io/gitea/modules/timeutil"
    24  
    25  	"xorm.io/builder"
    26  )
    27  
    28  // UpdateIssueCols updates cols of issue
    29  func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
    30  	if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil {
    31  		return err
    32  	}
    33  	return nil
    34  }
    35  
    36  func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) {
    37  	// Reload the issue
    38  	currentIssue, err := GetIssueByID(ctx, issue.ID)
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  
    43  	// Nothing should be performed if current status is same as target status
    44  	if currentIssue.IsClosed == isClosed {
    45  		if !issue.IsPull {
    46  			return nil, ErrIssueWasClosed{
    47  				ID: issue.ID,
    48  			}
    49  		}
    50  		return nil, ErrPullWasClosed{
    51  			ID: issue.ID,
    52  		}
    53  	}
    54  
    55  	issue.IsClosed = isClosed
    56  	return doChangeIssueStatus(ctx, issue, doer, isMergePull)
    57  }
    58  
    59  func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) {
    60  	// Check for open dependencies
    61  	if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) {
    62  		// only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies
    63  		noDeps, err := IssueNoDependenciesLeft(ctx, issue)
    64  		if err != nil {
    65  			return nil, err
    66  		}
    67  
    68  		if !noDeps {
    69  			return nil, ErrDependenciesLeft{issue.ID}
    70  		}
    71  	}
    72  
    73  	if issue.IsClosed {
    74  		issue.ClosedUnix = timeutil.TimeStampNow()
    75  	} else {
    76  		issue.ClosedUnix = 0
    77  	}
    78  
    79  	if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	// Update issue count of labels
    84  	if err := issue.LoadLabels(ctx); err != nil {
    85  		return nil, err
    86  	}
    87  	for idx := range issue.Labels {
    88  		if err := updateLabelCols(ctx, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil {
    89  			return nil, err
    90  		}
    91  	}
    92  
    93  	// Update issue count of milestone
    94  	if issue.MilestoneID > 0 {
    95  		if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
    96  			return nil, err
    97  		}
    98  	}
    99  
   100  	// update repository's issue closed number
   101  	if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	// New action comment
   106  	cmtType := CommentTypeClose
   107  	if !issue.IsClosed {
   108  		cmtType = CommentTypeReopen
   109  	} else if isMergePull {
   110  		cmtType = CommentTypeMergePull
   111  	}
   112  
   113  	return CreateComment(ctx, &CreateCommentOptions{
   114  		Type:  cmtType,
   115  		Doer:  doer,
   116  		Repo:  issue.Repo,
   117  		Issue: issue,
   118  	})
   119  }
   120  
   121  // ChangeIssueStatus changes issue status to open or closed.
   122  func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) {
   123  	if err := issue.LoadRepo(ctx); err != nil {
   124  		return nil, err
   125  	}
   126  	if err := issue.LoadPoster(ctx); err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	return changeIssueStatus(ctx, issue, doer, isClosed, false)
   131  }
   132  
   133  // ChangeIssueTitle changes the title of this issue, as the given user.
   134  func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, oldTitle string) (err error) {
   135  	ctx, committer, err := db.TxContext(ctx)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	defer committer.Close()
   140  
   141  	if err = UpdateIssueCols(ctx, issue, "name"); err != nil {
   142  		return fmt.Errorf("updateIssueCols: %w", err)
   143  	}
   144  
   145  	if err = issue.LoadRepo(ctx); err != nil {
   146  		return fmt.Errorf("loadRepo: %w", err)
   147  	}
   148  
   149  	opts := &CreateCommentOptions{
   150  		Type:     CommentTypeChangeTitle,
   151  		Doer:     doer,
   152  		Repo:     issue.Repo,
   153  		Issue:    issue,
   154  		OldTitle: oldTitle,
   155  		NewTitle: issue.Title,
   156  	}
   157  	if _, err = CreateComment(ctx, opts); err != nil {
   158  		return fmt.Errorf("createComment: %w", err)
   159  	}
   160  	if err = issue.AddCrossReferences(ctx, doer, true); err != nil {
   161  		return err
   162  	}
   163  
   164  	return committer.Commit()
   165  }
   166  
   167  // ChangeIssueRef changes the branch of this issue, as the given user.
   168  func ChangeIssueRef(ctx context.Context, issue *Issue, doer *user_model.User, oldRef string) (err error) {
   169  	ctx, committer, err := db.TxContext(ctx)
   170  	if err != nil {
   171  		return err
   172  	}
   173  	defer committer.Close()
   174  
   175  	if err = UpdateIssueCols(ctx, issue, "ref"); err != nil {
   176  		return fmt.Errorf("updateIssueCols: %w", err)
   177  	}
   178  
   179  	if err = issue.LoadRepo(ctx); err != nil {
   180  		return fmt.Errorf("loadRepo: %w", err)
   181  	}
   182  	oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix)
   183  	newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix)
   184  
   185  	opts := &CreateCommentOptions{
   186  		Type:   CommentTypeChangeIssueRef,
   187  		Doer:   doer,
   188  		Repo:   issue.Repo,
   189  		Issue:  issue,
   190  		OldRef: oldRefFriendly,
   191  		NewRef: newRefFriendly,
   192  	}
   193  	if _, err = CreateComment(ctx, opts); err != nil {
   194  		return fmt.Errorf("createComment: %w", err)
   195  	}
   196  
   197  	return committer.Commit()
   198  }
   199  
   200  // AddDeletePRBranchComment adds delete branch comment for pull request issue
   201  func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issueID int64, branchName string) error {
   202  	issue, err := GetIssueByID(ctx, issueID)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	opts := &CreateCommentOptions{
   207  		Type:   CommentTypeDeleteBranch,
   208  		Doer:   doer,
   209  		Repo:   repo,
   210  		Issue:  issue,
   211  		OldRef: branchName,
   212  	}
   213  	_, err = CreateComment(ctx, opts)
   214  	return err
   215  }
   216  
   217  // UpdateIssueAttachments update attachments by UUIDs for the issue
   218  func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) {
   219  	ctx, committer, err := db.TxContext(ctx)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	defer committer.Close()
   224  	attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
   225  	if err != nil {
   226  		return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
   227  	}
   228  	for i := 0; i < len(attachments); i++ {
   229  		attachments[i].IssueID = issueID
   230  		if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
   231  			return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
   232  		}
   233  	}
   234  	return committer.Commit()
   235  }
   236  
   237  // ChangeIssueContent changes issue content, as the given user.
   238  func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string) (err error) {
   239  	ctx, committer, err := db.TxContext(ctx)
   240  	if err != nil {
   241  		return err
   242  	}
   243  	defer committer.Close()
   244  
   245  	hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0)
   246  	if err != nil {
   247  		return fmt.Errorf("HasIssueContentHistory: %w", err)
   248  	}
   249  	if !hasContentHistory {
   250  		if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0,
   251  			issue.CreatedUnix, issue.Content, true); err != nil {
   252  			return fmt.Errorf("SaveIssueContentHistory: %w", err)
   253  		}
   254  	}
   255  
   256  	issue.Content = content
   257  
   258  	if err = UpdateIssueCols(ctx, issue, "content"); err != nil {
   259  		return fmt.Errorf("UpdateIssueCols: %w", err)
   260  	}
   261  
   262  	if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0,
   263  		timeutil.TimeStampNow(), issue.Content, false); err != nil {
   264  		return fmt.Errorf("SaveIssueContentHistory: %w", err)
   265  	}
   266  
   267  	if err = issue.AddCrossReferences(ctx, doer, true); err != nil {
   268  		return fmt.Errorf("addCrossReferences: %w", err)
   269  	}
   270  
   271  	return committer.Commit()
   272  }
   273  
   274  // NewIssueOptions represents the options of a new issue.
   275  type NewIssueOptions struct {
   276  	Repo        *repo_model.Repository
   277  	Issue       *Issue
   278  	LabelIDs    []int64
   279  	Attachments []string // In UUID format.
   280  	IsPull      bool
   281  }
   282  
   283  // NewIssueWithIndex creates issue with given index
   284  func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) {
   285  	e := db.GetEngine(ctx)
   286  	opts.Issue.Title = strings.TrimSpace(opts.Issue.Title)
   287  
   288  	if opts.Issue.MilestoneID > 0 {
   289  		milestone, err := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID)
   290  		if err != nil && !IsErrMilestoneNotExist(err) {
   291  			return fmt.Errorf("getMilestoneByID: %w", err)
   292  		}
   293  
   294  		// Assume milestone is invalid and drop silently.
   295  		opts.Issue.MilestoneID = 0
   296  		if milestone != nil {
   297  			opts.Issue.MilestoneID = milestone.ID
   298  			opts.Issue.Milestone = milestone
   299  		}
   300  	}
   301  
   302  	if opts.Issue.Index <= 0 {
   303  		return fmt.Errorf("no issue index provided")
   304  	}
   305  	if opts.Issue.ID > 0 {
   306  		return fmt.Errorf("issue exist")
   307  	}
   308  
   309  	if _, err := e.Insert(opts.Issue); err != nil {
   310  		return err
   311  	}
   312  
   313  	if opts.Issue.MilestoneID > 0 {
   314  		if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil {
   315  			return err
   316  		}
   317  
   318  		opts := &CreateCommentOptions{
   319  			Type:           CommentTypeMilestone,
   320  			Doer:           doer,
   321  			Repo:           opts.Repo,
   322  			Issue:          opts.Issue,
   323  			OldMilestoneID: 0,
   324  			MilestoneID:    opts.Issue.MilestoneID,
   325  		}
   326  		if _, err = CreateComment(ctx, opts); err != nil {
   327  			return err
   328  		}
   329  	}
   330  
   331  	if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil {
   332  		return err
   333  	}
   334  
   335  	if len(opts.LabelIDs) > 0 {
   336  		// During the session, SQLite3 driver cannot handle retrieve objects after update something.
   337  		// So we have to get all needed labels first.
   338  		labels := make([]*Label, 0, len(opts.LabelIDs))
   339  		if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil {
   340  			return fmt.Errorf("find all labels [label_ids: %v]: %w", opts.LabelIDs, err)
   341  		}
   342  
   343  		if err = opts.Issue.LoadPoster(ctx); err != nil {
   344  			return err
   345  		}
   346  
   347  		for _, label := range labels {
   348  			// Silently drop invalid labels.
   349  			if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID {
   350  				continue
   351  			}
   352  
   353  			if err = newIssueLabel(ctx, opts.Issue, label, opts.Issue.Poster); err != nil {
   354  				return fmt.Errorf("addLabel [id: %d]: %w", label.ID, err)
   355  			}
   356  		}
   357  	}
   358  
   359  	if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil {
   360  		return err
   361  	}
   362  
   363  	if len(opts.Attachments) > 0 {
   364  		attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
   365  		if err != nil {
   366  			return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
   367  		}
   368  
   369  		for i := 0; i < len(attachments); i++ {
   370  			attachments[i].IssueID = opts.Issue.ID
   371  			if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
   372  				return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
   373  			}
   374  		}
   375  	}
   376  	if err = opts.Issue.LoadAttributes(ctx); err != nil {
   377  		return err
   378  	}
   379  
   380  	return opts.Issue.AddCrossReferences(ctx, doer, false)
   381  }
   382  
   383  // NewIssue creates new issue with labels for repository.
   384  func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
   385  	ctx, committer, err := db.TxContext(ctx)
   386  	if err != nil {
   387  		return err
   388  	}
   389  	defer committer.Close()
   390  
   391  	idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID)
   392  	if err != nil {
   393  		return fmt.Errorf("generate issue index failed: %w", err)
   394  	}
   395  
   396  	issue.Index = idx
   397  
   398  	if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
   399  		Repo:        repo,
   400  		Issue:       issue,
   401  		LabelIDs:    labelIDs,
   402  		Attachments: uuids,
   403  	}); err != nil {
   404  		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
   405  			return err
   406  		}
   407  		return fmt.Errorf("newIssue: %w", err)
   408  	}
   409  
   410  	if err = committer.Commit(); err != nil {
   411  		return fmt.Errorf("Commit: %w", err)
   412  	}
   413  
   414  	return nil
   415  }
   416  
   417  // UpdateIssueMentions updates issue-user relations for mentioned users.
   418  func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error {
   419  	if len(mentions) == 0 {
   420  		return nil
   421  	}
   422  	ids := make([]int64, len(mentions))
   423  	for i, u := range mentions {
   424  		ids[i] = u.ID
   425  	}
   426  	if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil {
   427  		return fmt.Errorf("UpdateIssueUsersByMentions: %w", err)
   428  	}
   429  	return nil
   430  }
   431  
   432  // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
   433  func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) {
   434  	// if the deadline hasn't changed do nothing
   435  	if issue.DeadlineUnix == deadlineUnix {
   436  		return nil
   437  	}
   438  	ctx, committer, err := db.TxContext(ctx)
   439  	if err != nil {
   440  		return err
   441  	}
   442  	defer committer.Close()
   443  
   444  	// Update the deadline
   445  	if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil {
   446  		return err
   447  	}
   448  
   449  	// Make the comment
   450  	if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil {
   451  		return fmt.Errorf("createRemovedDueDateComment: %w", err)
   452  	}
   453  
   454  	return committer.Commit()
   455  }
   456  
   457  // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database.
   458  func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) {
   459  	rawMentions := references.FindAllMentionsMarkdown(content)
   460  	mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions)
   461  	if err != nil {
   462  		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
   463  	}
   464  
   465  	notBlocked := make([]*user_model.User, 0, len(mentions))
   466  	for _, user := range mentions {
   467  		if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
   468  			notBlocked = append(notBlocked, user)
   469  		}
   470  	}
   471  	mentions = notBlocked
   472  
   473  	if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
   474  		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
   475  	}
   476  	return mentions, err
   477  }
   478  
   479  // ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that
   480  // don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
   481  func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) {
   482  	if len(mentions) == 0 {
   483  		return nil, nil
   484  	}
   485  	if err = issue.LoadRepo(ctx); err != nil {
   486  		return nil, err
   487  	}
   488  
   489  	resolved := make(map[string]bool, 10)
   490  	var mentionTeams []string
   491  
   492  	if err := issue.Repo.LoadOwner(ctx); err != nil {
   493  		return nil, err
   494  	}
   495  
   496  	repoOwnerIsOrg := issue.Repo.Owner.IsOrganization()
   497  	if repoOwnerIsOrg {
   498  		mentionTeams = make([]string, 0, 5)
   499  	}
   500  
   501  	resolved[doer.LowerName] = true
   502  	for _, name := range mentions {
   503  		name := strings.ToLower(name)
   504  		if _, ok := resolved[name]; ok {
   505  			continue
   506  		}
   507  		if repoOwnerIsOrg && strings.Contains(name, "/") {
   508  			names := strings.Split(name, "/")
   509  			if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName {
   510  				continue
   511  			}
   512  			mentionTeams = append(mentionTeams, names[1])
   513  			resolved[name] = true
   514  		} else {
   515  			resolved[name] = false
   516  		}
   517  	}
   518  
   519  	if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 {
   520  		teams := make([]*organization.Team, 0, len(mentionTeams))
   521  		if err := db.GetEngine(ctx).
   522  			Join("INNER", "team_repo", "team_repo.team_id = team.id").
   523  			Where("team_repo.repo_id=?", issue.Repo.ID).
   524  			In("team.lower_name", mentionTeams).
   525  			Find(&teams); err != nil {
   526  			return nil, fmt.Errorf("find mentioned teams: %w", err)
   527  		}
   528  		if len(teams) != 0 {
   529  			checked := make([]int64, 0, len(teams))
   530  			unittype := unit.TypeIssues
   531  			if issue.IsPull {
   532  				unittype = unit.TypePullRequests
   533  			}
   534  			for _, team := range teams {
   535  				if team.AccessMode >= perm.AccessModeAdmin {
   536  					checked = append(checked, team.ID)
   537  					resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
   538  					continue
   539  				}
   540  				has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
   541  				if err != nil {
   542  					return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
   543  				}
   544  				if has {
   545  					checked = append(checked, team.ID)
   546  					resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
   547  				}
   548  			}
   549  			if len(checked) != 0 {
   550  				teamusers := make([]*user_model.User, 0, 20)
   551  				if err := db.GetEngine(ctx).
   552  					Join("INNER", "team_user", "team_user.uid = `user`.id").
   553  					In("`team_user`.team_id", checked).
   554  					And("`user`.is_active = ?", true).
   555  					And("`user`.prohibit_login = ?", false).
   556  					Find(&teamusers); err != nil {
   557  					return nil, fmt.Errorf("get teams users: %w", err)
   558  				}
   559  				if len(teamusers) > 0 {
   560  					users = make([]*user_model.User, 0, len(teamusers))
   561  					for _, user := range teamusers {
   562  						if already, ok := resolved[user.LowerName]; !ok || !already {
   563  							users = append(users, user)
   564  							resolved[user.LowerName] = true
   565  						}
   566  					}
   567  				}
   568  			}
   569  		}
   570  	}
   571  
   572  	// Remove names already in the list to avoid querying the database if pending names remain
   573  	mentionUsers := make([]string, 0, len(resolved))
   574  	for name, already := range resolved {
   575  		if !already {
   576  			mentionUsers = append(mentionUsers, name)
   577  		}
   578  	}
   579  	if len(mentionUsers) == 0 {
   580  		return users, err
   581  	}
   582  
   583  	if users == nil {
   584  		users = make([]*user_model.User, 0, len(mentionUsers))
   585  	}
   586  
   587  	unchecked := make([]*user_model.User, 0, len(mentionUsers))
   588  	if err := db.GetEngine(ctx).
   589  		Where("`user`.is_active = ?", true).
   590  		And("`user`.prohibit_login = ?", false).
   591  		In("`user`.lower_name", mentionUsers).
   592  		Find(&unchecked); err != nil {
   593  		return nil, fmt.Errorf("find mentioned users: %w", err)
   594  	}
   595  	for _, user := range unchecked {
   596  		if already := resolved[user.LowerName]; already || user.IsOrganization() {
   597  			continue
   598  		}
   599  		// Normal users must have read access to the referencing issue
   600  		perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user)
   601  		if err != nil {
   602  			return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err)
   603  		}
   604  		if !perm.CanReadIssuesOrPulls(issue.IsPull) {
   605  			continue
   606  		}
   607  		users = append(users, user)
   608  	}
   609  
   610  	return users, err
   611  }
   612  
   613  // UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID
   614  func UpdateIssuesMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error {
   615  	_, err := db.GetEngine(ctx).Table("issue").
   616  		Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
   617  		And("original_author_id = ?", originalAuthorID).
   618  		Update(map[string]any{
   619  			"poster_id":          posterID,
   620  			"original_author":    "",
   621  			"original_author_id": 0,
   622  		})
   623  	return err
   624  }
   625  
   626  // UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID
   627  func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error {
   628  	_, err := db.GetEngine(ctx).Table("reaction").
   629  		Where("original_author_id = ?", originalAuthorID).
   630  		And(migratedIssueCond(gitServiceType)).
   631  		Update(map[string]any{
   632  			"user_id":            userID,
   633  			"original_author":    "",
   634  			"original_author_id": 0,
   635  		})
   636  	return err
   637  }
   638  
   639  // DeleteIssuesByRepoID deletes issues by repositories id
   640  func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
   641  	// MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
   642  	// so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
   643  	sess := db.GetEngine(ctx)
   644  
   645  	for {
   646  		issueIDs := make([]int64, 0, db.DefaultMaxInSize)
   647  
   648  		err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs)
   649  		if err != nil {
   650  			return nil, err
   651  		}
   652  
   653  		if len(issueIDs) == 0 {
   654  			break
   655  		}
   656  
   657  		// Delete content histories
   658  		_, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{})
   659  		if err != nil {
   660  			return nil, err
   661  		}
   662  
   663  		// Delete comments and attachments
   664  		_, err = sess.In("issue_id", issueIDs).Delete(&Comment{})
   665  		if err != nil {
   666  			return nil, err
   667  		}
   668  
   669  		// Dependencies for issues in this repository
   670  		_, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{})
   671  		if err != nil {
   672  			return nil, err
   673  		}
   674  
   675  		// Delete dependencies for issues in other repositories
   676  		_, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{})
   677  		if err != nil {
   678  			return nil, err
   679  		}
   680  
   681  		_, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{})
   682  		if err != nil {
   683  			return nil, err
   684  		}
   685  
   686  		_, err = sess.In("issue_id", issueIDs).Delete(&Reaction{})
   687  		if err != nil {
   688  			return nil, err
   689  		}
   690  
   691  		_, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{})
   692  		if err != nil {
   693  			return nil, err
   694  		}
   695  
   696  		_, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{})
   697  		if err != nil {
   698  			return nil, err
   699  		}
   700  
   701  		_, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{})
   702  		if err != nil {
   703  			return nil, err
   704  		}
   705  
   706  		_, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{})
   707  		if err != nil {
   708  			return nil, err
   709  		}
   710  
   711  		_, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{})
   712  		if err != nil {
   713  			return nil, err
   714  		}
   715  
   716  		var attachments []*repo_model.Attachment
   717  		err = sess.In("issue_id", issueIDs).Find(&attachments)
   718  		if err != nil {
   719  			return nil, err
   720  		}
   721  
   722  		for j := range attachments {
   723  			attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
   724  		}
   725  
   726  		_, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{})
   727  		if err != nil {
   728  			return nil, err
   729  		}
   730  
   731  		_, err = sess.In("id", issueIDs).Delete(&Issue{})
   732  		if err != nil {
   733  			return nil, err
   734  		}
   735  	}
   736  
   737  	return attachmentPaths, err
   738  }
   739  
   740  // DeleteOrphanedIssues delete issues without a repo
   741  func DeleteOrphanedIssues(ctx context.Context) error {
   742  	var attachmentPaths []string
   743  	err := db.WithTx(ctx, func(ctx context.Context) error {
   744  		var ids []int64
   745  
   746  		if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
   747  			Join("LEFT", "repository", "issue.repo_id=repository.id").
   748  			Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
   749  			Find(&ids); err != nil {
   750  			return err
   751  		}
   752  
   753  		for i := range ids {
   754  			paths, err := DeleteIssuesByRepoID(ctx, ids[i])
   755  			if err != nil {
   756  				return err
   757  			}
   758  			attachmentPaths = append(attachmentPaths, paths...)
   759  		}
   760  
   761  		return nil
   762  	})
   763  	if err != nil {
   764  		return err
   765  	}
   766  
   767  	// Remove issue attachment files.
   768  	for i := range attachmentPaths {
   769  		system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
   770  	}
   771  	return nil
   772  }