mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
226 lines
6.5 KiB
Go
226 lines
6.5 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package issues
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sort"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/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 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
|
|
}
|