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.
379 lines
12 KiB
379 lines
12 KiB
// Copyright 2019 The Gitea Authors. All rights reserved. |
|
// SPDX-License-Identifier: MIT |
|
|
|
package issue |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
|
|
activities_model "code.gitea.io/gitea/models/activities" |
|
"code.gitea.io/gitea/models/db" |
|
issues_model "code.gitea.io/gitea/models/issues" |
|
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" |
|
user_model "code.gitea.io/gitea/models/user" |
|
"code.gitea.io/gitea/modules/container" |
|
"code.gitea.io/gitea/modules/git" |
|
"code.gitea.io/gitea/modules/gitrepo" |
|
"code.gitea.io/gitea/modules/log" |
|
"code.gitea.io/gitea/modules/storage" |
|
notify_service "code.gitea.io/gitea/services/notify" |
|
) |
|
|
|
// NewIssue creates new issue with labels for repository. |
|
func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64) error { |
|
if err := issue.LoadPoster(ctx); err != nil { |
|
return err |
|
} |
|
|
|
if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) { |
|
return user_model.ErrBlockedUser |
|
} |
|
|
|
if err := db.WithTx(ctx, func(ctx context.Context) error { |
|
if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil { |
|
return err |
|
} |
|
for _, assigneeID := range assigneeIDs { |
|
if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil { |
|
return err |
|
} |
|
} |
|
if projectID > 0 { |
|
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, projectID, 0); err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}); err != nil { |
|
return err |
|
} |
|
|
|
mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
notify_service.NewIssue(ctx, issue, mentions) |
|
if len(issue.Labels) > 0 { |
|
notify_service.IssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil) |
|
} |
|
if issue.Milestone != nil { |
|
notify_service.IssueChangeMilestone(ctx, issue.Poster, issue, 0) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// ChangeTitle changes the title of this issue, as the given user. |
|
func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, title string) error { |
|
oldTitle := issue.Title |
|
issue.Title = title |
|
|
|
if oldTitle == title { |
|
return nil |
|
} |
|
|
|
if err := issue.LoadRepo(ctx); err != nil { |
|
return err |
|
} |
|
|
|
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) { |
|
if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin { |
|
return user_model.ErrBlockedUser |
|
} |
|
} |
|
|
|
if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil { |
|
return err |
|
} |
|
|
|
var reviewNotifiers []*ReviewRequestNotifier |
|
if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { |
|
if err := issue.LoadPullRequest(ctx); err != nil { |
|
return err |
|
} |
|
|
|
var err error |
|
reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue.PullRequest) |
|
if err != nil { |
|
log.Error("PullRequestCodeOwnersReview: %v", err) |
|
} |
|
} |
|
|
|
notify_service.IssueChangeTitle(ctx, doer, issue, oldTitle) |
|
ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifiers) |
|
|
|
return nil |
|
} |
|
|
|
// ChangeTimeEstimate changes the time estimate of this issue, as the given user. |
|
func ChangeTimeEstimate(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, timeEstimate int64) (err error) { |
|
issue.TimeEstimate = timeEstimate |
|
|
|
return issues_model.ChangeIssueTimeEstimate(ctx, issue, doer, timeEstimate) |
|
} |
|
|
|
// ChangeIssueRef changes the branch of this issue, as the given user. |
|
func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, ref string) error { |
|
oldRef := issue.Ref |
|
issue.Ref = ref |
|
|
|
if err := issues_model.ChangeIssueRef(ctx, issue, doer, oldRef); err != nil { |
|
return err |
|
} |
|
|
|
notify_service.IssueChangeRef(ctx, doer, issue, oldRef) |
|
|
|
return nil |
|
} |
|
|
|
// UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s) |
|
// Deleting is done the GitHub way (quote from their api documentation): |
|
// https://developer.github.com/v3/issues/#edit-an-issue |
|
// "assignees" (array): Logins for Users to assign to this issue. |
|
// Pass one or more user logins to replace the set of assignees on this Issue. |
|
// Send an empty array ([]) to clear all assignees from the Issue. |
|
func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) { |
|
uniqueAssignees := container.SetOf(multipleAssignees...) |
|
|
|
// Keep the old assignee thingy for compatibility reasons |
|
if oneAssignee != "" { |
|
uniqueAssignees.Add(oneAssignee) |
|
} |
|
|
|
// Loop through all assignees to add them |
|
allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees)) |
|
for _, assigneeName := range uniqueAssignees.Values() { |
|
assignee, err := user_model.GetUserByName(ctx, assigneeName) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) { |
|
return user_model.ErrBlockedUser |
|
} |
|
|
|
allNewAssignees = append(allNewAssignees, assignee) |
|
} |
|
|
|
// Delete all old assignees not passed |
|
if err = DeleteNotPassedAssignee(ctx, issue, doer, allNewAssignees); err != nil { |
|
return err |
|
} |
|
|
|
// Add all new assignees |
|
// Update the assignee. The function will check if the user exists, is already |
|
// assigned (which he shouldn't as we deleted all assignees before) and |
|
// has access to the repo. |
|
for _, assignee := range allNewAssignees { |
|
// Extra method to prevent double adding (which would result in removing) |
|
_, err = AddAssigneeIfNotAssigned(ctx, issue, doer, assignee.ID, true) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
|
|
return err |
|
} |
|
|
|
// DeleteIssue deletes an issue |
|
func DeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) error { |
|
// load issue before deleting it |
|
if err := issue.LoadAttributes(ctx); err != nil { |
|
return err |
|
} |
|
if err := issue.LoadPullRequest(ctx); err != nil { |
|
return err |
|
} |
|
|
|
// delete entries in database |
|
attachmentPaths, err := deleteIssue(ctx, issue) |
|
if err != nil { |
|
return err |
|
} |
|
for _, attachmentPath := range attachmentPaths { |
|
system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachmentPath) |
|
} |
|
|
|
// delete pull request related git data |
|
if issue.IsPull { |
|
if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil { |
|
return err |
|
} |
|
if err := gitrepo.RemoveRef(ctx, issue.PullRequest.BaseRepo, issue.PullRequest.GetGitHeadRefName()); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
notify_service.DeleteIssue(ctx, doer, issue) |
|
|
|
return nil |
|
} |
|
|
|
// AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue. |
|
// Also checks for access of assigned user |
|
func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64, notify bool) (comment *issues_model.Comment, err error) { |
|
assignee, err := user_model.GetUserByID(ctx, assigneeID) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// Check if the user is already assigned |
|
isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assignee) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if isAssigned { |
|
// nothing to to |
|
return nil, nil |
|
} |
|
|
|
valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if !valid { |
|
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name} |
|
} |
|
|
|
if notify { |
|
_, comment, err = ToggleAssigneeWithNotify(ctx, issue, doer, assigneeID) |
|
return comment, err |
|
} |
|
_, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID) |
|
return comment, err |
|
} |
|
|
|
// GetRefEndNamesAndURLs retrieves the ref end names (e.g. refs/heads/branch-name -> branch-name) |
|
// and their respective URLs. |
|
func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[int64]string, map[int64]string) { |
|
issueRefEndNames := make(map[int64]string, len(issues)) |
|
issueRefURLs := make(map[int64]string, len(issues)) |
|
for _, issue := range issues { |
|
if issue.Ref != "" { |
|
ref := git.RefName(issue.Ref) |
|
issueRefEndNames[issue.ID] = ref.ShortName() |
|
issueRefURLs[issue.ID] = repoLink + "/src/" + ref.RefWebLinkPath() |
|
} |
|
} |
|
return issueRefEndNames, issueRefURLs |
|
} |
|
|
|
// deleteIssue deletes the issue |
|
func deleteIssue(ctx context.Context, issue *issues_model.Issue) ([]string, error) { |
|
return db.WithTx2(ctx, func(ctx context.Context) ([]string, error) { |
|
if _, err := db.GetEngine(ctx).ID(issue.ID).NoAutoCondition().Delete(issue); err != nil { |
|
return nil, err |
|
} |
|
|
|
if err := issues_model.DecrRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true, issue.IsClosed); err != nil { |
|
return nil, err |
|
} |
|
|
|
if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { |
|
return nil, fmt.Errorf("error updating counters for milestone id %d: %w", |
|
issue.MilestoneID, err) |
|
} |
|
|
|
if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID, issue.Index); err != nil { |
|
return nil, err |
|
} |
|
|
|
// find attachments related to this issue and remove them |
|
if err := issue.LoadAttachments(ctx); err != nil { |
|
return nil, err |
|
} |
|
|
|
var attachmentPaths []string |
|
for i := range issue.Attachments { |
|
attachmentPaths = append(attachmentPaths, issue.Attachments[i].RelativePath()) |
|
} |
|
|
|
// delete all database data still assigned to this issue |
|
if err := db.DeleteBeans(ctx, |
|
&issues_model.ContentHistory{IssueID: issue.ID}, |
|
&issues_model.Comment{IssueID: issue.ID}, |
|
&issues_model.IssueLabel{IssueID: issue.ID}, |
|
&issues_model.IssueDependency{IssueID: issue.ID}, |
|
&issues_model.IssueAssignees{IssueID: issue.ID}, |
|
&issues_model.IssueUser{IssueID: issue.ID}, |
|
&activities_model.Notification{IssueID: issue.ID}, |
|
&issues_model.Reaction{IssueID: issue.ID}, |
|
&issues_model.IssueWatch{IssueID: issue.ID}, |
|
&issues_model.Stopwatch{IssueID: issue.ID}, |
|
&issues_model.TrackedTime{IssueID: issue.ID}, |
|
&project_model.ProjectIssue{IssueID: issue.ID}, |
|
&repo_model.Attachment{IssueID: issue.ID}, |
|
&issues_model.PullRequest{IssueID: issue.ID}, |
|
&issues_model.Comment{RefIssueID: issue.ID}, |
|
&issues_model.IssueDependency{DependencyID: issue.ID}, |
|
&issues_model.Comment{DependentIssueID: issue.ID}, |
|
&issues_model.IssuePin{IssueID: issue.ID}, |
|
); err != nil { |
|
return nil, err |
|
} |
|
|
|
return attachmentPaths, nil |
|
}) |
|
} |
|
|
|
// DeleteOrphanedIssues delete issues without a repo |
|
func DeleteOrphanedIssues(ctx context.Context) error { |
|
var attachmentPaths []string |
|
err := db.WithTx(ctx, func(ctx context.Context) error { |
|
repoIDs, err := issues_model.GetOrphanedIssueRepoIDs(ctx) |
|
if err != nil { |
|
return err |
|
} |
|
for i := range repoIDs { |
|
paths, err := DeleteIssuesByRepoID(ctx, repoIDs[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.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachmentPaths[i]) |
|
} |
|
return nil |
|
} |
|
|
|
// DeleteIssuesByRepoID deletes issues by repositories id |
|
func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) { |
|
for { |
|
issues := make([]*issues_model.Issue, 0, db.DefaultMaxInSize) |
|
if err := db.GetEngine(ctx). |
|
Where("repo_id = ?", repoID). |
|
OrderBy("id"). |
|
Limit(db.DefaultMaxInSize). |
|
Find(&issues); err != nil { |
|
return nil, err |
|
} |
|
|
|
if len(issues) == 0 { |
|
break |
|
} |
|
|
|
for _, issue := range issues { |
|
issueAttachPaths, err := deleteIssue(ctx, issue) |
|
if err != nil { |
|
return nil, fmt.Errorf("deleteIssue [issue_id: %d]: %w", issue.ID, err) |
|
} |
|
|
|
attachmentPaths = append(attachmentPaths, issueAttachPaths...) |
|
} |
|
} |
|
|
|
return attachmentPaths, err |
|
}
|
|
|