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 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830
  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. // UpdateIssueByAPI updates all allowed fields of given issue.
  370. // If the issue status is changed a statusChangeComment is returned
  371. // similarly if the title is changed the titleChanged bool is set to true
  372. func UpdateIssueByAPI(ctx context.Context, issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) {
  373. ctx, committer, err := db.TxContext(ctx)
  374. if err != nil {
  375. return nil, false, err
  376. }
  377. defer committer.Close()
  378. if err := issue.LoadRepo(ctx); err != nil {
  379. return nil, false, fmt.Errorf("loadRepo: %w", err)
  380. }
  381. // Reload the issue
  382. currentIssue, err := GetIssueByID(ctx, issue.ID)
  383. if err != nil {
  384. return nil, false, err
  385. }
  386. if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(
  387. "name", "content", "milestone_id", "priority",
  388. "deadline_unix", "updated_unix", "is_locked").
  389. Update(issue); err != nil {
  390. return nil, false, err
  391. }
  392. titleChanged = currentIssue.Title != issue.Title
  393. if titleChanged {
  394. opts := &CreateCommentOptions{
  395. Type: CommentTypeChangeTitle,
  396. Doer: doer,
  397. Repo: issue.Repo,
  398. Issue: issue,
  399. OldTitle: currentIssue.Title,
  400. NewTitle: issue.Title,
  401. }
  402. _, err := CreateComment(ctx, opts)
  403. if err != nil {
  404. return nil, false, fmt.Errorf("createComment: %w", err)
  405. }
  406. }
  407. if currentIssue.IsClosed != issue.IsClosed {
  408. statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false)
  409. if err != nil {
  410. return nil, false, err
  411. }
  412. }
  413. if err := issue.AddCrossReferences(ctx, doer, true); err != nil {
  414. return nil, false, err
  415. }
  416. return statusChangeComment, titleChanged, committer.Commit()
  417. }
  418. // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
  419. func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) {
  420. // if the deadline hasn't changed do nothing
  421. if issue.DeadlineUnix == deadlineUnix {
  422. return nil
  423. }
  424. ctx, committer, err := db.TxContext(ctx)
  425. if err != nil {
  426. return err
  427. }
  428. defer committer.Close()
  429. // Update the deadline
  430. if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil {
  431. return err
  432. }
  433. // Make the comment
  434. if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil {
  435. return fmt.Errorf("createRemovedDueDateComment: %w", err)
  436. }
  437. return committer.Commit()
  438. }
  439. // DeleteInIssue delete records in beans with external key issue_id = ?
  440. func DeleteInIssue(ctx context.Context, issueID int64, beans ...any) error {
  441. e := db.GetEngine(ctx)
  442. for _, bean := range beans {
  443. if _, err := e.In("issue_id", issueID).Delete(bean); err != nil {
  444. return err
  445. }
  446. }
  447. return nil
  448. }
  449. // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database.
  450. func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) {
  451. rawMentions := references.FindAllMentionsMarkdown(content)
  452. mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions)
  453. if err != nil {
  454. return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
  455. }
  456. if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
  457. return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
  458. }
  459. return mentions, err
  460. }
  461. // ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that
  462. // don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
  463. func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) {
  464. if len(mentions) == 0 {
  465. return nil, nil
  466. }
  467. if err = issue.LoadRepo(ctx); err != nil {
  468. return nil, err
  469. }
  470. resolved := make(map[string]bool, 10)
  471. var mentionTeams []string
  472. if err := issue.Repo.LoadOwner(ctx); err != nil {
  473. return nil, err
  474. }
  475. repoOwnerIsOrg := issue.Repo.Owner.IsOrganization()
  476. if repoOwnerIsOrg {
  477. mentionTeams = make([]string, 0, 5)
  478. }
  479. resolved[doer.LowerName] = true
  480. for _, name := range mentions {
  481. name := strings.ToLower(name)
  482. if _, ok := resolved[name]; ok {
  483. continue
  484. }
  485. if repoOwnerIsOrg && strings.Contains(name, "/") {
  486. names := strings.Split(name, "/")
  487. if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName {
  488. continue
  489. }
  490. mentionTeams = append(mentionTeams, names[1])
  491. resolved[name] = true
  492. } else {
  493. resolved[name] = false
  494. }
  495. }
  496. if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 {
  497. teams := make([]*organization.Team, 0, len(mentionTeams))
  498. if err := db.GetEngine(ctx).
  499. Join("INNER", "team_repo", "team_repo.team_id = team.id").
  500. Where("team_repo.repo_id=?", issue.Repo.ID).
  501. In("team.lower_name", mentionTeams).
  502. Find(&teams); err != nil {
  503. return nil, fmt.Errorf("find mentioned teams: %w", err)
  504. }
  505. if len(teams) != 0 {
  506. checked := make([]int64, 0, len(teams))
  507. unittype := unit.TypeIssues
  508. if issue.IsPull {
  509. unittype = unit.TypePullRequests
  510. }
  511. for _, team := range teams {
  512. if team.AccessMode >= perm.AccessModeAdmin {
  513. checked = append(checked, team.ID)
  514. resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
  515. continue
  516. }
  517. has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
  518. if err != nil {
  519. return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
  520. }
  521. if has {
  522. checked = append(checked, team.ID)
  523. resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
  524. }
  525. }
  526. if len(checked) != 0 {
  527. teamusers := make([]*user_model.User, 0, 20)
  528. if err := db.GetEngine(ctx).
  529. Join("INNER", "team_user", "team_user.uid = `user`.id").
  530. In("`team_user`.team_id", checked).
  531. And("`user`.is_active = ?", true).
  532. And("`user`.prohibit_login = ?", false).
  533. Find(&teamusers); err != nil {
  534. return nil, fmt.Errorf("get teams users: %w", err)
  535. }
  536. if len(teamusers) > 0 {
  537. users = make([]*user_model.User, 0, len(teamusers))
  538. for _, user := range teamusers {
  539. if already, ok := resolved[user.LowerName]; !ok || !already {
  540. users = append(users, user)
  541. resolved[user.LowerName] = true
  542. }
  543. }
  544. }
  545. }
  546. }
  547. }
  548. // Remove names already in the list to avoid querying the database if pending names remain
  549. mentionUsers := make([]string, 0, len(resolved))
  550. for name, already := range resolved {
  551. if !already {
  552. mentionUsers = append(mentionUsers, name)
  553. }
  554. }
  555. if len(mentionUsers) == 0 {
  556. return users, err
  557. }
  558. if users == nil {
  559. users = make([]*user_model.User, 0, len(mentionUsers))
  560. }
  561. unchecked := make([]*user_model.User, 0, len(mentionUsers))
  562. if err := db.GetEngine(ctx).
  563. Where("`user`.is_active = ?", true).
  564. And("`user`.prohibit_login = ?", false).
  565. In("`user`.lower_name", mentionUsers).
  566. Find(&unchecked); err != nil {
  567. return nil, fmt.Errorf("find mentioned users: %w", err)
  568. }
  569. for _, user := range unchecked {
  570. if already := resolved[user.LowerName]; already || user.IsOrganization() {
  571. continue
  572. }
  573. // Normal users must have read access to the referencing issue
  574. perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user)
  575. if err != nil {
  576. return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err)
  577. }
  578. if !perm.CanReadIssuesOrPulls(issue.IsPull) {
  579. continue
  580. }
  581. users = append(users, user)
  582. }
  583. return users, err
  584. }
  585. // UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID
  586. func UpdateIssuesMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error {
  587. _, err := db.GetEngine(ctx).Table("issue").
  588. Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
  589. And("original_author_id = ?", originalAuthorID).
  590. Update(map[string]any{
  591. "poster_id": posterID,
  592. "original_author": "",
  593. "original_author_id": 0,
  594. })
  595. return err
  596. }
  597. // UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID
  598. func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error {
  599. _, err := db.GetEngine(ctx).Table("reaction").
  600. Where("original_author_id = ?", originalAuthorID).
  601. And(migratedIssueCond(gitServiceType)).
  602. Update(map[string]any{
  603. "user_id": userID,
  604. "original_author": "",
  605. "original_author_id": 0,
  606. })
  607. return err
  608. }
  609. // DeleteIssuesByRepoID deletes issues by repositories id
  610. func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
  611. // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
  612. // so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
  613. sess := db.GetEngine(ctx)
  614. for {
  615. issueIDs := make([]int64, 0, db.DefaultMaxInSize)
  616. err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs)
  617. if err != nil {
  618. return nil, err
  619. }
  620. if len(issueIDs) == 0 {
  621. break
  622. }
  623. // Delete content histories
  624. _, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{})
  625. if err != nil {
  626. return nil, err
  627. }
  628. // Delete comments and attachments
  629. _, err = sess.In("issue_id", issueIDs).Delete(&Comment{})
  630. if err != nil {
  631. return nil, err
  632. }
  633. // Dependencies for issues in this repository
  634. _, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{})
  635. if err != nil {
  636. return nil, err
  637. }
  638. // Delete dependencies for issues in other repositories
  639. _, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{})
  640. if err != nil {
  641. return nil, err
  642. }
  643. _, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{})
  644. if err != nil {
  645. return nil, err
  646. }
  647. _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{})
  648. if err != nil {
  649. return nil, err
  650. }
  651. _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{})
  652. if err != nil {
  653. return nil, err
  654. }
  655. _, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{})
  656. if err != nil {
  657. return nil, err
  658. }
  659. _, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{})
  660. if err != nil {
  661. return nil, err
  662. }
  663. _, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{})
  664. if err != nil {
  665. return nil, err
  666. }
  667. _, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{})
  668. if err != nil {
  669. return nil, err
  670. }
  671. var attachments []*repo_model.Attachment
  672. err = sess.In("issue_id", issueIDs).Find(&attachments)
  673. if err != nil {
  674. return nil, err
  675. }
  676. for j := range attachments {
  677. attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
  678. }
  679. _, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{})
  680. if err != nil {
  681. return nil, err
  682. }
  683. _, err = sess.In("id", issueIDs).Delete(&Issue{})
  684. if err != nil {
  685. return nil, err
  686. }
  687. }
  688. return attachmentPaths, err
  689. }
  690. // DeleteOrphanedIssues delete issues without a repo
  691. func DeleteOrphanedIssues(ctx context.Context) error {
  692. var attachmentPaths []string
  693. err := db.WithTx(ctx, func(ctx context.Context) error {
  694. var ids []int64
  695. if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
  696. Join("LEFT", "repository", "issue.repo_id=repository.id").
  697. Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
  698. Find(&ids); err != nil {
  699. return err
  700. }
  701. for i := range ids {
  702. paths, err := DeleteIssuesByRepoID(ctx, ids[i])
  703. if err != nil {
  704. return err
  705. }
  706. attachmentPaths = append(attachmentPaths, paths...)
  707. }
  708. return nil
  709. })
  710. if err != nil {
  711. return err
  712. }
  713. // Remove issue attachment files.
  714. for i := range attachmentPaths {
  715. system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
  716. }
  717. return nil
  718. }