mirror of
https://github.com/go-gitea/gitea.git
synced 2025-08-08 01:34:05 +09:00
Some checks failed
Fix the bug when deleting orphaned issues attachments. The attachments maybe stored on other storages service rather than disk.
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
|
|
}
|