gitea源码

issue_update.go 20KB

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