選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

issue_update.go 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  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. // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database.
  440. func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) {
  441. rawMentions := references.FindAllMentionsMarkdown(content)
  442. mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions)
  443. if err != nil {
  444. return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
  445. }
  446. notBlocked := make([]*user_model.User, 0, len(mentions))
  447. for _, user := range mentions {
  448. if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
  449. notBlocked = append(notBlocked, user)
  450. }
  451. }
  452. mentions = notBlocked
  453. if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
  454. return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
  455. }
  456. return mentions, err
  457. }
  458. // ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that
  459. // don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
  460. func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) {
  461. if len(mentions) == 0 {
  462. return nil, nil
  463. }
  464. if err = issue.LoadRepo(ctx); err != nil {
  465. return nil, err
  466. }
  467. resolved := make(map[string]bool, 10)
  468. var mentionTeams []string
  469. if err := issue.Repo.LoadOwner(ctx); err != nil {
  470. return nil, err
  471. }
  472. repoOwnerIsOrg := issue.Repo.Owner.IsOrganization()
  473. if repoOwnerIsOrg {
  474. mentionTeams = make([]string, 0, 5)
  475. }
  476. resolved[doer.LowerName] = true
  477. for _, name := range mentions {
  478. name := strings.ToLower(name)
  479. if _, ok := resolved[name]; ok {
  480. continue
  481. }
  482. if repoOwnerIsOrg && strings.Contains(name, "/") {
  483. names := strings.Split(name, "/")
  484. if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName {
  485. continue
  486. }
  487. mentionTeams = append(mentionTeams, names[1])
  488. resolved[name] = true
  489. } else {
  490. resolved[name] = false
  491. }
  492. }
  493. if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 {
  494. teams := make([]*organization.Team, 0, len(mentionTeams))
  495. if err := db.GetEngine(ctx).
  496. Join("INNER", "team_repo", "team_repo.team_id = team.id").
  497. Where("team_repo.repo_id=?", issue.Repo.ID).
  498. In("team.lower_name", mentionTeams).
  499. Find(&teams); err != nil {
  500. return nil, fmt.Errorf("find mentioned teams: %w", err)
  501. }
  502. if len(teams) != 0 {
  503. checked := make([]int64, 0, len(teams))
  504. unittype := unit.TypeIssues
  505. if issue.IsPull {
  506. unittype = unit.TypePullRequests
  507. }
  508. for _, team := range teams {
  509. if team.AccessMode >= perm.AccessModeAdmin {
  510. checked = append(checked, team.ID)
  511. resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
  512. continue
  513. }
  514. has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
  515. if err != nil {
  516. return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
  517. }
  518. if has {
  519. checked = append(checked, team.ID)
  520. resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
  521. }
  522. }
  523. if len(checked) != 0 {
  524. teamusers := make([]*user_model.User, 0, 20)
  525. if err := db.GetEngine(ctx).
  526. Join("INNER", "team_user", "team_user.uid = `user`.id").
  527. In("`team_user`.team_id", checked).
  528. And("`user`.is_active = ?", true).
  529. And("`user`.prohibit_login = ?", false).
  530. Find(&teamusers); err != nil {
  531. return nil, fmt.Errorf("get teams users: %w", err)
  532. }
  533. if len(teamusers) > 0 {
  534. users = make([]*user_model.User, 0, len(teamusers))
  535. for _, user := range teamusers {
  536. if already, ok := resolved[user.LowerName]; !ok || !already {
  537. users = append(users, user)
  538. resolved[user.LowerName] = true
  539. }
  540. }
  541. }
  542. }
  543. }
  544. }
  545. // Remove names already in the list to avoid querying the database if pending names remain
  546. mentionUsers := make([]string, 0, len(resolved))
  547. for name, already := range resolved {
  548. if !already {
  549. mentionUsers = append(mentionUsers, name)
  550. }
  551. }
  552. if len(mentionUsers) == 0 {
  553. return users, err
  554. }
  555. if users == nil {
  556. users = make([]*user_model.User, 0, len(mentionUsers))
  557. }
  558. unchecked := make([]*user_model.User, 0, len(mentionUsers))
  559. if err := db.GetEngine(ctx).
  560. Where("`user`.is_active = ?", true).
  561. And("`user`.prohibit_login = ?", false).
  562. In("`user`.lower_name", mentionUsers).
  563. Find(&unchecked); err != nil {
  564. return nil, fmt.Errorf("find mentioned users: %w", err)
  565. }
  566. for _, user := range unchecked {
  567. if already := resolved[user.LowerName]; already || user.IsOrganization() {
  568. continue
  569. }
  570. // Normal users must have read access to the referencing issue
  571. perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user)
  572. if err != nil {
  573. return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err)
  574. }
  575. if !perm.CanReadIssuesOrPulls(issue.IsPull) {
  576. continue
  577. }
  578. users = append(users, user)
  579. }
  580. return users, err
  581. }
  582. // UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID
  583. func UpdateIssuesMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error {
  584. _, err := db.GetEngine(ctx).Table("issue").
  585. Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
  586. And("original_author_id = ?", originalAuthorID).
  587. Update(map[string]any{
  588. "poster_id": posterID,
  589. "original_author": "",
  590. "original_author_id": 0,
  591. })
  592. return err
  593. }
  594. // UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID
  595. func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error {
  596. _, err := db.GetEngine(ctx).Table("reaction").
  597. Where("original_author_id = ?", originalAuthorID).
  598. And(migratedIssueCond(gitServiceType)).
  599. Update(map[string]any{
  600. "user_id": userID,
  601. "original_author": "",
  602. "original_author_id": 0,
  603. })
  604. return err
  605. }
  606. // DeleteIssuesByRepoID deletes issues by repositories id
  607. func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
  608. // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
  609. // so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
  610. sess := db.GetEngine(ctx)
  611. for {
  612. issueIDs := make([]int64, 0, db.DefaultMaxInSize)
  613. err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs)
  614. if err != nil {
  615. return nil, err
  616. }
  617. if len(issueIDs) == 0 {
  618. break
  619. }
  620. // Delete content histories
  621. _, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{})
  622. if err != nil {
  623. return nil, err
  624. }
  625. // Delete comments and attachments
  626. _, err = sess.In("issue_id", issueIDs).Delete(&Comment{})
  627. if err != nil {
  628. return nil, err
  629. }
  630. // Dependencies for issues in this repository
  631. _, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{})
  632. if err != nil {
  633. return nil, err
  634. }
  635. // Delete dependencies for issues in other repositories
  636. _, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{})
  637. if err != nil {
  638. return nil, err
  639. }
  640. _, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{})
  641. if err != nil {
  642. return nil, err
  643. }
  644. _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{})
  645. if err != nil {
  646. return nil, err
  647. }
  648. _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{})
  649. if err != nil {
  650. return nil, err
  651. }
  652. _, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{})
  653. if err != nil {
  654. return nil, err
  655. }
  656. _, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{})
  657. if err != nil {
  658. return nil, err
  659. }
  660. _, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{})
  661. if err != nil {
  662. return nil, err
  663. }
  664. _, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{})
  665. if err != nil {
  666. return nil, err
  667. }
  668. var attachments []*repo_model.Attachment
  669. err = sess.In("issue_id", issueIDs).Find(&attachments)
  670. if err != nil {
  671. return nil, err
  672. }
  673. for j := range attachments {
  674. attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
  675. }
  676. _, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{})
  677. if err != nil {
  678. return nil, err
  679. }
  680. _, err = sess.In("id", issueIDs).Delete(&Issue{})
  681. if err != nil {
  682. return nil, err
  683. }
  684. }
  685. return attachmentPaths, err
  686. }
  687. // DeleteOrphanedIssues delete issues without a repo
  688. func DeleteOrphanedIssues(ctx context.Context) error {
  689. var attachmentPaths []string
  690. err := db.WithTx(ctx, func(ctx context.Context) error {
  691. var ids []int64
  692. if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
  693. Join("LEFT", "repository", "issue.repo_id=repository.id").
  694. Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
  695. Find(&ids); err != nil {
  696. return err
  697. }
  698. for i := range ids {
  699. paths, err := DeleteIssuesByRepoID(ctx, ids[i])
  700. if err != nil {
  701. return err
  702. }
  703. attachmentPaths = append(attachmentPaths, paths...)
  704. }
  705. return nil
  706. })
  707. if err != nil {
  708. return err
  709. }
  710. // Remove issue attachment files.
  711. for i := range attachmentPaths {
  712. system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
  713. }
  714. return nil
  715. }