gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. // Copyright 2025 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "errors"
  7. "sort"
  8. "code.gitea.io/gitea/models/db"
  9. user_model "code.gitea.io/gitea/models/user"
  10. "code.gitea.io/gitea/modules/setting"
  11. "code.gitea.io/gitea/modules/util"
  12. )
  13. type IssuePin struct {
  14. ID int64 `xorm:"pk autoincr"`
  15. RepoID int64 `xorm:"UNIQUE(s) NOT NULL"`
  16. IssueID int64 `xorm:"UNIQUE(s) NOT NULL"`
  17. IsPull bool `xorm:"NOT NULL"`
  18. PinOrder int `xorm:"DEFAULT 0"`
  19. }
  20. var ErrIssueMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned issues has been readched")
  21. // IsErrIssueMaxPinReached returns if the error is, that the User can't pin more Issues
  22. func IsErrIssueMaxPinReached(err error) bool {
  23. return err == ErrIssueMaxPinReached
  24. }
  25. func init() {
  26. db.RegisterModel(new(IssuePin))
  27. }
  28. func GetIssuePin(ctx context.Context, issue *Issue) (*IssuePin, error) {
  29. pin := new(IssuePin)
  30. has, err := db.GetEngine(ctx).
  31. Where("repo_id = ?", issue.RepoID).
  32. And("issue_id = ?", issue.ID).Get(pin)
  33. if err != nil {
  34. return nil, err
  35. } else if !has {
  36. return nil, db.ErrNotExist{
  37. Resource: "IssuePin",
  38. ID: issue.ID,
  39. }
  40. }
  41. return pin, nil
  42. }
  43. func GetIssuePinsByIssueIDs(ctx context.Context, issueIDs []int64) ([]IssuePin, error) {
  44. var pins []IssuePin
  45. if err := db.GetEngine(ctx).In("issue_id", issueIDs).Find(&pins); err != nil {
  46. return nil, err
  47. }
  48. return pins, nil
  49. }
  50. // Pin pins a Issue
  51. func PinIssue(ctx context.Context, issue *Issue, user *user_model.User) error {
  52. return db.WithTx(ctx, func(ctx context.Context) error {
  53. pinnedIssuesNum, err := getPinnedIssuesNum(ctx, issue.RepoID, issue.IsPull)
  54. if err != nil {
  55. return err
  56. }
  57. // Check if the maximum allowed Pins reached
  58. if pinnedIssuesNum >= setting.Repository.Issue.MaxPinned {
  59. return ErrIssueMaxPinReached
  60. }
  61. pinnedIssuesMaxPinOrder, err := getPinnedIssuesMaxPinOrder(ctx, issue.RepoID, issue.IsPull)
  62. if err != nil {
  63. return err
  64. }
  65. if _, err = db.GetEngine(ctx).Insert(&IssuePin{
  66. RepoID: issue.RepoID,
  67. IssueID: issue.ID,
  68. IsPull: issue.IsPull,
  69. PinOrder: pinnedIssuesMaxPinOrder + 1,
  70. }); err != nil {
  71. return err
  72. }
  73. // Add the pin event to the history
  74. _, err = CreateComment(ctx, &CreateCommentOptions{
  75. Type: CommentTypePin,
  76. Doer: user,
  77. Repo: issue.Repo,
  78. Issue: issue,
  79. })
  80. return err
  81. })
  82. }
  83. // UnpinIssue unpins a Issue
  84. func UnpinIssue(ctx context.Context, issue *Issue, user *user_model.User) error {
  85. return db.WithTx(ctx, func(ctx context.Context) error {
  86. // This sets the Pin for all Issues that come after the unpined Issue to the correct value
  87. cnt, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Delete(new(IssuePin))
  88. if err != nil {
  89. return err
  90. }
  91. if cnt == 0 {
  92. return nil
  93. }
  94. // Add the unpin event to the history
  95. _, err = CreateComment(ctx, &CreateCommentOptions{
  96. Type: CommentTypeUnpin,
  97. Doer: user,
  98. Repo: issue.Repo,
  99. Issue: issue,
  100. })
  101. return err
  102. })
  103. }
  104. func getPinnedIssuesNum(ctx context.Context, repoID int64, isPull bool) (int, error) {
  105. var pinnedIssuesNum int
  106. _, err := db.GetEngine(ctx).SQL("SELECT count(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&pinnedIssuesNum)
  107. return pinnedIssuesNum, err
  108. }
  109. func getPinnedIssuesMaxPinOrder(ctx context.Context, repoID int64, isPull bool) (int, error) {
  110. var maxPinnedIssuesMaxPinOrder int
  111. _, err := db.GetEngine(ctx).SQL("SELECT max(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&maxPinnedIssuesMaxPinOrder)
  112. return maxPinnedIssuesMaxPinOrder, err
  113. }
  114. // MovePin moves a Pinned Issue to a new Position
  115. func MovePin(ctx context.Context, issue *Issue, newPosition int) error {
  116. if newPosition < 1 {
  117. return errors.New("The Position can't be lower than 1")
  118. }
  119. issuePin, err := GetIssuePin(ctx, issue)
  120. if err != nil {
  121. return err
  122. }
  123. if issuePin.PinOrder == newPosition {
  124. return nil
  125. }
  126. return db.WithTx(ctx, func(ctx context.Context) error {
  127. if issuePin.PinOrder > newPosition { // move the issue to a lower position
  128. _, err = db.GetEngine(ctx).Exec("UPDATE issue_pin SET pin_order = pin_order + 1 WHERE repo_id = ? AND is_pull = ? AND pin_order >= ? AND pin_order < ?", issue.RepoID, issue.IsPull, newPosition, issuePin.PinOrder)
  129. } else { // move the issue to a higher position
  130. // Lower the Position of all Pinned Issue that came after the current Position
  131. _, err = db.GetEngine(ctx).Exec("UPDATE issue_pin SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ? AND pin_order <= ?", issue.RepoID, issue.IsPull, issuePin.PinOrder, newPosition)
  132. }
  133. if err != nil {
  134. return err
  135. }
  136. _, err = db.GetEngine(ctx).
  137. Table("issue_pin").
  138. Where("id = ?", issuePin.ID).
  139. Update(map[string]any{
  140. "pin_order": newPosition,
  141. })
  142. return err
  143. })
  144. }
  145. func GetPinnedIssueIDs(ctx context.Context, repoID int64, isPull bool) ([]int64, error) {
  146. var issuePins []IssuePin
  147. if err := db.GetEngine(ctx).
  148. Table("issue_pin").
  149. Where("repo_id = ?", repoID).
  150. And("is_pull = ?", isPull).
  151. Find(&issuePins); err != nil {
  152. return nil, err
  153. }
  154. sort.Slice(issuePins, func(i, j int) bool {
  155. return issuePins[i].PinOrder < issuePins[j].PinOrder
  156. })
  157. var ids []int64
  158. for _, pin := range issuePins {
  159. ids = append(ids, pin.IssueID)
  160. }
  161. return ids, nil
  162. }
  163. func GetIssuePinsByRepoID(ctx context.Context, repoID int64, isPull bool) ([]*IssuePin, error) {
  164. var pins []*IssuePin
  165. if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil {
  166. return nil, err
  167. }
  168. return pins, nil
  169. }
  170. // GetPinnedIssues returns the pinned Issues for the given Repo and type
  171. func GetPinnedIssues(ctx context.Context, repoID int64, isPull bool) (IssueList, error) {
  172. issuePins, err := GetIssuePinsByRepoID(ctx, repoID, isPull)
  173. if err != nil {
  174. return nil, err
  175. }
  176. if len(issuePins) == 0 {
  177. return IssueList{}, nil
  178. }
  179. ids := make([]int64, 0, len(issuePins))
  180. for _, pin := range issuePins {
  181. ids = append(ids, pin.IssueID)
  182. }
  183. issues := make(IssueList, 0, len(ids))
  184. if err := db.GetEngine(ctx).In("id", ids).Find(&issues); err != nil {
  185. return nil, err
  186. }
  187. for _, issue := range issues {
  188. for _, pin := range issuePins {
  189. if pin.IssueID == issue.ID {
  190. issue.PinOrder = pin.PinOrder
  191. break
  192. }
  193. }
  194. if (!setting.IsProd || setting.IsInTesting) && issue.PinOrder == 0 {
  195. panic("It should not happen that a pinned Issue has no PinOrder")
  196. }
  197. }
  198. sort.Slice(issues, func(i, j int) bool {
  199. return issues[i].PinOrder < issues[j].PinOrder
  200. })
  201. if err = issues.LoadAttributes(ctx); err != nil {
  202. return nil, err
  203. }
  204. return issues, nil
  205. }
  206. // IsNewPinAllowed returns if a new Issue or Pull request can be pinned
  207. func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, error) {
  208. var maxPin int
  209. _, err := db.GetEngine(ctx).SQL("SELECT COUNT(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&maxPin)
  210. if err != nil {
  211. return false, err
  212. }
  213. return maxPin < setting.Repository.Issue.MaxPinned, nil
  214. }