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.

issue_update.go 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "fmt"
  7. "strings"
  8. "code.gitea.io/gitea/models/db"
  9. "code.gitea.io/gitea/models/organization"
  10. "code.gitea.io/gitea/models/perm"
  11. access_model "code.gitea.io/gitea/models/perm/access"
  12. project_model "code.gitea.io/gitea/models/project"
  13. repo_model "code.gitea.io/gitea/models/repo"
  14. system_model "code.gitea.io/gitea/models/system"
  15. "code.gitea.io/gitea/models/unit"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/references"
  19. api "code.gitea.io/gitea/modules/structs"
  20. "code.gitea.io/gitea/modules/timeutil"
  21. "xorm.io/builder"
  22. )
  23. // UpdateIssueCols updates cols of issue
  24. func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
  25. if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil {
  26. return err
  27. }
  28. return nil
  29. }
  30. func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) {
  31. // Reload the issue
  32. currentIssue, err := GetIssueByID(ctx, issue.ID)
  33. if err != nil {
  34. return nil, err
  35. }
  36. // Nothing should be performed if current status is same as target status
  37. if currentIssue.IsClosed == isClosed {
  38. if !issue.IsPull {
  39. return nil, ErrIssueWasClosed{
  40. ID: issue.ID,
  41. }
  42. }
  43. return nil, ErrPullWasClosed{
  44. ID: issue.ID,
  45. }
  46. }
  47. issue.IsClosed = isClosed
  48. return doChangeIssueStatus(ctx, issue, doer, isMergePull)
  49. }
  50. func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) {
  51. // Check for open dependencies
  52. if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) {
  53. // 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
  54. noDeps, err := IssueNoDependenciesLeft(ctx, issue)
  55. if err != nil {
  56. return nil, err
  57. }
  58. if !noDeps {
  59. return nil, ErrDependenciesLeft{issue.ID}
  60. }
  61. }
  62. if issue.IsClosed {
  63. issue.ClosedUnix = timeutil.TimeStampNow()
  64. } else {
  65. issue.ClosedUnix = 0
  66. }
  67. if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil {
  68. return nil, err
  69. }
  70. // Update issue count of labels
  71. if err := issue.LoadLabels(ctx); err != nil {
  72. return nil, err
  73. }
  74. for idx := range issue.Labels {
  75. if err := updateLabelCols(ctx, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil {
  76. return nil, err
  77. }
  78. }
  79. // Update issue count of milestone
  80. if issue.MilestoneID > 0 {
  81. if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
  82. return nil, err
  83. }
  84. }
  85. // update repository's issue closed number
  86. if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil {
  87. return nil, err
  88. }
  89. // New action comment
  90. cmtType := CommentTypeClose
  91. if !issue.IsClosed {
  92. cmtType = CommentTypeReopen
  93. } else if isMergePull {
  94. cmtType = CommentTypeMergePull
  95. }
  96. return CreateComment(ctx, &CreateCommentOptions{
  97. Type: cmtType,
  98. Doer: doer,
  99. Repo: issue.Repo,
  100. Issue: issue,
  101. })
  102. }
  103. // ChangeIssueStatus changes issue status to open or closed.
  104. func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) {
  105. if err := issue.LoadRepo(ctx); err != nil {
  106. return nil, err
  107. }
  108. if err := issue.LoadPoster(ctx); err != nil {
  109. return nil, err
  110. }
  111. return changeIssueStatus(ctx, issue, doer, isClosed, false)
  112. }
  113. // ChangeIssueTitle changes the title of this issue, as the given user.
  114. func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, oldTitle string) (err error) {
  115. ctx, committer, err := db.TxContext(ctx)
  116. if err != nil {
  117. return err
  118. }
  119. defer committer.Close()
  120. if err = UpdateIssueCols(ctx, issue, "name"); err != nil {
  121. return fmt.Errorf("updateIssueCols: %w", err)
  122. }
  123. if err = issue.LoadRepo(ctx); err != nil {
  124. return fmt.Errorf("loadRepo: %w", err)
  125. }
  126. opts := &CreateCommentOptions{
  127. Type: CommentTypeChangeTitle,
  128. Doer: doer,
  129. Repo: issue.Repo,
  130. Issue: issue,
  131. OldTitle: oldTitle,
  132. NewTitle: issue.Title,
  133. }
  134. if _, err = CreateComment(ctx, opts); err != nil {
  135. return fmt.Errorf("createComment: %w", err)
  136. }
  137. if err = issue.AddCrossReferences(ctx, doer, true); err != nil {
  138. return err
  139. }
  140. return committer.Commit()
  141. }
  142. // ChangeIssueRef changes the branch of this issue, as the given user.
  143. func ChangeIssueRef(ctx context.Context, issue *Issue, doer *user_model.User, oldRef string) (err error) {
  144. ctx, committer, err := db.TxContext(ctx)
  145. if err != nil {
  146. return err
  147. }
  148. defer committer.Close()
  149. if err = UpdateIssueCols(ctx, issue, "ref"); err != nil {
  150. return fmt.Errorf("updateIssueCols: %w", err)
  151. }
  152. if err = issue.LoadRepo(ctx); err != nil {
  153. return fmt.Errorf("loadRepo: %w", err)
  154. }
  155. oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix)
  156. newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix)
  157. opts := &CreateCommentOptions{
  158. Type: CommentTypeChangeIssueRef,
  159. Doer: doer,
  160. Repo: issue.Repo,
  161. Issue: issue,
  162. OldRef: oldRefFriendly,
  163. NewRef: newRefFriendly,
  164. }
  165. if _, err = CreateComment(ctx, opts); err != nil {
  166. return fmt.Errorf("createComment: %w", err)
  167. }
  168. return committer.Commit()
  169. }
  170. // AddDeletePRBranchComment adds delete branch comment for pull request issue
  171. func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issueID int64, branchName string) error {
  172. issue, err := GetIssueByID(ctx, issueID)
  173. if err != nil {
  174. return err
  175. }
  176. opts := &CreateCommentOptions{
  177. Type: CommentTypeDeleteBranch,
  178. Doer: doer,
  179. Repo: repo,
  180. Issue: issue,
  181. OldRef: branchName,
  182. }
  183. _, err = CreateComment(ctx, opts)
  184. return err
  185. }
  186. // UpdateIssueAttachments update attachments by UUIDs for the issue
  187. func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) {
  188. ctx, committer, err := db.TxContext(ctx)
  189. if err != nil {
  190. return err
  191. }
  192. defer committer.Close()
  193. attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
  194. if err != nil {
  195. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
  196. }
  197. for i := 0; i < len(attachments); i++ {
  198. attachments[i].IssueID = issueID
  199. if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
  200. return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
  201. }
  202. }
  203. return committer.Commit()
  204. }
  205. // ChangeIssueContent changes issue content, as the given user.
  206. func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string) (err error) {
  207. ctx, committer, err := db.TxContext(ctx)
  208. if err != nil {
  209. return err
  210. }
  211. defer committer.Close()
  212. hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0)
  213. if err != nil {
  214. return fmt.Errorf("HasIssueContentHistory: %w", err)
  215. }
  216. if !hasContentHistory {
  217. if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0,
  218. issue.CreatedUnix, issue.Content, true); err != nil {
  219. return fmt.Errorf("SaveIssueContentHistory: %w", err)
  220. }
  221. }
  222. issue.Content = content
  223. if err = UpdateIssueCols(ctx, issue, "content"); err != nil {
  224. return fmt.Errorf("UpdateIssueCols: %w", err)
  225. }
  226. if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0,
  227. timeutil.TimeStampNow(), issue.Content, false); err != nil {
  228. return fmt.Errorf("SaveIssueContentHistory: %w", err)
  229. }
  230. if err = issue.AddCrossReferences(ctx, doer, true); err != nil {
  231. return fmt.Errorf("addCrossReferences: %w", err)
  232. }
  233. return committer.Commit()
  234. }
  235. // NewIssueOptions represents the options of a new issue.
  236. type NewIssueOptions struct {
  237. Repo *repo_model.Repository
  238. Issue *Issue
  239. LabelIDs []int64
  240. Attachments []string // In UUID format.
  241. IsPull bool
  242. }
  243. // NewIssueWithIndex creates issue with given index
  244. func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) {
  245. e := db.GetEngine(ctx)
  246. opts.Issue.Title = strings.TrimSpace(opts.Issue.Title)
  247. if opts.Issue.MilestoneID > 0 {
  248. milestone, err := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID)
  249. if err != nil && !IsErrMilestoneNotExist(err) {
  250. return fmt.Errorf("getMilestoneByID: %w", err)
  251. }
  252. // Assume milestone is invalid and drop silently.
  253. opts.Issue.MilestoneID = 0
  254. if milestone != nil {
  255. opts.Issue.MilestoneID = milestone.ID
  256. opts.Issue.Milestone = milestone
  257. }
  258. }
  259. if opts.Issue.Index <= 0 {
  260. return fmt.Errorf("no issue index provided")
  261. }
  262. if opts.Issue.ID > 0 {
  263. return fmt.Errorf("issue exist")
  264. }
  265. if _, err := e.Insert(opts.Issue); err != nil {
  266. return err
  267. }
  268. if opts.Issue.MilestoneID > 0 {
  269. if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil {
  270. return err
  271. }
  272. opts := &CreateCommentOptions{
  273. Type: CommentTypeMilestone,
  274. Doer: doer,
  275. Repo: opts.Repo,
  276. Issue: opts.Issue,
  277. OldMilestoneID: 0,
  278. MilestoneID: opts.Issue.MilestoneID,
  279. }
  280. if _, err = CreateComment(ctx, opts); err != nil {
  281. return err
  282. }
  283. }
  284. if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil {
  285. return err
  286. }
  287. if len(opts.LabelIDs) > 0 {
  288. // During the session, SQLite3 driver cannot handle retrieve objects after update something.
  289. // So we have to get all needed labels first.
  290. labels := make([]*Label, 0, len(opts.LabelIDs))
  291. if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil {
  292. return fmt.Errorf("find all labels [label_ids: %v]: %w", opts.LabelIDs, err)
  293. }
  294. if err = opts.Issue.LoadPoster(ctx); err != nil {
  295. return err
  296. }
  297. for _, label := range labels {
  298. // Silently drop invalid labels.
  299. if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID {
  300. continue
  301. }
  302. if err = newIssueLabel(ctx, opts.Issue, label, opts.Issue.Poster); err != nil {
  303. return fmt.Errorf("addLabel [id: %d]: %w", label.ID, err)
  304. }
  305. }
  306. }
  307. if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil {
  308. return err
  309. }
  310. if len(opts.Attachments) > 0 {
  311. attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
  312. if err != nil {
  313. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
  314. }
  315. for i := 0; i < len(attachments); i++ {
  316. attachments[i].IssueID = opts.Issue.ID
  317. if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
  318. return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
  319. }
  320. }
  321. }
  322. if err = opts.Issue.LoadAttributes(ctx); err != nil {
  323. return err
  324. }
  325. return opts.Issue.AddCrossReferences(ctx, doer, false)
  326. }
  327. // NewIssue creates new issue with labels for repository.
  328. func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
  329. ctx, committer, err := db.TxContext(ctx)
  330. if err != nil {
  331. return err
  332. }
  333. defer committer.Close()
  334. idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID)
  335. if err != nil {
  336. return fmt.Errorf("generate issue index failed: %w", err)
  337. }
  338. issue.Index = idx
  339. if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
  340. Repo: repo,
  341. Issue: issue,
  342. LabelIDs: labelIDs,
  343. Attachments: uuids,
  344. }); err != nil {
  345. if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
  346. return err
  347. }
  348. return fmt.Errorf("newIssue: %w", err)
  349. }
  350. if err = committer.Commit(); err != nil {
  351. return fmt.Errorf("Commit: %w", err)
  352. }
  353. return nil
  354. }
  355. // UpdateIssueMentions updates issue-user relations for mentioned users.
  356. func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error {
  357. if len(mentions) == 0 {
  358. return nil
  359. }
  360. ids := make([]int64, len(mentions))
  361. for i, u := range mentions {
  362. ids[i] = u.ID
  363. }
  364. if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil {
  365. return fmt.Errorf("UpdateIssueUsersByMentions: %w", err)
  366. }
  367. return nil
  368. }
  369. // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
  370. func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) {
  371. // if the deadline hasn't changed do nothing
  372. if issue.DeadlineUnix == deadlineUnix {
  373. return nil
  374. }
  375. ctx, committer, err := db.TxContext(ctx)
  376. if err != nil {
  377. return err
  378. }
  379. defer committer.Close()
  380. // Update the deadline
  381. if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil {
  382. return err
  383. }
  384. // Make the comment
  385. if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil {
  386. return fmt.Errorf("createRemovedDueDateComment: %w", err)
  387. }
  388. return committer.Commit()
  389. }
  390. // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database.
  391. func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) {
  392. rawMentions := references.FindAllMentionsMarkdown(content)
  393. mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions)
  394. if err != nil {
  395. return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
  396. }
  397. notBlocked := make([]*user_model.User, 0, len(mentions))
  398. for _, user := range mentions {
  399. if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
  400. notBlocked = append(notBlocked, user)
  401. }
  402. }
  403. mentions = notBlocked
  404. if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
  405. return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
  406. }
  407. return mentions, err
  408. }
  409. // ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that
  410. // don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
  411. func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) {
  412. if len(mentions) == 0 {
  413. return nil, nil
  414. }
  415. if err = issue.LoadRepo(ctx); err != nil {
  416. return nil, err
  417. }
  418. resolved := make(map[string]bool, 10)
  419. var mentionTeams []string
  420. if err := issue.Repo.LoadOwner(ctx); err != nil {
  421. return nil, err
  422. }
  423. repoOwnerIsOrg := issue.Repo.Owner.IsOrganization()
  424. if repoOwnerIsOrg {
  425. mentionTeams = make([]string, 0, 5)
  426. }
  427. resolved[doer.LowerName] = true
  428. for _, name := range mentions {
  429. name := strings.ToLower(name)
  430. if _, ok := resolved[name]; ok {
  431. continue
  432. }
  433. if repoOwnerIsOrg && strings.Contains(name, "/") {
  434. names := strings.Split(name, "/")
  435. if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName {
  436. continue
  437. }
  438. mentionTeams = append(mentionTeams, names[1])
  439. resolved[name] = true
  440. } else {
  441. resolved[name] = false
  442. }
  443. }
  444. if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 {
  445. teams := make([]*organization.Team, 0, len(mentionTeams))
  446. if err := db.GetEngine(ctx).
  447. Join("INNER", "team_repo", "team_repo.team_id = team.id").
  448. Where("team_repo.repo_id=?", issue.Repo.ID).
  449. In("team.lower_name", mentionTeams).
  450. Find(&teams); err != nil {
  451. return nil, fmt.Errorf("find mentioned teams: %w", err)
  452. }
  453. if len(teams) != 0 {
  454. checked := make([]int64, 0, len(teams))
  455. unittype := unit.TypeIssues
  456. if issue.IsPull {
  457. unittype = unit.TypePullRequests
  458. }
  459. for _, team := range teams {
  460. if team.AccessMode >= perm.AccessModeAdmin {
  461. checked = append(checked, team.ID)
  462. resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
  463. continue
  464. }
  465. has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
  466. if err != nil {
  467. return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
  468. }
  469. if has {
  470. checked = append(checked, team.ID)
  471. resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
  472. }
  473. }
  474. if len(checked) != 0 {
  475. teamusers := make([]*user_model.User, 0, 20)
  476. if err := db.GetEngine(ctx).
  477. Join("INNER", "team_user", "team_user.uid = `user`.id").
  478. In("`team_user`.team_id", checked).
  479. And("`user`.is_active = ?", true).
  480. And("`user`.prohibit_login = ?", false).
  481. Find(&teamusers); err != nil {
  482. return nil, fmt.Errorf("get teams users: %w", err)
  483. }
  484. if len(teamusers) > 0 {
  485. users = make([]*user_model.User, 0, len(teamusers))
  486. for _, user := range teamusers {
  487. if already, ok := resolved[user.LowerName]; !ok || !already {
  488. users = append(users, user)
  489. resolved[user.LowerName] = true
  490. }
  491. }
  492. }
  493. }
  494. }
  495. }
  496. // Remove names already in the list to avoid querying the database if pending names remain
  497. mentionUsers := make([]string, 0, len(resolved))
  498. for name, already := range resolved {
  499. if !already {
  500. mentionUsers = append(mentionUsers, name)
  501. }
  502. }
  503. if len(mentionUsers) == 0 {
  504. return users, err
  505. }
  506. if users == nil {
  507. users = make([]*user_model.User, 0, len(mentionUsers))
  508. }
  509. unchecked := make([]*user_model.User, 0, len(mentionUsers))
  510. if err := db.GetEngine(ctx).
  511. Where("`user`.is_active = ?", true).
  512. And("`user`.prohibit_login = ?", false).
  513. In("`user`.lower_name", mentionUsers).
  514. Find(&unchecked); err != nil {
  515. return nil, fmt.Errorf("find mentioned users: %w", err)
  516. }
  517. for _, user := range unchecked {
  518. if already := resolved[user.LowerName]; already || user.IsOrganization() {
  519. continue
  520. }
  521. // Normal users must have read access to the referencing issue
  522. perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user)
  523. if err != nil {
  524. return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err)
  525. }
  526. if !perm.CanReadIssuesOrPulls(issue.IsPull) {
  527. continue
  528. }
  529. users = append(users, user)
  530. }
  531. return users, err
  532. }
  533. // UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID
  534. func UpdateIssuesMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error {
  535. _, err := db.GetEngine(ctx).Table("issue").
  536. Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
  537. And("original_author_id = ?", originalAuthorID).
  538. Update(map[string]any{
  539. "poster_id": posterID,
  540. "original_author": "",
  541. "original_author_id": 0,
  542. })
  543. return err
  544. }
  545. // UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID
  546. func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error {
  547. _, err := db.GetEngine(ctx).Table("reaction").
  548. Where("original_author_id = ?", originalAuthorID).
  549. And(migratedIssueCond(gitServiceType)).
  550. Update(map[string]any{
  551. "user_id": userID,
  552. "original_author": "",
  553. "original_author_id": 0,
  554. })
  555. return err
  556. }
  557. // DeleteIssuesByRepoID deletes issues by repositories id
  558. func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
  559. // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
  560. // so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
  561. sess := db.GetEngine(ctx)
  562. for {
  563. issueIDs := make([]int64, 0, db.DefaultMaxInSize)
  564. err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs)
  565. if err != nil {
  566. return nil, err
  567. }
  568. if len(issueIDs) == 0 {
  569. break
  570. }
  571. // Delete content histories
  572. _, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{})
  573. if err != nil {
  574. return nil, err
  575. }
  576. // Delete comments and attachments
  577. _, err = sess.In("issue_id", issueIDs).Delete(&Comment{})
  578. if err != nil {
  579. return nil, err
  580. }
  581. // Dependencies for issues in this repository
  582. _, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{})
  583. if err != nil {
  584. return nil, err
  585. }
  586. // Delete dependencies for issues in other repositories
  587. _, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{})
  588. if err != nil {
  589. return nil, err
  590. }
  591. _, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{})
  592. if err != nil {
  593. return nil, err
  594. }
  595. _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{})
  596. if err != nil {
  597. return nil, err
  598. }
  599. _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{})
  600. if err != nil {
  601. return nil, err
  602. }
  603. _, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{})
  604. if err != nil {
  605. return nil, err
  606. }
  607. _, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{})
  608. if err != nil {
  609. return nil, err
  610. }
  611. _, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{})
  612. if err != nil {
  613. return nil, err
  614. }
  615. _, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{})
  616. if err != nil {
  617. return nil, err
  618. }
  619. var attachments []*repo_model.Attachment
  620. err = sess.In("issue_id", issueIDs).Find(&attachments)
  621. if err != nil {
  622. return nil, err
  623. }
  624. for j := range attachments {
  625. attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
  626. }
  627. _, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{})
  628. if err != nil {
  629. return nil, err
  630. }
  631. _, err = sess.In("id", issueIDs).Delete(&Issue{})
  632. if err != nil {
  633. return nil, err
  634. }
  635. }
  636. return attachmentPaths, err
  637. }
  638. // DeleteOrphanedIssues delete issues without a repo
  639. func DeleteOrphanedIssues(ctx context.Context) error {
  640. var attachmentPaths []string
  641. err := db.WithTx(ctx, func(ctx context.Context) error {
  642. var ids []int64
  643. if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
  644. Join("LEFT", "repository", "issue.repo_id=repository.id").
  645. Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
  646. Find(&ids); err != nil {
  647. return err
  648. }
  649. for i := range ids {
  650. paths, err := DeleteIssuesByRepoID(ctx, ids[i])
  651. if err != nil {
  652. return err
  653. }
  654. attachmentPaths = append(attachmentPaths, paths...)
  655. }
  656. return nil
  657. })
  658. if err != nil {
  659. return err
  660. }
  661. // Remove issue attachment files.
  662. for i := range attachmentPaths {
  663. system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
  664. }
  665. return nil
  666. }