code.gitea.io/gitea@v1.22.3/services/issue/issue.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package issue 5 6 import ( 7 "context" 8 "fmt" 9 10 activities_model "code.gitea.io/gitea/models/activities" 11 "code.gitea.io/gitea/models/db" 12 issues_model "code.gitea.io/gitea/models/issues" 13 access_model "code.gitea.io/gitea/models/perm/access" 14 project_model "code.gitea.io/gitea/models/project" 15 repo_model "code.gitea.io/gitea/models/repo" 16 system_model "code.gitea.io/gitea/models/system" 17 user_model "code.gitea.io/gitea/models/user" 18 "code.gitea.io/gitea/modules/container" 19 "code.gitea.io/gitea/modules/git" 20 "code.gitea.io/gitea/modules/log" 21 "code.gitea.io/gitea/modules/storage" 22 notify_service "code.gitea.io/gitea/services/notify" 23 ) 24 25 // NewIssue creates new issue with labels for repository. 26 func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64) error { 27 if err := issue.LoadPoster(ctx); err != nil { 28 return err 29 } 30 31 if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) { 32 return user_model.ErrBlockedUser 33 } 34 35 if err := db.WithTx(ctx, func(ctx context.Context) error { 36 if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil { 37 return err 38 } 39 for _, assigneeID := range assigneeIDs { 40 if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil { 41 return err 42 } 43 } 44 if projectID > 0 { 45 if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, projectID, 0); err != nil { 46 return err 47 } 48 } 49 return nil 50 }); err != nil { 51 return err 52 } 53 54 mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content) 55 if err != nil { 56 return err 57 } 58 59 notify_service.NewIssue(ctx, issue, mentions) 60 if len(issue.Labels) > 0 { 61 notify_service.IssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil) 62 } 63 if issue.Milestone != nil { 64 notify_service.IssueChangeMilestone(ctx, issue.Poster, issue, 0) 65 } 66 67 return nil 68 } 69 70 // ChangeTitle changes the title of this issue, as the given user. 71 func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, title string) error { 72 oldTitle := issue.Title 73 issue.Title = title 74 75 if oldTitle == title { 76 return nil 77 } 78 79 if err := issue.LoadRepo(ctx); err != nil { 80 return err 81 } 82 83 if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) { 84 if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin { 85 return user_model.ErrBlockedUser 86 } 87 } 88 89 if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil { 90 return err 91 } 92 93 var reviewNotifiers []*ReviewRequestNotifier 94 if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { 95 var err error 96 reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest) 97 if err != nil { 98 log.Error("PullRequestCodeOwnersReview: %v", err) 99 } 100 } 101 102 notify_service.IssueChangeTitle(ctx, doer, issue, oldTitle) 103 ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifiers) 104 105 return nil 106 } 107 108 // ChangeIssueRef changes the branch of this issue, as the given user. 109 func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, ref string) error { 110 oldRef := issue.Ref 111 issue.Ref = ref 112 113 if err := issues_model.ChangeIssueRef(ctx, issue, doer, oldRef); err != nil { 114 return err 115 } 116 117 notify_service.IssueChangeRef(ctx, doer, issue, oldRef) 118 119 return nil 120 } 121 122 // UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s) 123 // Deleting is done the GitHub way (quote from their api documentation): 124 // https://developer.github.com/v3/issues/#edit-an-issue 125 // "assignees" (array): Logins for Users to assign to this issue. 126 // Pass one or more user logins to replace the set of assignees on this Issue. 127 // Send an empty array ([]) to clear all assignees from the Issue. 128 func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) { 129 uniqueAssignees := container.SetOf(multipleAssignees...) 130 131 // Keep the old assignee thingy for compatibility reasons 132 if oneAssignee != "" { 133 uniqueAssignees.Add(oneAssignee) 134 } 135 136 // Loop through all assignees to add them 137 allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees)) 138 for _, assigneeName := range uniqueAssignees.Values() { 139 assignee, err := user_model.GetUserByName(ctx, assigneeName) 140 if err != nil { 141 return err 142 } 143 144 if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) { 145 return user_model.ErrBlockedUser 146 } 147 148 allNewAssignees = append(allNewAssignees, assignee) 149 } 150 151 // Delete all old assignees not passed 152 if err = DeleteNotPassedAssignee(ctx, issue, doer, allNewAssignees); err != nil { 153 return err 154 } 155 156 // Add all new assignees 157 // Update the assignee. The function will check if the user exists, is already 158 // assigned (which he shouldn't as we deleted all assignees before) and 159 // has access to the repo. 160 for _, assignee := range allNewAssignees { 161 // Extra method to prevent double adding (which would result in removing) 162 _, err = AddAssigneeIfNotAssigned(ctx, issue, doer, assignee.ID, true) 163 if err != nil { 164 return err 165 } 166 } 167 168 return err 169 } 170 171 // DeleteIssue deletes an issue 172 func DeleteIssue(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue) error { 173 // load issue before deleting it 174 if err := issue.LoadAttributes(ctx); err != nil { 175 return err 176 } 177 if err := issue.LoadPullRequest(ctx); err != nil { 178 return err 179 } 180 181 // delete entries in database 182 if err := deleteIssue(ctx, issue); err != nil { 183 return err 184 } 185 186 // delete pull request related git data 187 if issue.IsPull && gitRepo != nil { 188 if err := gitRepo.RemoveReference(fmt.Sprintf("%s%d/head", git.PullPrefix, issue.PullRequest.Index)); err != nil { 189 return err 190 } 191 } 192 193 // If the Issue is pinned, we should unpin it before deletion to avoid problems with other pinned Issues 194 if issue.IsPinned() { 195 if err := issue.Unpin(ctx, doer); err != nil { 196 return err 197 } 198 } 199 200 notify_service.DeleteIssue(ctx, doer, issue) 201 202 return nil 203 } 204 205 // AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue. 206 // Also checks for access of assigned user 207 func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64, notify bool) (comment *issues_model.Comment, err error) { 208 assignee, err := user_model.GetUserByID(ctx, assigneeID) 209 if err != nil { 210 return nil, err 211 } 212 213 // Check if the user is already assigned 214 isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assignee) 215 if err != nil { 216 return nil, err 217 } 218 if isAssigned { 219 // nothing to to 220 return nil, nil 221 } 222 223 valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull) 224 if err != nil { 225 return nil, err 226 } 227 if !valid { 228 return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name} 229 } 230 231 if notify { 232 _, comment, err = ToggleAssigneeWithNotify(ctx, issue, doer, assigneeID) 233 return comment, err 234 } 235 _, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID) 236 return comment, err 237 } 238 239 // GetRefEndNamesAndURLs retrieves the ref end names (e.g. refs/heads/branch-name -> branch-name) 240 // and their respective URLs. 241 func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[int64]string, map[int64]string) { 242 issueRefEndNames := make(map[int64]string, len(issues)) 243 issueRefURLs := make(map[int64]string, len(issues)) 244 for _, issue := range issues { 245 if issue.Ref != "" { 246 issueRefEndNames[issue.ID] = git.RefName(issue.Ref).ShortName() 247 issueRefURLs[issue.ID] = git.RefURL(repoLink, issue.Ref) 248 } 249 } 250 return issueRefEndNames, issueRefURLs 251 } 252 253 // deleteIssue deletes the issue 254 func deleteIssue(ctx context.Context, issue *issues_model.Issue) error { 255 ctx, committer, err := db.TxContext(ctx) 256 if err != nil { 257 return err 258 } 259 defer committer.Close() 260 261 e := db.GetEngine(ctx) 262 if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil { 263 return err 264 } 265 266 // update the total issue numbers 267 if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil { 268 return err 269 } 270 // if the issue is closed, update the closed issue numbers 271 if issue.IsClosed { 272 if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil { 273 return err 274 } 275 } 276 277 if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { 278 return fmt.Errorf("error updating counters for milestone id %d: %w", 279 issue.MilestoneID, err) 280 } 281 282 if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID, issue.Index); err != nil { 283 return err 284 } 285 286 // find attachments related to this issue and remove them 287 if err := issue.LoadAttributes(ctx); err != nil { 288 return err 289 } 290 291 for i := range issue.Attachments { 292 system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath()) 293 } 294 295 // delete all database data still assigned to this issue 296 if err := db.DeleteBeans(ctx, 297 &issues_model.ContentHistory{IssueID: issue.ID}, 298 &issues_model.Comment{IssueID: issue.ID}, 299 &issues_model.IssueLabel{IssueID: issue.ID}, 300 &issues_model.IssueDependency{IssueID: issue.ID}, 301 &issues_model.IssueAssignees{IssueID: issue.ID}, 302 &issues_model.IssueUser{IssueID: issue.ID}, 303 &activities_model.Notification{IssueID: issue.ID}, 304 &issues_model.Reaction{IssueID: issue.ID}, 305 &issues_model.IssueWatch{IssueID: issue.ID}, 306 &issues_model.Stopwatch{IssueID: issue.ID}, 307 &issues_model.TrackedTime{IssueID: issue.ID}, 308 &project_model.ProjectIssue{IssueID: issue.ID}, 309 &repo_model.Attachment{IssueID: issue.ID}, 310 &issues_model.PullRequest{IssueID: issue.ID}, 311 &issues_model.Comment{RefIssueID: issue.ID}, 312 &issues_model.IssueDependency{DependencyID: issue.ID}, 313 &issues_model.Comment{DependentIssueID: issue.ID}, 314 ); err != nil { 315 return err 316 } 317 318 return committer.Commit() 319 }