gitea源码

issue.go 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2020 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package issues
  5. import (
  6. "context"
  7. "fmt"
  8. "html/template"
  9. "regexp"
  10. "slices"
  11. "strconv"
  12. "code.gitea.io/gitea/models/db"
  13. project_model "code.gitea.io/gitea/models/project"
  14. repo_model "code.gitea.io/gitea/models/repo"
  15. user_model "code.gitea.io/gitea/models/user"
  16. "code.gitea.io/gitea/modules/container"
  17. "code.gitea.io/gitea/modules/log"
  18. "code.gitea.io/gitea/modules/optional"
  19. "code.gitea.io/gitea/modules/setting"
  20. api "code.gitea.io/gitea/modules/structs"
  21. "code.gitea.io/gitea/modules/timeutil"
  22. "code.gitea.io/gitea/modules/util"
  23. "xorm.io/builder"
  24. )
  25. // ErrIssueNotExist represents a "IssueNotExist" kind of error.
  26. type ErrIssueNotExist struct {
  27. ID int64
  28. RepoID int64
  29. Index int64
  30. }
  31. // IsErrIssueNotExist checks if an error is a ErrIssueNotExist.
  32. func IsErrIssueNotExist(err error) bool {
  33. _, ok := err.(ErrIssueNotExist)
  34. return ok
  35. }
  36. func (err ErrIssueNotExist) Error() string {
  37. return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
  38. }
  39. func (err ErrIssueNotExist) Unwrap() error {
  40. return util.ErrNotExist
  41. }
  42. // ErrNewIssueInsert is used when the INSERT statement in newIssue fails
  43. type ErrNewIssueInsert struct {
  44. OriginalError error
  45. }
  46. // IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert.
  47. func IsErrNewIssueInsert(err error) bool {
  48. _, ok := err.(ErrNewIssueInsert)
  49. return ok
  50. }
  51. func (err ErrNewIssueInsert) Error() string {
  52. return err.OriginalError.Error()
  53. }
  54. var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed")
  55. // Issue represents an issue or pull request of repository.
  56. type Issue struct {
  57. ID int64 `xorm:"pk autoincr"`
  58. RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
  59. Repo *repo_model.Repository `xorm:"-"`
  60. Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
  61. PosterID int64 `xorm:"INDEX"`
  62. Poster *user_model.User `xorm:"-"`
  63. OriginalAuthor string
  64. OriginalAuthorID int64 `xorm:"index"`
  65. Title string `xorm:"name"`
  66. Content string `xorm:"LONGTEXT"`
  67. RenderedContent template.HTML `xorm:"-"`
  68. ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
  69. Labels []*Label `xorm:"-"`
  70. isLabelsLoaded bool `xorm:"-"`
  71. MilestoneID int64 `xorm:"INDEX"`
  72. Milestone *Milestone `xorm:"-"`
  73. isMilestoneLoaded bool `xorm:"-"`
  74. Project *project_model.Project `xorm:"-"`
  75. Priority int
  76. AssigneeID int64 `xorm:"-"`
  77. Assignee *user_model.User `xorm:"-"`
  78. isAssigneeLoaded bool `xorm:"-"`
  79. IsClosed bool `xorm:"INDEX"`
  80. IsRead bool `xorm:"-"`
  81. IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
  82. PullRequest *PullRequest `xorm:"-"`
  83. NumComments int
  84. // TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl"
  85. Ref string
  86. PinOrder int `xorm:"-"` // 0 means not loaded, -1 means loaded but not pinned
  87. DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
  88. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  89. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  90. ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
  91. Attachments []*repo_model.Attachment `xorm:"-"`
  92. isAttachmentsLoaded bool `xorm:"-"`
  93. Comments CommentList `xorm:"-"`
  94. Reactions ReactionList `xorm:"-"`
  95. TotalTrackedTime int64 `xorm:"-"`
  96. Assignees []*user_model.User `xorm:"-"`
  97. // IsLocked limits commenting abilities to users on an issue
  98. // with write access
  99. IsLocked bool `xorm:"NOT NULL DEFAULT false"`
  100. // For view issue page.
  101. ShowRole RoleDescriptor `xorm:"-"`
  102. // Time estimate
  103. TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
  104. }
  105. var (
  106. issueTasksPat = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`)
  107. issueTasksDonePat = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`)
  108. )
  109. // IssueIndex represents the issue index table
  110. type IssueIndex db.ResourceIndex
  111. func init() {
  112. db.RegisterModel(new(Issue))
  113. db.RegisterModel(new(IssueIndex))
  114. }
  115. // LoadTotalTimes load total tracked time
  116. func (issue *Issue) LoadTotalTimes(ctx context.Context) (err error) {
  117. opts := FindTrackedTimesOptions{IssueID: issue.ID}
  118. issue.TotalTrackedTime, err = opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time")
  119. if err != nil {
  120. return err
  121. }
  122. return nil
  123. }
  124. // IsOverdue checks if the issue is overdue
  125. func (issue *Issue) IsOverdue() bool {
  126. if issue.IsClosed {
  127. return issue.ClosedUnix >= issue.DeadlineUnix
  128. }
  129. return timeutil.TimeStampNow() >= issue.DeadlineUnix
  130. }
  131. // LoadRepo loads issue's repository
  132. func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
  133. if issue.Repo == nil && issue.RepoID != 0 {
  134. issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
  135. if err != nil {
  136. return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err)
  137. }
  138. }
  139. return nil
  140. }
  141. func (issue *Issue) LoadAttachments(ctx context.Context) (err error) {
  142. if issue.isAttachmentsLoaded || issue.Attachments != nil {
  143. return nil
  144. }
  145. issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
  146. if err != nil {
  147. return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
  148. }
  149. issue.isAttachmentsLoaded = true
  150. return nil
  151. }
  152. // IsTimetrackerEnabled returns true if the repo enables timetracking
  153. func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
  154. if err := issue.LoadRepo(ctx); err != nil {
  155. log.Error(fmt.Sprintf("loadRepo: %v", err))
  156. return false
  157. }
  158. return issue.Repo.IsTimetrackerEnabled(ctx)
  159. }
  160. // LoadPoster loads poster
  161. func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
  162. if issue.Poster == nil && issue.PosterID != 0 {
  163. issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
  164. if err != nil {
  165. issue.PosterID = user_model.GhostUserID
  166. issue.Poster = user_model.NewGhostUser()
  167. if !user_model.IsErrUserNotExist(err) {
  168. return fmt.Errorf("getUserByID.(poster) [%d]: %w", issue.PosterID, err)
  169. }
  170. return nil
  171. }
  172. }
  173. return err
  174. }
  175. // LoadPullRequest loads pull request info
  176. func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {
  177. if issue.IsPull {
  178. if issue.PullRequest == nil && issue.ID != 0 {
  179. issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID)
  180. if err != nil {
  181. if IsErrPullRequestNotExist(err) {
  182. return err
  183. }
  184. return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err)
  185. }
  186. }
  187. if issue.PullRequest != nil {
  188. issue.PullRequest.Issue = issue
  189. }
  190. }
  191. return nil
  192. }
  193. func (issue *Issue) loadComments(ctx context.Context) (err error) {
  194. return issue.loadCommentsByType(ctx, CommentTypeUndefined)
  195. }
  196. // LoadDiscussComments loads discuss comments
  197. func (issue *Issue) LoadDiscussComments(ctx context.Context) error {
  198. return issue.loadCommentsByType(ctx, CommentTypeComment)
  199. }
  200. func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err error) {
  201. if issue.Comments != nil {
  202. return nil
  203. }
  204. issue.Comments, err = FindComments(ctx, &FindCommentsOptions{
  205. IssueID: issue.ID,
  206. Type: tp,
  207. })
  208. for _, comment := range issue.Comments {
  209. comment.Issue = issue
  210. }
  211. return err
  212. }
  213. func (issue *Issue) loadReactions(ctx context.Context) (err error) {
  214. if issue.Reactions != nil {
  215. return nil
  216. }
  217. reactions, _, err := FindReactions(ctx, FindReactionsOptions{
  218. IssueID: issue.ID,
  219. })
  220. if err != nil {
  221. return err
  222. }
  223. if err = issue.LoadRepo(ctx); err != nil {
  224. return err
  225. }
  226. // Load reaction user data
  227. if _, err := reactions.LoadUsers(ctx, issue.Repo); err != nil {
  228. return err
  229. }
  230. // Cache comments to map
  231. comments := make(map[int64]*Comment)
  232. for _, comment := range issue.Comments {
  233. comments[comment.ID] = comment
  234. }
  235. // Add reactions either to issue or comment
  236. for _, react := range reactions {
  237. if react.CommentID == 0 {
  238. issue.Reactions = append(issue.Reactions, react)
  239. } else if comment, ok := comments[react.CommentID]; ok {
  240. comment.Reactions = append(comment.Reactions, react)
  241. }
  242. }
  243. return nil
  244. }
  245. // LoadMilestone load milestone of this issue.
  246. func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
  247. if !issue.isMilestoneLoaded && (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
  248. issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
  249. if err != nil && !IsErrMilestoneNotExist(err) {
  250. return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
  251. }
  252. issue.isMilestoneLoaded = true
  253. }
  254. return nil
  255. }
  256. func (issue *Issue) LoadPinOrder(ctx context.Context) error {
  257. if issue.PinOrder != 0 {
  258. return nil
  259. }
  260. issuePin, err := GetIssuePin(ctx, issue)
  261. if err != nil && !db.IsErrNotExist(err) {
  262. return err
  263. }
  264. if issuePin != nil {
  265. issue.PinOrder = issuePin.PinOrder
  266. } else {
  267. issue.PinOrder = -1
  268. }
  269. return nil
  270. }
  271. // LoadAttributes loads the attribute of this issue.
  272. func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
  273. if err = issue.LoadRepo(ctx); err != nil {
  274. return err
  275. }
  276. if err = issue.LoadPoster(ctx); err != nil {
  277. return err
  278. }
  279. if err = issue.LoadLabels(ctx); err != nil {
  280. return err
  281. }
  282. if err = issue.LoadMilestone(ctx); err != nil {
  283. return err
  284. }
  285. if err = issue.LoadProject(ctx); err != nil {
  286. return err
  287. }
  288. if err = issue.LoadAssignees(ctx); err != nil {
  289. return err
  290. }
  291. if err = issue.LoadPullRequest(ctx); err != nil && !IsErrPullRequestNotExist(err) {
  292. // It is possible pull request is not yet created.
  293. return err
  294. }
  295. if err = issue.LoadAttachments(ctx); err != nil {
  296. return err
  297. }
  298. if err = issue.loadComments(ctx); err != nil {
  299. return err
  300. }
  301. if err = issue.LoadPinOrder(ctx); err != nil {
  302. return err
  303. }
  304. if err = issue.Comments.LoadAttributes(ctx); err != nil {
  305. return err
  306. }
  307. if issue.IsTimetrackerEnabled(ctx) {
  308. if err = issue.LoadTotalTimes(ctx); err != nil {
  309. return err
  310. }
  311. }
  312. return issue.loadReactions(ctx)
  313. }
  314. // IsPinned returns if a Issue is pinned
  315. func (issue *Issue) IsPinned() bool {
  316. if issue.PinOrder == 0 {
  317. setting.PanicInDevOrTesting("issue's pinorder has not been loaded")
  318. }
  319. return issue.PinOrder > 0
  320. }
  321. func (issue *Issue) ResetAttributesLoaded() {
  322. issue.isLabelsLoaded = false
  323. issue.isMilestoneLoaded = false
  324. issue.isAttachmentsLoaded = false
  325. issue.isAssigneeLoaded = false
  326. }
  327. // GetIsRead load the `IsRead` field of the issue
  328. func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
  329. issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
  330. if has, err := db.GetEngine(ctx).Get(issueUser); err != nil {
  331. return err
  332. } else if !has {
  333. issue.IsRead = false
  334. return nil
  335. }
  336. issue.IsRead = issueUser.IsRead
  337. return nil
  338. }
  339. // APIURL returns the absolute APIURL to this issue.
  340. func (issue *Issue) APIURL(ctx context.Context) string {
  341. if issue.Repo == nil {
  342. err := issue.LoadRepo(ctx)
  343. if err != nil {
  344. log.Error("Issue[%d].APIURL(): %v", issue.ID, err)
  345. return ""
  346. }
  347. }
  348. return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index)
  349. }
  350. // HTMLURL returns the absolute URL to this issue.
  351. func (issue *Issue) HTMLURL(ctx context.Context) string {
  352. var path string
  353. if issue.IsPull {
  354. path = "pulls"
  355. } else {
  356. path = "issues"
  357. }
  358. return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(ctx), path, issue.Index)
  359. }
  360. // Link returns the issue's relative URL.
  361. func (issue *Issue) Link() string {
  362. var path string
  363. if issue.IsPull {
  364. path = "pulls"
  365. } else {
  366. path = "issues"
  367. }
  368. return fmt.Sprintf("%s/%s/%d", issue.Repo.Link(), path, issue.Index)
  369. }
  370. // DiffURL returns the absolute URL to this diff
  371. func (issue *Issue) DiffURL() string {
  372. if issue.IsPull {
  373. return fmt.Sprintf("%s/pulls/%d.diff", issue.Repo.HTMLURL(), issue.Index)
  374. }
  375. return ""
  376. }
  377. // PatchURL returns the absolute URL to this patch
  378. func (issue *Issue) PatchURL() string {
  379. if issue.IsPull {
  380. return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index)
  381. }
  382. return ""
  383. }
  384. // State returns string representation of issue status.
  385. func (issue *Issue) State() api.StateType {
  386. if issue.IsClosed {
  387. return api.StateClosed
  388. }
  389. return api.StateOpen
  390. }
  391. // HashTag returns unique hash tag for issue.
  392. func (issue *Issue) HashTag() string {
  393. return fmt.Sprintf("issue-%d", issue.ID)
  394. }
  395. // IsPoster returns true if given user by ID is the poster.
  396. func (issue *Issue) IsPoster(uid int64) bool {
  397. return issue.OriginalAuthorID == 0 && issue.PosterID == uid
  398. }
  399. // GetTasks returns the amount of tasks in the issues content
  400. func (issue *Issue) GetTasks() int {
  401. return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
  402. }
  403. // GetTasksDone returns the amount of completed tasks in the issues content
  404. func (issue *Issue) GetTasksDone() int {
  405. return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
  406. }
  407. // GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close.
  408. func (issue *Issue) GetLastEventTimestamp() timeutil.TimeStamp {
  409. if issue.IsClosed {
  410. return issue.ClosedUnix
  411. }
  412. return issue.CreatedUnix
  413. }
  414. // GetLastEventLabel returns the localization label for the current issue.
  415. func (issue *Issue) GetLastEventLabel() string {
  416. if issue.IsClosed {
  417. if issue.IsPull && issue.PullRequest.HasMerged {
  418. return "repo.pulls.merged_by"
  419. }
  420. return "repo.issues.closed_by"
  421. }
  422. return "repo.issues.opened_by"
  423. }
  424. // GetLastComment return last comment for the current issue.
  425. func (issue *Issue) GetLastComment(ctx context.Context) (*Comment, error) {
  426. var c Comment
  427. exist, err := db.GetEngine(ctx).Where("type = ?", CommentTypeComment).
  428. And("issue_id = ?", issue.ID).Desc("created_unix").Get(&c)
  429. if err != nil {
  430. return nil, err
  431. }
  432. if !exist {
  433. return nil, nil
  434. }
  435. return &c, nil
  436. }
  437. // GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
  438. func (issue *Issue) GetLastEventLabelFake() string {
  439. if issue.IsClosed {
  440. if issue.IsPull && issue.PullRequest.HasMerged {
  441. return "repo.pulls.merged_by_fake"
  442. }
  443. return "repo.issues.closed_by_fake"
  444. }
  445. return "repo.issues.opened_by_fake"
  446. }
  447. // GetIssueByIndex returns raw issue without loading attributes by index in a repository.
  448. func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
  449. if index < 1 {
  450. return nil, ErrIssueNotExist{}
  451. }
  452. issue := &Issue{
  453. RepoID: repoID,
  454. Index: index,
  455. }
  456. has, err := db.GetEngine(ctx).Get(issue)
  457. if err != nil {
  458. return nil, err
  459. } else if !has {
  460. return nil, ErrIssueNotExist{0, repoID, index}
  461. }
  462. return issue, nil
  463. }
  464. func isPullToCond(isPull optional.Option[bool]) builder.Cond {
  465. if isPull.Has() {
  466. return builder.Eq{"is_pull": isPull.Value()}
  467. }
  468. return builder.NewCond()
  469. }
  470. func FindLatestUpdatedIssues(ctx context.Context, repoID int64, isPull optional.Option[bool], pageSize int) (IssueList, error) {
  471. issues := make([]*Issue, 0, pageSize)
  472. err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
  473. And(isPullToCond(isPull)).
  474. OrderBy("updated_unix DESC").
  475. Limit(pageSize).
  476. Find(&issues)
  477. return issues, err
  478. }
  479. func FindIssuesSuggestionByKeyword(ctx context.Context, repoID int64, keyword string, isPull optional.Option[bool], excludedID int64, pageSize int) (IssueList, error) {
  480. cond := builder.NewCond()
  481. if excludedID > 0 {
  482. cond = cond.And(builder.Neq{"`id`": excludedID})
  483. }
  484. // It seems that GitHub searches both title and content (maybe sorting by the search engine's ranking system?)
  485. // The first PR (https://github.com/go-gitea/gitea/pull/32327) uses "search indexer" to search "name(title) + content"
  486. // But it seems that searching "content" (especially LIKE by DB engine) generates worse (unusable) results.
  487. // So now (https://github.com/go-gitea/gitea/pull/33538) it only searches "name(title)", leave the improvements to the future.
  488. cond = cond.And(db.BuildCaseInsensitiveLike("`name`", keyword))
  489. issues := make([]*Issue, 0, pageSize)
  490. err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
  491. And(isPullToCond(isPull)).
  492. And(cond).
  493. OrderBy("updated_unix DESC, `index` DESC").
  494. Limit(pageSize).
  495. Find(&issues)
  496. return issues, err
  497. }
  498. // GetIssueWithAttrsByIndex returns issue by index in a repository.
  499. func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
  500. issue, err := GetIssueByIndex(ctx, repoID, index)
  501. if err != nil {
  502. return nil, err
  503. }
  504. return issue, issue.LoadAttributes(ctx)
  505. }
  506. // GetIssueByID returns an issue by given ID.
  507. func GetIssueByID(ctx context.Context, id int64) (*Issue, error) {
  508. issue := new(Issue)
  509. has, err := db.GetEngine(ctx).ID(id).Get(issue)
  510. if err != nil {
  511. return nil, err
  512. } else if !has {
  513. return nil, ErrIssueNotExist{id, 0, 0}
  514. }
  515. return issue, nil
  516. }
  517. // GetIssuesByIDs return issues with the given IDs.
  518. // If keepOrder is true, the order of the returned issues will be the same as the given IDs.
  519. func GetIssuesByIDs(ctx context.Context, issueIDs []int64, keepOrder ...bool) (IssueList, error) {
  520. issues := make([]*Issue, 0, len(issueIDs))
  521. if len(issueIDs) == 0 {
  522. return issues, nil
  523. }
  524. if err := db.GetEngine(ctx).In("id", issueIDs).Find(&issues); err != nil {
  525. return nil, err
  526. }
  527. if len(keepOrder) > 0 && keepOrder[0] {
  528. m := make(map[int64]*Issue, len(issues))
  529. appended := container.Set[int64]{}
  530. for _, issue := range issues {
  531. m[issue.ID] = issue
  532. }
  533. issues = issues[:0]
  534. for _, id := range issueIDs {
  535. if issue, ok := m[id]; ok && !appended.Contains(id) { // make sure the id is existed and not appended
  536. appended.Add(id)
  537. issues = append(issues, issue)
  538. }
  539. }
  540. }
  541. return issues, nil
  542. }
  543. // GetIssueIDsByRepoID returns all issue ids by repo id
  544. func GetIssueIDsByRepoID(ctx context.Context, repoID int64) ([]int64, error) {
  545. ids := make([]int64, 0, 10)
  546. err := db.GetEngine(ctx).Table("issue").Cols("id").Where("repo_id = ?", repoID).Find(&ids)
  547. return ids, err
  548. }
  549. // GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue,
  550. // but skips joining with `user` for performance reasons.
  551. // User permissions must be verified elsewhere if required.
  552. func GetParticipantsIDsByIssueID(ctx context.Context, issueID int64) ([]int64, error) {
  553. userIDs := make([]int64, 0, 5)
  554. return userIDs, db.GetEngine(ctx).
  555. Table("comment").
  556. Cols("poster_id").
  557. Where("issue_id = ?", issueID).
  558. And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
  559. Distinct("poster_id").
  560. Find(&userIDs)
  561. }
  562. // IsUserParticipantsOfIssue return true if user is participants of an issue
  563. func IsUserParticipantsOfIssue(ctx context.Context, user *user_model.User, issue *Issue) bool {
  564. userIDs, err := issue.GetParticipantIDsByIssue(ctx)
  565. if err != nil {
  566. log.Error(err.Error())
  567. return false
  568. }
  569. return slices.Contains(userIDs, user.ID)
  570. }
  571. // DependencyInfo represents high level information about an issue which is a dependency of another issue.
  572. type DependencyInfo struct {
  573. Issue `xorm:"extends"`
  574. repo_model.Repository `xorm:"extends"`
  575. }
  576. // GetParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author
  577. func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, error) {
  578. if issue == nil {
  579. return nil, nil
  580. }
  581. userIDs := make([]int64, 0, 5)
  582. if err := db.GetEngine(ctx).Table("comment").Cols("poster_id").
  583. Where("`comment`.issue_id = ?", issue.ID).
  584. And("`comment`.type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
  585. And("`user`.is_active = ?", true).
  586. And("`user`.prohibit_login = ?", false).
  587. Join("INNER", "`user`", "`user`.id = `comment`.poster_id").
  588. Distinct("poster_id").
  589. Find(&userIDs); err != nil {
  590. return nil, fmt.Errorf("get poster IDs: %w", err)
  591. }
  592. if !slices.Contains(userIDs, issue.PosterID) {
  593. return append(userIDs, issue.PosterID), nil
  594. }
  595. return userIDs, nil
  596. }
  597. // BlockedByDependencies finds all Dependencies an issue is blocked by
  598. func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, err error) {
  599. sess := db.GetEngine(ctx).
  600. Table("issue").
  601. Join("INNER", "repository", "repository.id = issue.repo_id").
  602. Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id").
  603. Where("issue_id = ?", issue.ID).
  604. // sort by repo id then created date, with the issues of the same repo at the beginning of the list
  605. OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
  606. if opts.Page > 0 {
  607. sess = db.SetSessionPagination(sess, &opts)
  608. }
  609. err = sess.Find(&issueDeps)
  610. for _, depInfo := range issueDeps {
  611. depInfo.Issue.Repo = &depInfo.Repository
  612. }
  613. return issueDeps, err
  614. }
  615. // BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
  616. func (issue *Issue) BlockingDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) {
  617. err = db.GetEngine(ctx).
  618. Table("issue").
  619. Join("INNER", "repository", "repository.id = issue.repo_id").
  620. Join("INNER", "issue_dependency", "issue_dependency.issue_id = issue.id").
  621. Where("dependency_id = ?", issue.ID).
  622. // sort by repo id then created date, with the issues of the same repo at the beginning of the list
  623. OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID).
  624. Find(&issueDeps)
  625. for _, depInfo := range issueDeps {
  626. depInfo.Issue.Repo = &depInfo.Repository
  627. }
  628. return issueDeps, err
  629. }
  630. func migratedIssueCond(tp api.GitServiceType) builder.Cond {
  631. return builder.In("issue_id",
  632. builder.Select("issue.id").
  633. From("issue").
  634. InnerJoin("repository", "issue.repo_id = repository.id").
  635. Where(builder.Eq{
  636. "repository.original_service_type": tp,
  637. }),
  638. )
  639. }
  640. // RemapExternalUser ExternalUserRemappable interface
  641. func (issue *Issue) RemapExternalUser(externalName string, externalID, userID int64) error {
  642. issue.OriginalAuthor = externalName
  643. issue.OriginalAuthorID = externalID
  644. issue.PosterID = userID
  645. return nil
  646. }
  647. // GetUserID ExternalUserRemappable interface
  648. func (issue *Issue) GetUserID() int64 { return issue.PosterID }
  649. // GetExternalName ExternalUserRemappable interface
  650. func (issue *Issue) GetExternalName() string { return issue.OriginalAuthor }
  651. // GetExternalID ExternalUserRemappable interface
  652. func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID }
  653. // HasOriginalAuthor returns if an issue was migrated and has an original author.
  654. func (issue *Issue) HasOriginalAuthor() bool {
  655. return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
  656. }
  657. // InsertIssues insert issues to database
  658. func InsertIssues(ctx context.Context, issues ...*Issue) error {
  659. return db.WithTx(ctx, func(ctx context.Context) error {
  660. for _, issue := range issues {
  661. if err := insertIssue(ctx, issue); err != nil {
  662. return err
  663. }
  664. }
  665. return nil
  666. })
  667. }
  668. func insertIssue(ctx context.Context, issue *Issue) error {
  669. sess := db.GetEngine(ctx)
  670. if _, err := sess.NoAutoTime().Insert(issue); err != nil {
  671. return err
  672. }
  673. issueLabels := make([]IssueLabel, 0, len(issue.Labels))
  674. for _, label := range issue.Labels {
  675. issueLabels = append(issueLabels, IssueLabel{
  676. IssueID: issue.ID,
  677. LabelID: label.ID,
  678. })
  679. }
  680. if len(issueLabels) > 0 {
  681. if _, err := sess.Insert(issueLabels); err != nil {
  682. return err
  683. }
  684. }
  685. for _, reaction := range issue.Reactions {
  686. reaction.IssueID = issue.ID
  687. }
  688. if len(issue.Reactions) > 0 {
  689. if _, err := sess.Insert(issue.Reactions); err != nil {
  690. return err
  691. }
  692. }
  693. return nil
  694. }
  695. // ChangeIssueTimeEstimate changes the plan time of this issue, as the given user.
  696. func ChangeIssueTimeEstimate(ctx context.Context, issue *Issue, doer *user_model.User, timeEstimate int64) error {
  697. return db.WithTx(ctx, func(ctx context.Context) error {
  698. if err := UpdateIssueCols(ctx, &Issue{ID: issue.ID, TimeEstimate: timeEstimate}, "time_estimate"); err != nil {
  699. return fmt.Errorf("updateIssueCols: %w", err)
  700. }
  701. if err := issue.LoadRepo(ctx); err != nil {
  702. return fmt.Errorf("loadRepo: %w", err)
  703. }
  704. opts := &CreateCommentOptions{
  705. Type: CommentTypeChangeTimeEstimate,
  706. Doer: doer,
  707. Repo: issue.Repo,
  708. Issue: issue,
  709. Content: strconv.FormatInt(timeEstimate, 10),
  710. }
  711. if _, err := CreateComment(ctx, opts); err != nil {
  712. return fmt.Errorf("createComment: %w", err)
  713. }
  714. return nil
  715. })
  716. }