gitea源码

issue.go 12KB

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