mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Backport #34142 by @lunny Fix the bug when deleting orphaned issues attachments. The attachments maybe stored on other storages service rather than disk. Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
		
			
				
	
	
		
			726 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			726 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package issues
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"code.gitea.io/gitea/models/db"
 | 
						|
	"code.gitea.io/gitea/models/organization"
 | 
						|
	access_model "code.gitea.io/gitea/models/perm/access"
 | 
						|
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						|
	"code.gitea.io/gitea/models/unit"
 | 
						|
	user_model "code.gitea.io/gitea/models/user"
 | 
						|
	"code.gitea.io/gitea/modules/git"
 | 
						|
	"code.gitea.io/gitea/modules/references"
 | 
						|
	api "code.gitea.io/gitea/modules/structs"
 | 
						|
	"code.gitea.io/gitea/modules/timeutil"
 | 
						|
	"code.gitea.io/gitea/modules/util"
 | 
						|
 | 
						|
	"xorm.io/builder"
 | 
						|
)
 | 
						|
 | 
						|
// UpdateIssueCols updates cols of issue
 | 
						|
func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
 | 
						|
	_, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue)
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
// ErrIssueIsClosed is used when close a closed issue
 | 
						|
type ErrIssueIsClosed struct {
 | 
						|
	ID     int64
 | 
						|
	RepoID int64
 | 
						|
	Index  int64
 | 
						|
	IsPull bool
 | 
						|
}
 | 
						|
 | 
						|
// IsErrIssueIsClosed checks if an error is a ErrIssueIsClosed.
 | 
						|
func IsErrIssueIsClosed(err error) bool {
 | 
						|
	_, ok := err.(ErrIssueIsClosed)
 | 
						|
	return ok
 | 
						|
}
 | 
						|
 | 
						|
func (err ErrIssueIsClosed) Error() string {
 | 
						|
	return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already closed", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index)
 | 
						|
}
 | 
						|
 | 
						|
