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.go 26KB

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