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

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