code.gitea.io/gitea@v1.21.7/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  // UpdateIssueByAPI updates all allowed fields of given issue.
   433  // If the issue status is changed a statusChangeComment is returned
   434  // similarly if the title is changed the titleChanged bool is set to true
   435  func UpdateIssueByAPI(ctx context.Context, issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) {
   436  	ctx, committer, err := db.TxContext(ctx)
   437  	if err != nil {
   438  		return nil, false, err
   439  	}
   440  	defer committer.Close()
   441  
   442  	if err := issue.LoadRepo(ctx); err != nil {
   443  		return nil, false, fmt.Errorf("loadRepo: %w", err)
   444  	}
   445  
   446  	// Reload the issue
   447  	currentIssue, err := GetIssueByID(ctx, issue.ID)
   448  	if err != nil {
   449  		return nil, false, err
   450  	}
   451  
   452  	if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(
   453  		"name", "content", "milestone_id", "priority",
   454  		"deadline_unix", "updated_unix", "is_locked").
   455  		Update(issue); err != nil {
   456  		return nil, false, err
   457  	}
   458  
   459  	titleChanged = currentIssue.Title != issue.Title
   460  	if titleChanged {
   461  		opts := &CreateCommentOptions{
   462  			Type:     CommentTypeChangeTitle,
   463  			Doer:     doer,
   464  			Repo:     issue.Repo,
   465  			Issue:    issue,
   466  			OldTitle: currentIssue.Title,
   467  			NewTitle: issue.Title,
   468  		}
   469  		_, err := CreateComment(ctx, opts)
   470  		if err != nil {
   471  			return nil, false, fmt.Errorf("createComment: %w", err)
   472  		}
   473  	}
   474  
   475  	if currentIssue.IsClosed != issue.IsClosed {
   476  		statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false)
   477  		if err != nil {
   478  			return nil, false, err
   479  		}
   480  	}
   481  
   482  	if err := issue.AddCrossReferences(ctx, doer, true); err != nil {
   483  		return nil, false, err
   484  	}
   485  	return statusChangeComment, titleChanged, committer.Commit()
   486  }
   487  
   488  // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
   489  func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) {
   490  	// if the deadline hasn't changed do nothing
   491  	if issue.DeadlineUnix == deadlineUnix {
   492  		return nil
   493  	}
   494  	ctx, committer, err := db.TxContext(ctx)
   495  	if err != nil {
   496  		return err
   497  	}
   498  	defer committer.Close()
   499  
   500  	// Update the deadline
   501  	if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil {
   502  		return err
   503  	}
   504  
   505  	// Make the comment
   506  	if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil {
   507  		return fmt.Errorf("createRemovedDueDateComment: %w", err)
   508  	}
   509  
   510  	return committer.Commit()
   511  }
   512  
   513  // DeleteInIssue delete records in beans with external key issue_id = ?
   514  func DeleteInIssue(ctx context.Context, issueID int64, beans ...any) error {
   515  	e := db.GetEngine(ctx)
   516  	for _, bean := range beans {
   517  		if _, err := e.In("issue_id", issueID).Delete(bean); err != nil {
   518  			return err
   519  		}
   520  	}
   521  	return nil
   522  }
   523  
   524  // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database.
   525  func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) {
   526  	rawMentions := references.FindAllMentionsMarkdown(content)
   527  	mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions)
   528  	if err != nil {
   529  		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
   530  	}
   531  	if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
   532  		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
   533  	}
   534  	return mentions, err
   535  }
   536  
   537  // ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that
   538  // don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
   539  func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) {
   540  	if len(mentions) == 0 {
   541  		return nil, nil
   542  	}
   543  	if err = issue.LoadRepo(ctx); err != nil {
   544  		return nil, err
   545  	}
   546  
   547  	resolved := make(map[string]bool, 10)
   548  	var mentionTeams []string
   549  
   550  	if err := issue.Repo.LoadOwner(ctx); err != nil {
   551  		return nil, err
   552  	}
   553  
   554  	repoOwnerIsOrg := issue.Repo.Owner.IsOrganization()
   555  	if repoOwnerIsOrg {
   556  		mentionTeams = make([]string, 0, 5)
   557  	}
   558  
   559  	resolved[doer.LowerName] = true
   560  	for _, name := range mentions {
   561  		name := strings.ToLower(name)
   562  		if _, ok := resolved[name]; ok {
   563  			continue
   564  		}
   565  		if repoOwnerIsOrg && strings.Contains(name, "/") {
   566  			names := strings.Split(name, "/")
   567  			if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName {
   568  				continue
   569  			}
   570  			mentionTeams = append(mentionTeams, names[1])
   571  			resolved[name] = true
   572  		} else {
   573  			resolved[name] = false
   574  		}
   575  	}
   576  
   577  	if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 {
   578  		teams := make([]*organization.Team, 0, len(mentionTeams))
   579  		if err := db.GetEngine(ctx).
   580  			Join("INNER", "team_repo", "team_repo.team_id = team.id").
   581  			Where("team_repo.repo_id=?", issue.Repo.ID).
   582  			In("team.lower_name", mentionTeams).
   583  			Find(&teams); err != nil {
   584  			return nil, fmt.Errorf("find mentioned teams: %w", err)
   585  		}
   586  		if len(teams) != 0 {
   587  			checked := make([]int64, 0, len(teams))
   588  			unittype := unit.TypeIssues
   589  			if issue.IsPull {
   590  				unittype = unit.TypePullRequests
   591  			}
   592  			for _, team := range teams {
   593  				if team.AccessMode >= perm.AccessModeAdmin {
   594  					checked = append(checked, team.ID)
   595  					resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
   596  					continue
   597  				}
   598  				has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
   599  				if err != nil {
   600  					return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
   601  				}
   602  				if has {
   603  					checked = append(checked, team.ID)
   604  					resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
   605  				}
   606  			}
   607  			if len(checked) != 0 {
   608  				teamusers := make([]*user_model.User, 0, 20)
   609  				if err := db.GetEngine(ctx).
   610  					Join("INNER", "team_user", "team_user.uid = `user`.id").
   611  					In("`team_user`.team_id", checked).
   612  					And("`user`.is_active = ?", true).
   613  					And("`user`.prohibit_login = ?", false).
   614  					Find(&teamusers); err != nil {
   615  					return nil, fmt.Errorf("get teams users: %w", err)
   616  				}
   617  				if len(teamusers) > 0 {
   618  					users = make([]*user_model.User, 0, len(teamusers))
   619  					for _, user := range teamusers {
   620  						if already, ok := resolved[user.LowerName]; !ok || !already {
   621  							users = append(users, user)
   622  							resolved[user.LowerName] = true
   623  						}
   624  					}
   625  				}
   626  			}
   627  		}
   628  	}
   629  
   630  	// Remove names already in the list to avoid querying the database if pending names remain
   631  	mentionUsers := make([]string, 0, len(resolved))
   632  	for name, already := range resolved {
   633  		if !already {
   634  			mentionUsers = append(mentionUsers, name)
   635  		}
   636  	}
   637  	if len(mentionUsers) == 0 {
   638  		return users, err
   639  	}
   640  
   641  	if users == nil {
   642  		users = make([]*user_model.User, 0, len(mentionUsers))
   643  	}
   644  
   645  	unchecked := make([]*user_model.User, 0, len(mentionUsers))
   646  	if err := db.GetEngine(ctx).
   647  		Where("`user`.is_active = ?", true).
   648  		And("`user`.prohibit_login = ?", false).
   649  		In("`user`.lower_name", mentionUsers).
   650  		Find(&unchecked); err != nil {
   651  		return nil, fmt.Errorf("find mentioned users: %w", err)
   652  	}
   653  	for _, user := range unchecked {
   654  		if already := resolved[user.LowerName]; already || user.IsOrganization() {
   655  			continue
   656  		}
   657  		// Normal users must have read access to the referencing issue
   658  		perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user)
   659  		if err != nil {
   660  			return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err)
   661  		}
   662  		if !perm.CanReadIssuesOrPulls(issue.IsPull) {
   663  			continue
   664  		}
   665  		users = append(users, user)
   666  	}
   667  
   668  	return users, err
   669  }
   670  
   671  // UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID
   672  func UpdateIssuesMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error {
   673  	_, err := db.GetEngine(ctx).Table("issue").
   674  		Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
   675  		And("original_author_id = ?", originalAuthorID).
   676  		Update(map[string]any{
   677  			"poster_id":          posterID,
   678  			"original_author":    "",
   679  			"original_author_id": 0,
   680  		})
   681  	return err
   682  }
   683  
   684  // UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID
   685  func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error {
   686  	_, err := db.GetEngine(ctx).Table("reaction").
   687  		Where("original_author_id = ?", originalAuthorID).
   688  		And(migratedIssueCond(gitServiceType)).
   689  		Update(map[string]any{
   690  			"user_id":            userID,
   691  			"original_author":    "",
   692  			"original_author_id": 0,
   693  		})
   694  	return err
   695  }
   696  
   697  // DeleteIssuesByRepoID deletes issues by repositories id
   698  func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
   699  	// MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
   700  	// so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
   701  	sess := db.GetEngine(ctx)
   702  
   703  	for {
   704  		issueIDs := make([]int64, 0, db.DefaultMaxInSize)
   705  
   706  		err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs)
   707  		if err != nil {
   708  			return nil, err
   709  		}
   710  
   711  		if len(issueIDs) == 0 {
   712  			break
   713  		}
   714  
   715  		// Delete content histories
   716  		_, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{})
   717  		if err != nil {
   718  			return nil, err
   719  		}
   720  
   721  		// Delete comments and attachments
   722  		_, err = sess.In("issue_id", issueIDs).Delete(&Comment{})
   723  		if err != nil {
   724  			return nil, err
   725  		}
   726  
   727  		// Dependencies for issues in this repository
   728  		_, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{})
   729  		if err != nil {
   730  			return nil, err
   731  		}
   732  
   733  		// Delete dependencies for issues in other repositories
   734  		_, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{})
   735  		if err != nil {
   736  			return nil, err
   737  		}
   738  
   739  		_, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{})
   740  		if err != nil {
   741  			return nil, err
   742  		}
   743  
   744  		_, err = sess.In("issue_id", issueIDs).Delete(&Reaction{})
   745  		if err != nil {
   746  			return nil, err
   747  		}
   748  
   749  		_, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{})
   750  		if err != nil {
   751  			return nil, err
   752  		}
   753  
   754  		_, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{})
   755  		if err != nil {
   756  			return nil, err
   757  		}
   758  
   759  		_, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{})
   760  		if err != nil {
   761  			return nil, err
   762  		}
   763  
   764  		_, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{})
   765  		if err != nil {
   766  			return nil, err
   767  		}
   768  
   769  		_, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{})
   770  		if err != nil {
   771  			return nil, err
   772  		}
   773  
   774  		var attachments []*repo_model.Attachment
   775  		err = sess.In("issue_id", issueIDs).Find(&attachments)
   776  		if err != nil {
   777  			return nil, err
   778  		}
   779  
   780  		for j := range attachments {
   781  			attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
   782  		}
   783  
   784  		_, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{})
   785  		if err != nil {
   786  			return nil, err
   787  		}
   788  
   789  		_, err = sess.In("id", issueIDs).Delete(&Issue{})
   790  		if err != nil {
   791  			return nil, err
   792  		}
   793  	}
   794  
   795  	return attachmentPaths, err
   796  }
   797  
   798  // DeleteOrphanedIssues delete issues without a repo
   799  func DeleteOrphanedIssues(ctx context.Context) error {
   800  	var attachmentPaths []string
   801  	err := db.WithTx(ctx, func(ctx context.Context) error {
   802  		var ids []int64
   803  
   804  		if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
   805  			Join("LEFT", "repository", "issue.repo_id=repository.id").
   806  			Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
   807  			Find(&ids); err != nil {
   808  			return err
   809  		}
   810  
   811  		for i := range ids {
   812  			paths, err := DeleteIssuesByRepoID(ctx, ids[i])
   813  			if err != nil {
   814  				return err
   815  			}
   816  			attachmentPaths = append(attachmentPaths, paths...)
   817  		}
   818  
   819  		return nil
   820  	})
   821  	if err != nil {
   822  		return err
   823  	}
   824  
   825  	// Remove issue attachment files.
   826  	for i := range attachmentPaths {
   827  		system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
   828  	}
   829  	return nil
   830  }