mirror of
				https://gitcode.com/gitea/gitea.git
				synced 2025-10-25 12:26:40 +08:00 
			
		
		
		
	 38cf43d060
			
		
	
	38cf43d060
	
	
	
		
			
			This PR - [x] Move some functions from `issues.go` to `issue_stats.go` and `issue_label.go` - [x] Remove duplicated issue options `UserIssueStatsOption` to keep only one `IssuesOptions`
		
			
				
	
	
		
			816 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			816 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2023 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package issues
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"strings"
 | |
| 
 | |
| 	"code.gitea.io/gitea/models/db"
 | |
| 	"code.gitea.io/gitea/models/organization"
 | |
| 	"code.gitea.io/gitea/models/perm"
 | |
| 	access_model "code.gitea.io/gitea/models/perm/access"
 | |
| 	project_model "code.gitea.io/gitea/models/project"
 | |
| 	repo_model "code.gitea.io/gitea/models/repo"
 | |
| 	system_model "code.gitea.io/gitea/models/system"
 | |
| 	"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"
 | |
| 
 | |
| 	"xorm.io/builder"
 | |
| )
 | |
| 
 | |
| // UpdateIssueCols updates cols of issue
 | |
| func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
 | |
| 	if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) {
 | |
| 	// Reload the issue
 | |
| 	currentIssue, err := GetIssueByID(ctx, issue.ID)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Nothing should be performed if current status is same as target status
 | |
| 	if currentIssue.IsClosed == isClosed {
 | |
| 		if !issue.IsPull {
 | |
| 			return nil, ErrIssueWasClosed{
 | |
| 				ID: issue.ID,
 | |
| 			}
 | |
| 		}
 | |
| 		return nil, ErrPullWasClosed{
 | |
| 			ID: issue.ID,
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	issue.IsClosed = isClosed
 | |
| 	return doChangeIssueStatus(ctx, issue, doer, isMergePull)
 | |
| }
 | |
| 
 | |
| func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) {
 | |
| 	// Check for open dependencies
 | |
| 	if issue.IsClosed && 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}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if issue.IsClosed {
 | |
| 		issue.ClosedUnix = timeutil.TimeStampNow()
 | |
| 	} else {
 | |
| 		issue.ClosedUnix = 0
 | |
| 	}
 | |
| 
 | |
| 	if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// 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
 | |
| 	}
 | |
| 
 | |
| 	// New action comment
 | |
| 	cmtType := CommentTypeClose
 | |
| 	if !issue.IsClosed {
 | |
| 		cmtType = CommentTypeReopen
 | |
| 	} else if isMergePull {
 | |
| 		cmtType = CommentTypeMergePull
 | |
| 	}
 | |
| 
 | |