func SetIssueAsClosed(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) {
 | 
						|
	if issue.IsClosed {
 | 
						|
		return nil, ErrIssueIsClosed{
 | 
						|
			ID:     issue.ID,
 | 
						|
			RepoID: issue.RepoID,
 | 
						|
			Index:  issue.Index,
 | 
						|
			IsPull: issue.IsPull,
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Check for open dependencies
 | 
						|
	if issue.Repo.IsDependenciesEnabled(ctx) {
 | 
						|
		// 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
 | 
						|
		noDeps, err := IssueNoDependenciesLeft(ctx, issue)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		if !noDeps {
 | 
						|
			return nil, ErrDependenciesLeft{issue.ID}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	issue.IsClosed = true
 | 
						|
	issue.ClosedUnix = timeutil.TimeStampNow()
 | 
						|
 | 
						|
	if cnt, err := db.GetEngine(ctx).ID(issue.ID).Cols("is_closed", "closed_unix").
 | 
						|
		Where("is_closed = ?", false).
 | 
						|
		Update(issue); err != nil {
 | 
						|
		return nil, err
 | 
						|
	} else if cnt != 1 {
 | 
						|
		return nil, ErrIssueAlreadyChanged
 | 
						|
	}
 | 
						|
 | 
						|
	return updateIssueNumbers(ctx, issue, doer, util.Iif(isMergePull, CommentTypeMergePull, CommentTypeClose))
 | 
						|
}
 | 
						|
 | 
						|
// ErrIssueIsOpen is used when reopen an opened issue
 | 
						|
type ErrIssueIsOpen struct {
 | 
						|
	ID     int64
 | 
						|
	RepoID int64
 | 
						|
	IsPull bool
 | 
						|
	Index  int64
 | 
						|
}
 | 
						|
 | 
						|
// IsErrIssueIsOpen checks if an error is a ErrIssueIsOpen.
 | 
						|
func IsErrIssueIsOpen(err error) bool {
 | 
						|
	_, ok := err.(ErrIssueIsOpen)
 | 
						|
	return ok
 | 
						|
}
 | 
						|
 | 
						|
func (err ErrIssueIsOpen) Error() string {
 | 
						|
	return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index)
 | 
						|
}
 | 
						|
 | 
						|
func setIssueAsReopen(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) {
 | 
						|
	if !issue.IsClosed {
 | 
						|
		return nil, ErrIssueIsOpen{
 | 
						|
			ID:     issue.ID,
 | 
						|
			RepoID: issue.RepoID,
 | 
						|
			Index:  issue.Index,
 | 
						|
			IsPull: issue.IsPull,
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	issue.IsClosed = false
 | 
						|
	issue.ClosedUnix = 0
 | 
						|
 | 
						|
	if cnt, err := db.GetEngine(ctx).ID(issue.ID).Cols("is_closed", "closed_unix").
 | 
						|
		Where("is_closed = ?", true).
 | 
						|
		Update(issue); err != nil {
 | 
						|
		return nil, err
 | 
						|
	} else if cnt != 1 {
 | 
						|
		return nil, ErrIssueAlreadyChanged
 | 
						|
	}
 | 
						|
 | 
						|
	return updateIssueNumbers(ctx, issue, doer, CommentTypeReopen)
 | 
						|
}
 | 
						|
 | 
						|
func updateIssueNumbers(ctx context.Context, issue *Issue, doer *user_model.User, cmtType CommentType) (*Comment, error) {
 | 
						|
	// Update issue count of labels
 | 
						|
	if err := issue.LoadLabels(ctx); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	for idx := range issue.Labels {
 | 
						|
		if err := updateLabelCols(ctx, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Update issue count of milestone
 | 
						|
	if issue.MilestoneID > 0 {
 | 
						|
		if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// update repository's issue closed number
 | 
						|
	if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return CreateComment(ctx, &CreateCommentOptions{
 | 
						|
		Type:  cmtType,
 | 
						|
		Doer:  doer,
 | 
						|
		Repo:  issue.Repo,
 | 
						|
		Issue: issue,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// CloseIssue changes issue status to closed.
 | 
						|
func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) {
 | 
						|
	if err := issue.LoadRepo(ctx); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if err := issue.LoadPoster(ctx); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	ctx, committer, err := db.TxContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer committer.Close()
 | 
						|
 | 
						|
	comment, err := SetIssueAsClosed(ctx, issue, doer, false)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if err := committer.Commit(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return comment, nil
 | 
						|
}
 | 
						|
 | 
						|
// ReopenIssue changes issue status to open.
 | 
						|
func ReopenIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) {
 | 
						|
	if err := issue.LoadRepo(ctx); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if err := issue.LoadPoster(ctx); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	ctx, committer, err := db.TxContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer committer.Close()
 | 
						|
 | 
						|
	comment, err := setIssueAsReopen(ctx, issue, doer)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if err := committer.Commit(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return comment, nil
 | 
						|
}
 | 
						|
 | 
						|
// ChangeIssueTitle changes the title of this issue, as the given user.
 | 
						|
func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, oldTitle string) (err error) {
 | 
						|
	ctx, committer, err := db.TxContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer committer.Close()
 | 
						|
 | 
						|
	issue.Title = util.EllipsisDisplayString(issue.Title, 255)
 | 
						|
	if err = UpdateIssueCols(ctx, issue, "name"); err != nil {
 | 
						|
		return fmt.Errorf("updateIssueCols: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	if err = issue.LoadRepo(ctx); err != nil {
 | 
						|
		return fmt.Errorf("loadRepo: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	opts := &CreateCommentOptions{
 | 
						|
		Type:     CommentTypeChangeTitle,
 | 
						|
		Doer:     doer,
 | 
						|
		Repo:     issue.Repo,
 | 
						|
		Issue:    issue,
 | 
						|
		OldTitle: oldTitle,
 | 
						|
		NewTitle: issue.Title,
 | 
						|
	}
 | 
						|
	if _, err = CreateComment(ctx, opts); err != nil {
 | 
						|
		return fmt.Errorf("createComment: %w", err)
 | 
						|
	}
 | 
						|
	if err = issue.AddCrossReferences(ctx, doer, true); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return committer.Commit()
 | 
						|
}
 | 
						|
 | 
						|
// ChangeIssueRef changes the branch of this issue, as the given user.
 | 
						|
func ChangeIssueRef(ctx context.Context, issue *Issue, doer *user_model.User, oldRef string) (err error) {
 | 
						|
	ctx, committer, err := db.TxContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer committer.Close()
 | 
						|
 | 
						|
	if err = UpdateIssueCols(ctx, issue, "ref"); err != nil {
 | 
						|
		return fmt.Errorf("updateIssueCols: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	if err = issue.LoadRepo(ctx); err != nil {
 | 
						|
		return fmt.Errorf("loadRepo: %w", err)
 | 
						|
	}
 | 
						|
	oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix)
 | 
						|
	newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix)
 | 
						|
 | 
						|
	opts := &CreateCommentOptions{
 | 
						|
		Type:   CommentTypeChangeIssueRef,
 | 
						|
		Doer:   doer,
 | 
						|
		Repo:   issue.Repo,
 | 
						|
		Issue:  issue,
 | 
						|
		OldRef: oldRefFriendly,
 | 
						|
		NewRef: newRefFriendly,
 | 
						|
	}
 | 
						|
	if _, err = CreateComment(ctx, opts); err != nil {
 | 
						|
		return fmt.Errorf("createComment: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return committer.Commit()
 | 
						|
}
 | 
						|
 | 
						|
// AddDeletePRBranchComment adds delete branch comment for pull request issue
 | 
						|
func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issueID int64, branchName string) error {
 | 
						|
	issue, err := GetIssueByID(ctx, issueID)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	opts := &CreateCommentOptions{
 | 
						|
		Type:   CommentTypeDeleteBranch,
 | 
						|
		Doer:   doer,
 | 
						|
		Repo:   repo,
 | 
						|
		Issue:  issue,
 | 
						|
		OldRef: branchName,
 | 
						|
	}
 | 
						|
	_, err = CreateComment(ctx, opts)
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
// UpdateIssueAttachments update attachments by UUIDs for the issue
 | 
						|
func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) {
 | 
						|
	ctx, committer, err := db.TxContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer committer.Close()
 | 
						|
	attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
 | 
						|
	}
 | 
						|
	for i := 0; i < len(attachments); i++ {
 | 
						|
		attachments[i].IssueID = issueID
 | 
						|
		if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
 | 
						|
			return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return committer.Commit()
 | 
						|
}
 | 
						|
 | 
						|
// ChangeIssueContent changes issue content, as the given user.
 | 
						|
func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string, contentVersion int) (err error) {
 | 
						|
	ctx, committer, err := db.TxContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer committer.Close()
 | 
						|
 | 
						|
	hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0)
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("HasIssueContentHistory: %w", err)
 | 
						|
	}
 | 
						|
	if !hasContentHistory {
 | 
						|
		if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0,
 | 
						|
			issue.CreatedUnix, issue.Content, true); err != nil {
 | 
						|
			return fmt.Errorf("SaveIssueContentHistory: %w", err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	issue.Content = content
 | 
						|
	issue.ContentVersion = contentVersion + 1
 | 
						|
 | 
						|
	affected, err := db.GetEngine(ctx).ID(issue.ID).Cols("content", "content_version").Where("content_version = ?", contentVersion).Update(issue)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	if affected == 0 {
 | 
						|
		return ErrIssueAlreadyChanged
 | 
						|
	}
 | 
						|
 | 
						|
	if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0,
 | 
						|
		timeutil.TimeStampNow(), issue.Content, false); err != nil {
 | 
						|
		return fmt.Errorf("SaveIssueContentHistory: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	if err = issue.AddCrossReferences(ctx, doer, true); err != nil {
 | 
						|
		return fmt.Errorf("addCrossReferences: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return committer.Commit()
 | 
						|
}
 | 
						|
 | 
						|
// NewIssueOptions represents the options of a new issue.
 | 
						|
type NewIssueOptions struct {
 | 
						|
	Repo        *repo_model.Repository
 | 
						|
	Issue       *Issue
 | 
						|
	LabelIDs    []int64
 | 
						|
	Attachments []string // In UUID format.
 | 
						|
	IsPull      bool
 | 
						|
}
 | 
						|
 | 
						|
// NewIssueWithIndex creates issue with given index
 | 
						|
func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) {
 | 
						|
	e := db.GetEngine(ctx)
 | 
						|
	opts.Issue.Title = strings.TrimSpace(opts.Issue.Title)
 | 
						|
 | 
						|
	if opts.Issue.MilestoneID > 0 {
 | 
						|
		milestone, err := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID)
 | 
						|
		if err != nil && !IsErrMilestoneNotExist(err) {
 | 
						|
			return fmt.Errorf("getMilestoneByID: %w", err)
 | 
						|
		}
 | 
						|
 | 
						|
		// Assume milestone is invalid and drop silently.
 | 
						|
		opts.Issue.MilestoneID = 0
 | 
						|
		if milestone != nil {
 | 
						|
			opts.Issue.MilestoneID = milestone.ID
 | 
						|
			opts.Issue.Milestone = milestone
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if opts.Issue.Index <= 0 {
 | 
						|
		return errors.New("no issue index provided")
 | 
						|
	}
 | 
						|
	if opts.Issue.ID > 0 {
 | 
						|
		return errors.New("issue exist")
 | 
						|
	}
 | 
						|
 | 
						|
	if _, err := e.Insert(opts.Issue); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if opts.Issue.MilestoneID > 0 {
 | 
						|
		if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		opts := &CreateCommentOptions{
 | 
						|
			Type:           CommentTypeMilestone,
 | 
						|
			Doer:           doer,
 | 
						|
			Repo:           opts.Repo,
 | 
						|
			Issue:          opts.Issue,
 | 
						|
			OldMilestoneID: 0,
 | 
						|
			MilestoneID:    opts.Issue.MilestoneID,
 | 
						|
		}
 | 
						|
		if _, err = CreateComment(ctx, opts); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if len(opts.LabelIDs) > 0 {
 | 
						|
		// During the session, SQLite3 driver cannot handle retrieve objects after update something.
 | 
						|
		// So we have to get all needed labels first.
 | 
						|
		labels := make([]*Label, 0, len(opts.LabelIDs))
 | 
						|
		if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil {
 | 
						|
			return fmt.Errorf("find all labels [label_ids: %v]: %w", opts.LabelIDs, err)
 | 
						|
		}
 | 
						|
 | 
						|
		if err = opts.Issue.LoadPoster(ctx); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		for _, label := range labels {
 | 
						|
			// Silently drop invalid labels.
 | 
						|
			if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			if err = newIssueLabel(ctx, opts.Issue, label, opts.Issue.Poster); err != nil {
 | 
						|
				return fmt.Errorf("addLabel [id: %d]: %w", label.ID, err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if err := UpdateIssueAttachments(ctx, opts.Issue.ID, opts.Attachments); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if err = opts.Issue.LoadAttributes(ctx); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return opts.Issue.AddCrossReferences(ctx, doer, false)
 | 
						|
}
 | 
						|
 | 
						|
// NewIssue creates new issue with labels for repository.
 | 
						|
// The title will be cut off at 255 characters if it's longer than 255 characters.
 | 
						|
func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
 | 
						|
	ctx, committer, err := db.TxContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer committer.Close()
 | 
						|
 | 
						|
	idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID)
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("generate issue index failed: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	issue.Index = idx
 | 
						|
	issue.Title = util.EllipsisDisplayString(issue.Title, 255)
 | 
						|
 | 
						|
	if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
 | 
						|
		Repo:        repo,
 | 
						|
		Issue:       issue,
 | 
						|
		LabelIDs:    labelIDs,
 | 
						|
		Attachments: uuids,
 | 
						|
	}); err != nil {
 | 
						|
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		return fmt.Errorf("newIssue: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	if err = committer.Commit(); err != nil {
 | 
						|
		return fmt.Errorf("Commit: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// UpdateIssueMentions updates issue-user relations for mentioned users.
 | 
						|
func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error {
 | 
						|
	if len(mentions) == 0 {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	ids := make([]int64, len(mentions))
 | 
						|
	for i, u := range mentions {
 | 
						|
		ids[i] = u.ID
 | 
						|
	}
 | 
						|
	if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil {
 | 
						|
		return fmt.Errorf("UpdateIssueUsersByMentions: %w", err)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
 | 
						|
func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) {
 | 
						|
	// if the deadline hasn't changed do nothing
 | 
						|
	if issue.DeadlineUnix == deadlineUnix {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	ctx, committer, err := db.TxContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer committer.Close()
 | 
						|
 | 
						|
	// Update the deadline
 | 
						|
	if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Make the comment
 | 
						|
	if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil {
 | 
						|
		return fmt.Errorf("createRemovedDueDateComment: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return committer.Commit()
 | 
						|
}
 | 
						|
 | 
						|
// FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database.
 | 
						|
func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) {
 | 
						|
	rawMentions := references.FindAllMentionsMarkdown(content)
 | 
						|
	mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
 | 
						|
	}
 | 
						|
 | 
						|
	notBlocked := make([]*user_model.User, 0, len(mentions))
 | 
						|
	for _, user := range mentions {
 | 
						|
		if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
 | 
						|
			notBlocked = append(notBlocked, user)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	mentions = notBlocked
 | 
						|
 | 
						|
	if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
 | 
						|
		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
 | 
						|
	}
 | 
						|
	return mentions, err
 | 
						|
}
 | 
						|
 | 
						|
// ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that
 | 
						|
// don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
 | 
						|
func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) {
 | 
						|
	if len(mentions) == 0 {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
	if err = issue.LoadRepo(ctx); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	resolved := make(map[string]bool, 10)
 | 
						|
	var mentionTeams []string
 | 
						|
 | 
						|
	if err := issue.Repo.LoadOwner(ctx); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	repoOwnerIsOrg := issue.Repo.Owner.IsOrganization()
 | 
						|
	if repoOwnerIsOrg {
 | 
						|
		mentionTeams = make([]string, 0, 5)
 | 
						|
	}
 | 
						|
 | 
						|
	resolved[doer.LowerName] = true
 | 
						|
	for _, name := range mentions {
 | 
						|
		name := strings.ToLower(name)
 | 
						|
		if _, ok := resolved[name]; ok {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if repoOwnerIsOrg && strings.Contains(name, "/") {
 | 
						|
			names := strings.Split(name, "/")
 | 
						|
			if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			mentionTeams = append(mentionTeams, names[1])
 | 
						|
			resolved[name] = true
 | 
						|
		} else {
 | 
						|
			resolved[name] = false
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 {
 | 
						|
		teams := make([]*organization.Team, 0, len(mentionTeams))
 | 
						|
		if err := db.GetEngine(ctx).
 | 
						|
			Join("INNER", "team_repo", "team_repo.team_id = team.id").
 | 
						|
			Where("team_repo.repo_id=?", issue.Repo.ID).
 | 
						|
			In("team.lower_name", mentionTeams).
 | 
						|
			Find(&teams); err != nil {
 | 
						|
			return nil, fmt.Errorf("find mentioned teams: %w", err)
 | 
						|
		}
 | 
						|
		if len(teams) != 0 {
 | 
						|
			checked := make([]int64, 0, len(teams))
 | 
						|
			unittype := unit.TypeIssues
 | 
						|
			if issue.IsPull {
 | 
						|
				unittype = unit.TypePullRequests
 | 
						|
			}
 | 
						|
			for _, team := range teams {
 | 
						|
				if team.HasAdminAccess() {
 | 
						|
					checked = append(checked, team.ID)
 | 
						|
					resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
 | 
						|
				if err != nil {
 | 
						|
					return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
 | 
						|
				}
 | 
						|
				if has {
 | 
						|
					checked = append(checked, team.ID)
 | 
						|
					resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if len(checked) != 0 {
 | 
						|
				teamusers := make([]*user_model.User, 0, 20)
 | 
						|
				if err := db.GetEngine(ctx).
 | 
						|
					Join("INNER", "team_user", "team_user.uid = `user`.id").
 | 
						|
					In("`team_user`.team_id", checked).
 | 
						|
					And("`user`.is_active = ?", true).
 | 
						|
					And("`user`.prohibit_login = ?", false).
 | 
						|
					Find(&teamusers); err != nil {
 | 
						|
					return nil, fmt.Errorf("get teams users: %w", err)
 | 
						|
				}
 | 
						|
				if len(teamusers) > 0 {
 | 
						|
					users = make([]*user_model.User, 0, len(teamusers))
 | 
						|
					for _, user := range teamusers {
 | 
						|
						if already, ok := resolved[user.LowerName]; !ok || !already {
 | 
						|
							users = append(users, user)
 | 
						|
							resolved[user.LowerName] = true
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Remove names already in the list to avoid querying the database if pending names remain
 | 
						|
	mentionUsers := make([]string, 0, len(resolved))
 | 
						|
	for name, already := range resolved {
 | 
						|
		if !already {
 | 
						|
			mentionUsers = append(mentionUsers, name)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if len(mentionUsers) == 0 {
 | 
						|
		return users, err
 | 
						|
	}
 | 
						|
 | 
						|
	if users == nil {
 | 
						|
		users = make([]*user_model.User, 0, len(mentionUsers))
 | 
						|
	}
 | 
						|
 | 
						|
	unchecked := make([]*user_model.User, 0, len(mentionUsers))
 | 
						|
	if err := db.GetEngine(ctx).
 | 
						|
		Where("`user`.is_active = ?", true).
 | 
						|
		And("`user`.prohibit_login = ?", false).
 | 
						|
		In("`user`.lower_name", mentionUsers).
 | 
						|
		Find(&unchecked); err != nil {
 | 
						|
		return nil, fmt.Errorf("find mentioned users: %w", err)
 | 
						|
	}
 | 
						|
	for _, user := range unchecked {
 | 
						|
		if already := resolved[user.LowerName]; already || user.IsOrganization() {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		// Normal users must have read access to the referencing issue
 | 
						|
		perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user)
 | 
						|
		if err != nil {
 | 
						|
			return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err)
 | 
						|
		}
 | 
						|
		if !perm.CanReadIssuesOrPulls(issue.IsPull) {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		users = append(users, user)
 | 
						|
	}
 | 
						|
 | 
						|
	return users, err
 | 
						|
}
 | 
						|
 | 
						|
// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID
 | 
						|
func UpdateIssuesMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error {
 | 
						|
	_, err := db.GetEngine(ctx).Table("issue").
 | 
						|
		Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
 | 
						|
		And("original_author_id = ?", originalAuthorID).
 | 
						|
		Update(map[string]any{
 | 
						|
			"poster_id":          posterID,
 | 
						|
			"original_author":    "",
 | 
						|
			"original_author_id": 0,
 | 
						|
		})
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
// UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID
 | 
						|
func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error {
 | 
						|
	_, err := db.GetEngine(ctx).Table("reaction").
 | 
						|
		Where("original_author_id = ?", originalAuthorID).
 | 
						|
		And(migratedIssueCond(gitServiceType)).
 | 
						|
		Update(map[string]any{
 | 
						|
			"user_id":            userID,
 | 
						|
			"original_author":    "",
 | 
						|
			"original_author_id": 0,
 | 
						|
		})
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
func GetOrphanedIssueRepoIDs(ctx context.Context) ([]int64, error) {
 | 
						|
	var repoIDs []int64
 | 
						|
	if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
 | 
						|
		Join("LEFT", "repository", "issue.repo_id=repository.id").
 | 
						|
		Where(builder.IsNull{"repository.id"}).
 | 
						|
		Find(&repoIDs); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return repoIDs, nil
 | 
						|
}
 |