mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
716 lines
22 KiB
716 lines
22 KiB
// 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 |
|
switch cmtType { |
|
case CommentTypeClose, CommentTypeMergePull: |
|
// only increase closed count |
|
if err := IncrRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil { |
|
return nil, err |
|
} |
|
case CommentTypeReopen: |
|
// only decrease closed count |
|
if err := DecrRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false, true); err != nil { |
|
return nil, err |
|
} |
|
default: |
|
return nil, fmt.Errorf("invalid comment type: %d", cmtType) |
|
} |
|
|
|
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 |
|
} |
|
|
|
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { |
|
return SetIssueAsClosed(ctx, issue, doer, false) |
|
}) |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { |
|
return setIssueAsReopen(ctx, issue, doer) |
|
}) |
|
} |
|
|
|
// 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) { |
|
return db.WithTx(ctx, func(ctx context.Context) error { |
|
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) |
|
} |
|
return issue.AddCrossReferences(ctx, doer, true) |
|
}) |
|
} |
|
|
|
// 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) { |
|
return db.WithTx(ctx, func(ctx context.Context) error { |
|
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 nil |
|
}) |
|
} |
|
|
|
// 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) { |
|
return db.WithTx(ctx, func(ctx context.Context) error { |
|
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) |
|
if err != nil { |
|
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) |
|
} |
|
for i := range attachments { |
|
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 nil |
|
}) |
|
} |
|
|
|
// 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) { |
|
return db.WithTx(ctx, func(ctx context.Context) error { |
|
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 nil |
|
}) |
|
} |
|
|
|
// NewIssueOptions represents the options of a new issue. |
|
type NewIssueOptions struct { |
|
Repo *repo_model.Repository |
|
Issue *Issue |
|
LabelIDs []int64 |
|
Attachments []string // In UUID format. |
|
} |
|
|
|
// 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 |
|
} |
|
} |
|
|
|
// Update repository issue total count |
|
if err := IncrRepoIssueNumbers(ctx, opts.Repo.ID, opts.Issue.IsPull, true); 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) { |
|
return db.WithTx(ctx, func(ctx context.Context) error { |
|
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) |
|
} |
|
return nil |
|
}) |
|
} |
|
|
|
// IncrRepoIssueNumbers increments repository issue numbers. |
|
func IncrRepoIssueNumbers(ctx context.Context, repoID int64, isPull, totalOrClosed bool) error { |
|
dbSession := db.GetEngine(ctx) |
|
var colName string |
|
if totalOrClosed { |
|
colName = util.Iif(isPull, "num_pulls", "num_issues") |
|
} else { |
|
colName = util.Iif(isPull, "num_closed_pulls", "num_closed_issues") |
|
} |
|
_, err := dbSession.Incr(colName).ID(repoID). |
|
NoAutoCondition().NoAutoTime(). |
|
Update(new(repo_model.Repository)) |
|
return err |
|
} |
|
|
|
// DecrRepoIssueNumbers decrements repository issue numbers. |
|
func DecrRepoIssueNumbers(ctx context.Context, repoID int64, isPull, includeTotal, includeClosed bool) error { |
|
if !includeTotal && !includeClosed { |
|
return fmt.Errorf("no numbers to decrease for repo id %d", repoID) |
|
} |
|
|
|
dbSession := db.GetEngine(ctx) |
|
if includeTotal { |
|
colName := util.Iif(isPull, "num_pulls", "num_issues") |
|
dbSession = dbSession.Decr(colName) |
|
} |
|
if includeClosed { |
|
closedColName := util.Iif(isPull, "num_closed_pulls", "num_closed_issues") |
|
dbSession = dbSession.Decr(closedColName) |
|
} |
|
_, err := dbSession.ID(repoID). |
|
NoAutoCondition().NoAutoTime(). |
|
Update(new(repo_model.Repository)) |
|
return err |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
return db.WithTx(ctx, func(ctx context.Context) error { |
|
// 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 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) |
|
} |
|
|
|
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 |
|
}
|
|
|