| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- // Copyright 2025 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package issues
-
- import (
- "context"
- "errors"
- "sort"
-
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- )
-
- type IssuePin struct {
- ID int64 `xorm:"pk autoincr"`
- RepoID int64 `xorm:"UNIQUE(s) NOT NULL"`
- IssueID int64 `xorm:"UNIQUE(s) NOT NULL"`
- IsPull bool `xorm:"NOT NULL"`
- PinOrder int `xorm:"DEFAULT 0"`
- }
-
- var ErrIssueMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned issues has been readched")
-
- // IsErrIssueMaxPinReached returns if the error is, that the User can't pin more Issues
- func IsErrIssueMaxPinReached(err error) bool {
- return err == ErrIssueMaxPinReached
- }
-
- func init() {
- db.RegisterModel(new(IssuePin))
- }
-
- func GetIssuePin(ctx context.Context, issue *Issue) (*IssuePin, error) {
- pin := new(IssuePin)
- has, err := db.GetEngine(ctx).
- Where("repo_id = ?", issue.RepoID).
- And("issue_id = ?", issue.ID).Get(pin)
- if err != nil {
- return nil, err
- } else if !has {
- return nil, db.ErrNotExist{
- Resource: "IssuePin",
- ID: issue.ID,
- }
- }
- return pin, nil
- }
-
- func GetIssuePinsByIssueIDs(ctx context.Context, issueIDs []int64) ([]IssuePin, error) {
- var pins []IssuePin
- if err := db.GetEngine(ctx).In("issue_id", issueIDs).Find(&pins); err != nil {
- return nil, err
- }
- return pins, nil
- }
-
- // Pin pins a Issue
- func PinIssue(ctx context.Context, issue *Issue, user *user_model.User) error {
- return db.WithTx(ctx, func(ctx context.Context) error {
- pinnedIssuesNum, err := getPinnedIssuesNum(ctx, issue.RepoID, issue.IsPull)
- if err != nil {
- return err
- }
-
- // Check if the maximum allowed Pins reached
- if pinnedIssuesNum >= setting.Repository.Issue.MaxPinned {
- return ErrIssueMaxPinReached
- }
-
- pinnedIssuesMaxPinOrder, err := getPinnedIssuesMaxPinOrder(ctx, issue.RepoID, issue.IsPull)
- if err != nil {
- return err
- }
-
- if _, err = db.GetEngine(ctx).Insert(&IssuePin{
- RepoID: issue.RepoID,
- IssueID: issue.ID,
- IsPull: issue.IsPull,
- PinOrder: pinnedIssuesMaxPinOrder + 1,
- }); err != nil {
- return err
- }
-
- // Add the pin event to the history
- _, err = CreateComment(ctx, &CreateCommentOptions{
- Type: CommentTypePin,
- Doer: user,
- Repo: issue.Repo,
- Issue: issue,
- })
- return err
- })
- }
-
- // UnpinIssue unpins a Issue
- func UnpinIssue(ctx context.Context, issue *Issue, user *user_model.User) error {
- return db.WithTx(ctx, func(ctx context.Context) error {
- // This sets the Pin for all Issues that come after the unpined Issue to the correct value
- cnt, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Delete(new(IssuePin))
- if err != nil {
- return err
- }
- if cnt == 0 {
- return nil
- }
-
- // Add the unpin event to the history
- _, err = CreateComment(ctx, &CreateCommentOptions{
- Type: CommentTypeUnpin,
- Doer: user,
- Repo: issue.Repo,
- Issue: issue,
- })
- return err
- })
- }
-
- func getPinnedIssuesNum(ctx context.Context, repoID int64, isPull bool) (int, error) {
- var pinnedIssuesNum int
- _, err := db.GetEngine(ctx).SQL("SELECT count(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&pinnedIssuesNum)
- return pinnedIssuesNum, err
- }
-
- func getPinnedIssuesMaxPinOrder(ctx context.Context, repoID int64, isPull bool) (int, error) {
- var maxPinnedIssuesMaxPinOrder int
- _, err := db.GetEngine(ctx).SQL("SELECT max(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&maxPinnedIssuesMaxPinOrder)
- return maxPinnedIssuesMaxPinOrder, err
- }
-
- // MovePin moves a Pinned Issue to a new Position
- func MovePin(ctx context.Context, issue *Issue, newPosition int) error {
- if newPosition < 1 {
- return errors.New("The Position can't be lower than 1")
- }
-
- issuePin, err := GetIssuePin(ctx, issue)
- if err != nil {
- return err
- }
- if issuePin.PinOrder == newPosition {
- return nil
- }
-
- return db.WithTx(ctx, func(ctx context.Context) error {
- if issuePin.PinOrder > newPosition { // move the issue to a lower position
- _, 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)
- } else { // move the issue to a higher position
- // Lower the Position of all Pinned Issue that came after the current Position
- _, 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)
- }
- if err != nil {
- return err
- }
-
- _, err = db.GetEngine(ctx).
- Table("issue_pin").
- Where("id = ?", issuePin.ID).
- Update(map[string]any{
- "pin_order": newPosition,
- })
- return err
- })
- }
-
- func GetPinnedIssueIDs(ctx context.Context, repoID int64, isPull bool) ([]int64, error) {
- var issuePins []IssuePin
- if err := db.GetEngine(ctx).
- Table("issue_pin").
- Where("repo_id = ?", repoID).
- And("is_pull = ?", isPull).
- Find(&issuePins); err != nil {
- return nil, err
- }
-
- sort.Slice(issuePins, func(i, j int) bool {
- return issuePins[i].PinOrder < issuePins[j].PinOrder
- })
-
- var ids []int64
- for _, pin := range issuePins {
- ids = append(ids, pin.IssueID)
- }
- return ids, nil
- }
-
- func GetIssuePinsByRepoID(ctx context.Context, repoID int64, isPull bool) ([]*IssuePin, error) {
- var pins []*IssuePin
- if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil {
- return nil, err
- }
- return pins, nil
- }
-
- // GetPinnedIssues returns the pinned Issues for the given Repo and type
- func GetPinnedIssues(ctx context.Context, repoID int64, isPull bool) (IssueList, error) {
- issuePins, err := GetIssuePinsByRepoID(ctx, repoID, isPull)
- if err != nil {
- return nil, err
- }
- if len(issuePins) == 0 {
- return IssueList{}, nil
- }
- ids := make([]int64, 0, len(issuePins))
- for _, pin := range issuePins {
- ids = append(ids, pin.IssueID)
- }
-
- issues := make(IssueList, 0, len(ids))
- if err := db.GetEngine(ctx).In("id", ids).Find(&issues); err != nil {
- return nil, err
- }
- for _, issue := range issues {
- for _, pin := range issuePins {
- if pin.IssueID == issue.ID {
- issue.PinOrder = pin.PinOrder
- break
- }
- }
- if (!setting.IsProd || setting.IsInTesting) && issue.PinOrder == 0 {
- panic("It should not happen that a pinned Issue has no PinOrder")
- }
- }
- sort.Slice(issues, func(i, j int) bool {
- return issues[i].PinOrder < issues[j].PinOrder
- })
-
- if err = issues.LoadAttributes(ctx); err != nil {
- return nil, err
- }
-
- return issues, nil
- }
-
- // IsNewPinAllowed returns if a new Issue or Pull request can be pinned
- func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, error) {
- var maxPin int
- _, err := db.GetEngine(ctx).SQL("SELECT COUNT(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&maxPin)
- if err != nil {
- return false, err
- }
-
- return maxPin < setting.Repository.Issue.MaxPinned, nil
- }
|