| 	return CreateComment(ctx, &CreateCommentOptions{
 | |
| 		Type:  cmtType,
 | |
| 		Doer:  doer,
 | |
| 		Repo:  issue.Repo,
 | |
| 		Issue: issue,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // ChangeIssueStatus changes issue status to open or closed.
 | |
| func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) {
 | |
| 	if err := issue.LoadRepo(ctx); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if err := issue.LoadPoster(ctx); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return changeIssueStatus(ctx, issue, doer, isClosed, false)
 | |
| }
 | |
| 
 | |
| // 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()
 | |
| 
 | |
| 	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(issue *Issue, doer *user_model.User, oldRef string) (err error) {
 | |
| 	ctx, committer, err := db.TxContext(db.DefaultContext)
 | |
| 	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(issueID int64, uuids []string) (err error) {
 | |
| 	ctx, committer, err := db.TxContext(db.DefaultContext)
 | |
| 	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(issue *Issue, doer *user_model.User, content string) (err error) {
 | |
| 	ctx, committer, err := db.TxContext(db.DefaultContext)
 | |
| 	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
 | |
| 
 | |
| 	if err = UpdateIssueCols(ctx, issue, "content"); err != nil {
 | |
| 		return fmt.Errorf("UpdateIssueCols: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	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 fmt.Errorf("no issue index provided")
 | |
| 	}
 | |
| 	if opts.Issue.ID > 0 {
 | |
| 		return fmt.Errorf("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 len(opts.Attachments) > 0 {
 | |
| 		attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
 | |
| 		}
 | |
| 
 | |
| 		for i := 0; i < len(attachments); i++ {
 | |
| 			attachments[i].IssueID = opts.Issue.ID
 | |
| 			if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
 | |
| 				return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, 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.
 | |
| func NewIssue(repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
 | |
| 	ctx, committer, err := db.TxContext(db.DefaultContext)
 | |
| 	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
 | |
| 
 | |
| 	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
 | |
| }
 | |
| 
 | |
| // UpdateIssueByAPI updates all allowed fields of given issue.
 | |
| // If the issue status is changed a statusChangeComment is returned
 | |
| // similarly if the title is changed the titleChanged bool is set to true
 | |
| func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) {
 | |
| 	ctx, committer, err := db.TxContext(db.DefaultContext)
 | |
| 	if err != nil {
 | |
| 		return nil, false, err
 | |
| 	}
 | |
| 	defer committer.Close()
 | |
| 
 | |
| 	if err := issue.LoadRepo(ctx); err != nil {
 | |
| 		return nil, false, fmt.Errorf("loadRepo: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Reload the issue
 | |
| 	currentIssue, err := GetIssueByID(ctx, issue.ID)
 | |
| 	if err != nil {
 | |
| 		return nil, false, err
 | |
| 	}
 | |
| 
 | |
| 	if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(
 | |
| 		"name", "content", "milestone_id", "priority",
 | |
| 		"deadline_unix", "updated_unix", "is_locked").
 | |
| 		Update(issue); err != nil {
 | |
| 		return nil, false, err
 | |
| 	}
 | |
| 
 | |
| 	titleChanged = currentIssue.Title != issue.Title
 | |
| 	if titleChanged {
 | |
| 		opts := &CreateCommentOptions{
 | |
| 			Type:     CommentTypeChangeTitle,
 | |
| 			Doer:     doer,
 | |
| 			Repo:     issue.Repo,
 | |
| 			Issue:    issue,
 | |
| 			OldTitle: currentIssue.Title,
 | |
| 			NewTitle: issue.Title,
 | |
| 		}
 | |
| 		_, err := CreateComment(ctx, opts)
 | |
| 		if err != nil {
 | |
| 			return nil, false, fmt.Errorf("createComment: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if currentIssue.IsClosed != issue.IsClosed {
 | |
| 		statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false)
 | |
| 		if err != nil {
 | |
| 			return nil, false, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if err := issue.AddCrossReferences(ctx, doer, true); err != nil {
 | |
| 		return nil, false, err
 | |
| 	}
 | |
| 	return statusChangeComment, titleChanged, committer.Commit()
 | |
| }
 | |
| 
 | |
| // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
 | |
| func UpdateIssueDeadline(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(db.DefaultContext)
 | |
| 	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()
 | |
| }
 | |
| 
 | |
| // DeleteInIssue delete records in beans with external key issue_id = ?
 | |
| func DeleteInIssue(ctx context.Context, issueID int64, beans ...interface{}) error {
 | |
| 	e := db.GetEngine(ctx)
 | |
| 	for _, bean := range beans {
 | |
| 		if _, err := e.In("issue_id", issueID).Delete(bean); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // 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)
 | |
| 	}
 | |
| 	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
 | |
| 	}
 | |
| 	if err = issue.LoadRepo(ctx); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	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.AccessMode >= perm.AccessModeAdmin {
 | |
| 					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
 | |
| 	}
 | |
| 
 | |
| 	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(gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error {
 | |
| 	_, err := db.GetEngine(db.DefaultContext).Table("issue").
 | |
| 		Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
 | |
| 		And("original_author_id = ?", originalAuthorID).
 | |
| 		Update(map[string]interface{}{
 | |
| 			"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(gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error {
 | |
| 	_, err := db.GetEngine(db.DefaultContext).Table("reaction").
 | |
| 		Where("original_author_id = ?", originalAuthorID).
 | |
| 		And(migratedIssueCond(gitServiceType)).
 | |
| 		Update(map[string]interface{}{
 | |
| 			"user_id":            userID,
 | |
| 			"original_author":    "",
 | |
| 			"original_author_id": 0,
 | |
| 		})
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // DeleteIssuesByRepoID deletes issues by repositories id
 | |
| func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
 | |
| 	deleteCond := builder.Select("id").From("issue").Where(builder.Eq{"issue.repo_id": repoID})
 | |
| 
 | |
| 	sess := db.GetEngine(ctx)
 | |
| 	// Delete content histories
 | |
| 	if _, err = sess.In("issue_id", deleteCond).
 | |
| 		Delete(&ContentHistory{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Delete comments and attachments
 | |
| 	if _, err = sess.In("issue_id", deleteCond).
 | |
| 		Delete(&Comment{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Dependencies for issues in this repository
 | |
| 	if _, err = sess.In("issue_id", deleteCond).
 | |
| 		Delete(&IssueDependency{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Delete dependencies for issues in other repositories
 | |
| 	if _, err = sess.In("dependency_id", deleteCond).
 | |
| 		Delete(&IssueDependency{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if _, err = sess.In("issue_id", deleteCond).
 | |
| 		Delete(&IssueUser{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if _, err = sess.In("issue_id", deleteCond).
 | |
| 		Delete(&Reaction{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if _, err = sess.In("issue_id", deleteCond).
 | |
| 		Delete(&IssueWatch{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if _, err = sess.In("issue_id", deleteCond).
 | |
| 		Delete(&Stopwatch{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if _, err = sess.In("issue_id", deleteCond).
 | |
| 		Delete(&TrackedTime{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if _, err = sess.In("issue_id", deleteCond).
 | |
| 		Delete(&project_model.ProjectIssue{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if _, err = sess.In("dependent_issue_id", deleteCond).
 | |
| 		Delete(&Comment{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var attachments []*repo_model.Attachment
 | |
| 	if err = sess.In("issue_id", deleteCond).
 | |
| 		Find(&attachments); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	for j := range attachments {
 | |
| 		attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
 | |
| 	}
 | |
| 
 | |
| 	if _, err = sess.In("issue_id", deleteCond).
 | |
| 		Delete(&repo_model.Attachment{}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if _, err = db.DeleteByBean(ctx, &Issue{RepoID: repoID}); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	return attachmentPaths, err
 | |
| }
 | |
| 
 | |
| // DeleteOrphanedIssues delete issues without a repo
 | |
| func DeleteOrphanedIssues(ctx context.Context) error {
 | |
| 	var attachmentPaths []string
 | |
| 	err := db.WithTx(ctx, func(ctx context.Context) error {
 | |
| 		var ids []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"}).GroupBy("issue.repo_id").
 | |
| 			Find(&ids); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		for i := range ids {
 | |
| 			paths, err := DeleteIssuesByRepoID(ctx, ids[i])
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			attachmentPaths = append(attachmentPaths, paths...)
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Remove issue attachment files.
 | |
| 	for i := range attachmentPaths {
 | |
| 		system_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i])
 | |
| 	}
 | |
| 	return nil
 | |
| }
 |