2014-03-20 16:04:56 -04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2020-01-01 23:51:10 +01:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2014-03-20 16:04:56 -04:00
2022-06-13 17:37:59 +08:00
package issues
2014-03-20 16:04:56 -04:00
2014-03-22 13:50:50 -04:00
import (
2021-09-23 16:45:36 +01:00
"context"
2015-08-10 14:42:50 +08:00
"fmt"
2018-01-03 09:34:13 +01:00
"regexp"
2023-09-07 17:37:47 +08:00
"slices"
2014-05-07 16:51:14 -04:00
2021-09-19 19:49:59 +08:00
"code.gitea.io/gitea/models/db"
2022-03-29 22:16:31 +08:00
project_model "code.gitea.io/gitea/models/project"
2021-11-19 21:39:57 +08:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 17:49:20 +08:00
user_model "code.gitea.io/gitea/models/user"
2023-07-31 14:28:53 +08:00
"code.gitea.io/gitea/modules/container"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/log"
2023-05-25 15:17:19 +02:00
"code.gitea.io/gitea/modules/setting"
2019-05-11 18:21:34 +08:00
api "code.gitea.io/gitea/modules/structs"
2019-08-15 22:46:21 +08:00
"code.gitea.io/gitea/modules/timeutil"
2017-01-24 21:43:02 -05:00
"code.gitea.io/gitea/modules/util"
2017-12-25 18:25:16 -05:00
2019-06-23 23:22:43 +08:00
"xorm.io/builder"
2014-03-22 13:50:50 -04:00
)
2022-06-13 17:37:59 +08:00
// ErrIssueNotExist represents a "IssueNotExist" kind of error.
type ErrIssueNotExist struct {
ID int64
RepoID int64
Index int64
}
// IsErrIssueNotExist checks if an error is a ErrIssueNotExist.
func IsErrIssueNotExist ( err error ) bool {
_ , ok := err . ( ErrIssueNotExist )
return ok
}
func ( err ErrIssueNotExist ) Error ( ) string {
return fmt . Sprintf ( "issue does not exist [id: %d, repo_id: %d, index: %d]" , err . ID , err . RepoID , err . Index )
}
2022-10-18 06:50:37 +01:00
func ( err ErrIssueNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2022-06-13 17:37:59 +08:00
// ErrIssueIsClosed represents a "IssueIsClosed" kind of error.
type ErrIssueIsClosed struct {
ID int64
RepoID int64
Index int64
}
// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist.
func IsErrIssueIsClosed ( err error ) bool {
_ , ok := err . ( ErrIssueIsClosed )
return ok
}
func ( err ErrIssueIsClosed ) Error ( ) string {
return fmt . Sprintf ( "issue is closed [id: %d, repo_id: %d, index: %d]" , err . ID , err . RepoID , err . Index )
}
// ErrNewIssueInsert is used when the INSERT statement in newIssue fails
type ErrNewIssueInsert struct {
OriginalError error
}
// IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert.
func IsErrNewIssueInsert ( err error ) bool {
_ , ok := err . ( ErrNewIssueInsert )
return ok
}
func ( err ErrNewIssueInsert ) Error ( ) string {
return err . OriginalError . Error ( )
}
// ErrIssueWasClosed is used when close a closed issue
type ErrIssueWasClosed struct {
ID int64
Index int64
}
// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed.
func IsErrIssueWasClosed ( err error ) bool {
_ , ok := err . ( ErrIssueWasClosed )
return ok
}
func ( err ErrIssueWasClosed ) Error ( ) string {
return fmt . Sprintf ( "Issue [%d] %d was already closed" , err . ID , err . Index )
}
2014-03-22 13:50:50 -04:00
// Issue represents an issue or pull request of repository.
2014-03-20 16:04:56 -04:00
type Issue struct {
2021-12-10 09:27:50 +08:00
ID int64 ` xorm:"pk autoincr" `
RepoID int64 ` xorm:"INDEX UNIQUE(repo_index)" `
Repo * repo_model . Repository ` xorm:"-" `
Index int64 ` xorm:"UNIQUE(repo_index)" ` // Index in one repository.
PosterID int64 ` xorm:"INDEX" `
Poster * user_model . User ` xorm:"-" `
2019-07-07 22:14:12 -04:00
OriginalAuthor string
2022-06-13 17:37:59 +08:00
OriginalAuthorID int64 ` xorm:"index" `
Title string ` xorm:"name" `
Content string ` xorm:"LONGTEXT" `
RenderedContent string ` xorm:"-" `
Labels [ ] * Label ` xorm:"-" `
MilestoneID int64 ` xorm:"INDEX" `
Milestone * Milestone ` xorm:"-" `
Project * project_model . Project ` xorm:"-" `
2019-07-07 22:14:12 -04:00
Priority int
2021-11-24 17:49:20 +08:00
AssigneeID int64 ` xorm:"-" `
Assignee * user_model . User ` xorm:"-" `
IsClosed bool ` xorm:"INDEX" `
IsRead bool ` xorm:"-" `
IsPull bool ` xorm:"INDEX" ` // Indicates whether is a pull request or not.
PullRequest * PullRequest ` xorm:"-" `
2019-07-07 22:14:12 -04:00
NumComments int
Ref string
2023-05-25 15:17:19 +02:00
PinOrder int ` xorm:"DEFAULT 0" `
2016-03-09 19:53:30 -05:00
2019-08-15 22:46:21 +08:00
DeadlineUnix timeutil . TimeStamp ` xorm:"INDEX" `
2018-05-01 21:05:28 +02:00
2019-08-15 22:46:21 +08:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
ClosedUnix timeutil . TimeStamp ` xorm:"INDEX" `
2015-08-12 17:04:23 +08:00
2022-12-23 19:35:43 +08:00
Attachments [ ] * repo_model . Attachment ` xorm:"-" `
2023-05-21 20:48:28 +08:00
Comments CommentList ` xorm:"-" `
2022-12-23 19:35:43 +08:00
Reactions ReactionList ` xorm:"-" `
TotalTrackedTime int64 ` xorm:"-" `
Assignees [ ] * user_model . User ` xorm:"-" `
2019-02-18 21:55:04 +01:00
// IsLocked limits commenting abilities to users on an issue
// with write access
IsLocked bool ` xorm:"NOT NULL DEFAULT false" `
2020-09-11 02:09:14 +08:00
// For view issue page.
2021-11-11 07:29:30 +01:00
ShowRole RoleDescriptor ` xorm:"-" `
2015-08-12 17:04:23 +08:00
}
2018-01-03 09:34:13 +01:00
var (
issueTasksPat * regexp . Regexp
issueTasksDonePat * regexp . Regexp
)
2021-03-15 02:52:12 +08:00
const (
2021-06-14 10:22:55 +08:00
issueTasksRegexpStr = ` (^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.) `
issueTasksDoneRegexpStr = ` (^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.) `
2021-03-15 02:52:12 +08:00
)
2018-01-03 09:34:13 +01:00
2021-09-19 19:49:59 +08:00
// IssueIndex represents the issue index table
type IssueIndex db . ResourceIndex
2018-01-03 09:34:13 +01:00
func init ( ) {
issueTasksPat = regexp . MustCompile ( issueTasksRegexpStr )
issueTasksDonePat = regexp . MustCompile ( issueTasksDoneRegexpStr )
2021-09-19 19:49:59 +08:00
db . RegisterModel ( new ( Issue ) )
db . RegisterModel ( new ( IssueIndex ) )
2018-01-03 09:34:13 +01:00
}
2022-06-13 17:37:59 +08:00
// LoadTotalTimes load total tracked time
func ( issue * Issue ) LoadTotalTimes ( ctx context . Context ) ( err error ) {
2018-04-29 07:58:47 +02:00
opts := FindTrackedTimesOptions { IssueID : issue . ID }
2022-05-20 22:08:52 +08:00
issue . TotalTrackedTime , err = opts . toSession ( db . GetEngine ( ctx ) ) . SumInt ( & TrackedTime { } , "time" )
2018-04-29 07:58:47 +02:00
if err != nil {
return err
}
return nil
}
2018-05-01 21:05:28 +02:00
// IsOverdue checks if the issue is overdue
func ( issue * Issue ) IsOverdue ( ) bool {
2021-03-08 01:55:57 +00:00
if issue . IsClosed {
return issue . ClosedUnix >= issue . DeadlineUnix
}
2019-08-15 22:46:21 +08:00
return timeutil . TimeStampNow ( ) >= issue . DeadlineUnix
2018-05-01 21:05:28 +02:00
}
2018-12-13 23:55:43 +08:00
// LoadRepo loads issue's repository
2022-04-08 17:11:15 +08:00
func ( issue * Issue ) LoadRepo ( ctx context . Context ) ( err error ) {
2023-03-28 19:23:25 +02:00
if issue . Repo == nil && issue . RepoID != 0 {
2022-12-03 10:48:26 +08:00
issue . Repo , err = repo_model . GetRepositoryByID ( ctx , issue . RepoID )
2016-03-13 23:20:22 -04:00
if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getRepositoryByID [%d]: %w" , issue . RepoID , err )
2016-03-13 23:20:22 -04:00
}
2016-08-26 13:40:53 -07:00
}
2016-12-17 19:49:17 +08:00
return nil
}
2018-04-29 07:58:47 +02:00
// IsTimetrackerEnabled returns true if the repo enables timetracking
2022-12-10 10:46:31 +08:00
func ( issue * Issue ) IsTimetrackerEnabled ( ctx context . Context ) bool {
2022-04-08 17:11:15 +08:00
if err := issue . LoadRepo ( ctx ) ; err != nil {
2019-04-02 08:48:31 +01:00
log . Error ( fmt . Sprintf ( "loadRepo: %v" , err ) )
2018-04-29 07:58:47 +02:00
return false
}
2022-12-10 10:46:31 +08:00
return issue . Repo . IsTimetrackerEnabled ( ctx )
2018-04-29 07:58:47 +02:00
}
2017-01-28 14:01:07 -02:00
// GetPullRequest returns the issue pull request
func ( issue * Issue ) GetPullRequest ( ) ( pr * PullRequest , err error ) {
if ! issue . IsPull {
return nil , fmt . Errorf ( "Issue is not a pull request" )
}
2022-05-20 22:08:52 +08:00
pr , err = GetPullRequestByIssueID ( db . DefaultContext , issue . ID )
2018-10-18 19:23:05 +08:00
if err != nil {
return nil , err
}
pr . Issue = issue
2022-06-20 12:02:49 +02:00
return pr , err
2017-01-28 14:01:07 -02:00
}
2018-12-13 23:55:43 +08:00
// LoadPoster loads poster
2022-11-19 09:12:33 +01:00
func ( issue * Issue ) LoadPoster ( ctx context . Context ) ( err error ) {
2023-03-28 19:23:25 +02:00
if issue . Poster == nil && issue . PosterID != 0 {
2023-01-31 09:45:19 +08:00
issue . Poster , err = user_model . GetPossibleUserByID ( ctx , issue . PosterID )
2016-03-13 23:20:22 -04:00
if err != nil {
2016-09-20 17:54:47 +08:00
issue . PosterID = - 1
2021-11-24 17:49:20 +08:00
issue . Poster = user_model . NewGhostUser ( )
if ! user_model . IsErrUserNotExist ( err ) {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getUserByID.(poster) [%d]: %w" , issue . PosterID , err )
2016-03-13 23:20:22 -04:00
}
2023-07-09 13:58:06 +02:00
return nil
2016-03-13 23:20:22 -04:00
}
2016-08-26 13:40:53 -07:00
}
2022-06-20 12:02:49 +02:00
return err
2017-01-30 20:46:45 +08:00
}
2016-03-13 23:20:22 -04:00
2022-11-19 09:12:33 +01:00
// LoadPullRequest loads pull request info
func ( issue * Issue ) LoadPullRequest ( ctx context . Context ) ( err error ) {
2023-02-21 00:15:49 +00:00
if issue . IsPull {
2023-03-28 19:23:25 +02:00
if issue . PullRequest == nil && issue . ID != 0 {
2023-02-21 00:15:49 +00:00
issue . PullRequest , err = GetPullRequestByIssueID ( ctx , issue . ID )
if err != nil {
if IsErrPullRequestNotExist ( err ) {
return err
}
return fmt . Errorf ( "getPullRequestByIssueID [%d]: %w" , issue . ID , err )
2017-07-26 00:16:45 -07:00
}
}
2023-03-28 19:23:25 +02:00
if issue . PullRequest != nil {
issue . PullRequest . Issue = issue
}
2017-07-26 00:16:45 -07:00
}
return nil
}
2022-05-20 22:08:52 +08:00
func ( issue * Issue ) loadComments ( ctx context . Context ) ( err error ) {
2023-04-20 14:39:44 +08:00
return issue . loadCommentsByType ( ctx , CommentTypeUndefined )
2019-02-19 22:39:39 +08:00
}
// LoadDiscussComments loads discuss comments
2022-11-19 09:12:33 +01:00
func ( issue * Issue ) LoadDiscussComments ( ctx context . Context ) error {
return issue . loadCommentsByType ( ctx , CommentTypeComment )
2019-02-19 22:39:39 +08:00
}
2022-05-20 22:08:52 +08:00
func ( issue * Issue ) loadCommentsByType ( ctx context . Context , tp CommentType ) ( err error ) {
2017-09-16 13:16:21 -07:00
if issue . Comments != nil {
return nil
}
2022-05-20 22:08:52 +08:00
issue . Comments , err = FindComments ( ctx , & FindCommentsOptions {
2017-09-16 13:16:21 -07:00
IssueID : issue . ID ,
2019-02-19 22:39:39 +08:00
Type : tp ,
2017-09-16 13:16:21 -07:00
} )
return err
}
2021-12-10 09:27:50 +08:00
func ( issue * Issue ) loadReactions ( ctx context . Context ) ( err error ) {
2017-12-04 01:14:26 +02:00
if issue . Reactions != nil {
return nil
}
2022-06-13 17:37:59 +08:00
reactions , _ , err := FindReactions ( ctx , FindReactionsOptions {
2017-12-04 01:14:26 +02:00
IssueID : issue . ID ,
} )
if err != nil {
return err
}
2022-04-08 17:11:15 +08:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2020-01-15 19:14:07 +08:00
return err
}
2017-12-04 01:14:26 +02:00
// Load reaction user data
2022-06-20 12:02:49 +02:00
if _ , err := reactions . LoadUsers ( ctx , issue . Repo ) ; err != nil {
2017-12-04 01:14:26 +02:00
return err
}
// Cache comments to map
comments := make ( map [ int64 ] * Comment )
for _ , comment := range issue . Comments {
comments [ comment . ID ] = comment
}
// Add reactions either to issue or comment
for _ , react := range reactions {
if react . CommentID == 0 {
issue . Reactions = append ( issue . Reactions , react )
} else if comment , ok := comments [ react . CommentID ] ; ok {
comment . Reactions = append ( comment . Reactions , react )
}
}
return nil
}
2022-11-19 09:12:33 +01:00
// LoadMilestone load milestone of this issue.
func ( issue * Issue ) LoadMilestone ( ctx context . Context ) ( err error ) {
2020-05-24 22:38:34 +08:00
if ( issue . Milestone == nil || issue . Milestone . ID != issue . MilestoneID ) && issue . MilestoneID > 0 {
2022-06-13 17:37:59 +08:00
issue . Milestone , err = GetMilestoneByRepoID ( ctx , issue . RepoID , issue . MilestoneID )
if err != nil && ! IsErrMilestoneNotExist ( err ) {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w" , issue . RepoID , issue . MilestoneID , err )
2020-01-01 23:51:10 +01:00
}
}
return nil
}
2022-06-13 17:37:59 +08:00
// LoadAttributes loads the attribute of this issue.
func ( issue * Issue ) LoadAttributes ( ctx context . Context ) ( err error ) {
2022-04-08 17:11:15 +08:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2023-07-09 13:58:06 +02:00
return err
2017-01-30 20:46:45 +08:00
}
2022-11-19 09:12:33 +01:00
if err = issue . LoadPoster ( ctx ) ; err != nil {
2023-07-09 13:58:06 +02:00
return err
2017-01-30 20:46:45 +08:00
}
2022-04-28 13:48:48 +02:00
if err = issue . LoadLabels ( ctx ) ; err != nil {
2023-07-09 13:58:06 +02:00
return err
2016-08-26 13:40:53 -07:00
}
2015-08-10 21:47:23 +08:00
2022-11-19 09:12:33 +01:00
if err = issue . LoadMilestone ( ctx ) ; err != nil {
2023-07-09 13:58:06 +02:00
return err
2015-08-05 20:23:08 +08:00
}
2023-02-21 04:21:56 +09:00
if err = issue . LoadProject ( ctx ) ; err != nil {
2023-07-09 13:58:06 +02:00
return err
2020-08-17 04:07:38 +01:00
}
2022-05-20 22:08:52 +08:00
if err = issue . LoadAssignees ( ctx ) ; err != nil {
2023-07-09 13:58:06 +02:00
return err
2016-08-14 03:32:24 -07:00
}
2022-11-19 09:12:33 +01:00
if err = issue . LoadPullRequest ( ctx ) ; err != nil && ! IsErrPullRequestNotExist ( err ) {
2016-08-14 03:32:24 -07:00
// It is possible pull request is not yet created.
2017-07-26 00:16:45 -07:00
return err
2016-08-14 03:32:24 -07:00
}
2016-08-26 13:40:53 -07:00
if issue . Attachments == nil {
2022-05-20 22:08:52 +08:00
issue . Attachments , err = repo_model . GetAttachmentsByIssueID ( ctx , issue . ID )
2016-08-26 13:40:53 -07:00
if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getAttachmentsByIssueID [%d]: %w" , issue . ID , err )
2016-08-26 13:40:53 -07:00
}
}
2022-05-20 22:08:52 +08:00
if err = issue . loadComments ( ctx ) ; err != nil {
2017-12-04 01:14:26 +02:00
return err
2016-08-26 13:40:53 -07:00
}
2019-04-18 13:00:03 +08:00
2023-07-22 22:14:27 +08:00
if err = issue . Comments . LoadAttributes ( ctx ) ; err != nil {
2019-04-18 13:00:03 +08:00
return err
}
2022-12-10 10:46:31 +08:00
if issue . IsTimetrackerEnabled ( ctx ) {
2022-06-13 17:37:59 +08:00
if err = issue . LoadTotalTimes ( ctx ) ; err != nil {
2018-04-29 07:58:47 +02:00
return err
}
}
2016-08-26 13:40:53 -07:00
2021-12-10 09:27:50 +08:00
return issue . loadReactions ( ctx )
2016-08-14 03:32:24 -07:00
}
2017-02-03 02:22:39 -05:00
// GetIsRead load the `IsRead` field of the issue
func ( issue * Issue ) GetIsRead ( userID int64 ) error {
issueUser := & IssueUser { IssueID : issue . ID , UID : userID }
2021-09-23 16:45:36 +01:00
if has , err := db . GetEngine ( db . DefaultContext ) . Get ( issueUser ) ; err != nil {
2017-02-03 02:22:39 -05:00
return err
} else if ! has {
2017-02-08 22:47:24 -05:00
issue . IsRead = false
return nil
2017-02-03 02:22:39 -05:00
}
issue . IsRead = issueUser . IsRead
return nil
}
2017-03-03 22:35:42 +08:00
// APIURL returns the absolute APIURL to this issue.
func ( issue * Issue ) APIURL ( ) string {
2020-04-21 15:48:53 +02:00
if issue . Repo == nil {
2022-04-08 17:11:15 +08:00
err := issue . LoadRepo ( db . DefaultContext )
2020-04-21 15:48:53 +02:00
if err != nil {
log . Error ( "Issue[%d].APIURL(): %v" , issue . ID , err )
return ""
}
}
2020-01-14 16:37:19 +01:00
return fmt . Sprintf ( "%s/issues/%d" , issue . Repo . APIURL ( ) , issue . Index )
2017-03-03 22:35:42 +08:00
}
2016-11-24 09:41:11 +01:00
// HTMLURL returns the absolute URL to this issue.
2016-08-16 10:19:09 -07:00
func ( issue * Issue ) HTMLURL ( ) string {
var path string
if issue . IsPull {
path = "pulls"
} else {
path = "issues"
}
return fmt . Sprintf ( "%s/%s/%d" , issue . Repo . HTMLURL ( ) , path , issue . Index )
}
2023-02-07 02:09:18 +08:00
// Link returns the issue's relative URL.
2021-11-16 18:18:25 +00:00
func ( issue * Issue ) Link ( ) string {
var path string
if issue . IsPull {
path = "pulls"
} else {
path = "issues"
}
return fmt . Sprintf ( "%s/%s/%d" , issue . Repo . Link ( ) , path , issue . Index )
}
2016-12-02 12:10:39 +01:00
// DiffURL returns the absolute URL to this diff
func ( issue * Issue ) DiffURL ( ) string {
if issue . IsPull {
return fmt . Sprintf ( "%s/pulls/%d.diff" , issue . Repo . HTMLURL ( ) , issue . Index )
}
return ""
}
// PatchURL returns the absolute URL to this patch
func ( issue * Issue ) PatchURL ( ) string {
if issue . IsPull {
return fmt . Sprintf ( "%s/pulls/%d.patch" , issue . Repo . HTMLURL ( ) , issue . Index )
}
return ""
}
2016-03-13 23:20:22 -04:00
// State returns string representation of issue status.
2016-11-22 12:24:39 +01:00
func ( issue * Issue ) State ( ) api . StateType {
if issue . IsClosed {
2016-11-29 09:25:47 +01:00
return api . StateClosed
2016-03-13 23:20:22 -04:00
}
2016-11-29 09:25:47 +01:00
return api . StateOpen
2016-08-14 03:32:24 -07:00
}
// HashTag returns unique hash tag for issue.
2016-11-22 12:24:39 +01:00
func ( issue * Issue ) HashTag ( ) string {
2020-12-25 09:59:32 +00:00
return fmt . Sprintf ( "issue-%d" , issue . ID )
2016-03-13 23:20:22 -04:00
}
2015-08-13 16:07:11 +08:00
// IsPoster returns true if given user by ID is the poster.
2016-11-22 12:24:39 +01:00
func ( issue * Issue ) IsPoster ( uid int64 ) bool {
2020-01-17 18:23:46 +08:00
return issue . OriginalAuthorID == 0 && issue . PosterID == uid
2015-08-13 16:07:11 +08:00
}
2018-01-03 09:34:13 +01:00
// GetTasks returns the amount of tasks in the issues content
func ( issue * Issue ) GetTasks ( ) int {
return len ( issueTasksPat . FindAllStringIndex ( issue . Content , - 1 ) )
}
// GetTasksDone returns the amount of completed tasks in the issues content
func ( issue * Issue ) GetTasksDone ( ) int {
return len ( issueTasksDonePat . FindAllStringIndex ( issue . Content , - 1 ) )
}
2023-05-18 18:45:25 +08:00
// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close.
func ( issue * Issue ) GetLastEventTimestamp ( ) timeutil . TimeStamp {
if issue . IsClosed {
return issue . ClosedUnix
2015-09-02 16:18:09 -04:00
}
2023-05-18 18:45:25 +08:00
return issue . CreatedUnix
}
2015-09-02 16:18:09 -04:00
2023-05-18 18:45:25 +08:00
// GetLastEventLabel returns the localization label for the current issue.
func ( issue * Issue ) GetLastEventLabel ( ) string {
if issue . IsClosed {
if issue . IsPull && issue . PullRequest . HasMerged {
return "repo.pulls.merged_by"
}
return "repo.issues.closed_by"
2015-08-25 22:58:34 +08:00
}
2023-05-18 18:45:25 +08:00
return "repo.issues.opened_by"
2015-08-25 22:58:34 +08:00
}
2023-05-18 18:45:25 +08:00
// GetLastComment return last comment for the current issue.
func ( issue * Issue ) GetLastComment ( ) ( * Comment , error ) {
var c Comment
exist , err := db . GetEngine ( db . DefaultContext ) . Where ( "type = ?" , CommentTypeComment ) .
And ( "issue_id = ?" , issue . ID ) . Desc ( "created_unix" ) . Get ( & c )
2019-02-21 13:01:28 +08:00
if err != nil {
2023-05-18 18:45:25 +08:00
return nil , err
2019-02-21 13:01:28 +08:00
}
2023-05-18 18:45:25 +08:00
if ! exist {
return nil , nil
2021-03-06 23:11:12 +08:00
}
2023-05-18 18:45:25 +08:00
return & c , nil
}
2019-02-21 13:01:28 +08:00
2023-05-18 18:45:25 +08:00
// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
func ( issue * Issue ) GetLastEventLabelFake ( ) string {
if issue . IsClosed {
if issue . IsPull && issue . PullRequest . HasMerged {
return "repo.pulls.merged_by_fake"
}
return "repo.issues.closed_by_fake"
2019-02-21 13:01:28 +08:00
}
2023-05-18 18:45:25 +08:00
return "repo.issues.opened_by_fake"
2019-02-21 13:01:28 +08:00
}
2023-05-18 18:45:25 +08:00
// GetIssueByIndex returns raw issue without loading attributes by index in a repository.
2023-07-22 22:14:27 +08:00
func GetIssueByIndex ( ctx context . Context , repoID , index int64 ) ( * Issue , error ) {
2023-05-18 18:45:25 +08:00
if index < 1 {
return nil , ErrIssueNotExist { }
2020-05-16 22:05:19 +01:00
}
2023-05-18 18:45:25 +08:00
issue := & Issue {
RepoID : repoID ,
Index : index ,
2020-05-16 22:05:19 +01:00
}
2023-07-22 22:14:27 +08:00
has , err := db . GetEngine ( ctx ) . Get ( issue )
2020-05-16 22:05:19 +01:00
if err != nil {
2023-05-18 18:45:25 +08:00
return nil , err
} else if ! has {
return nil , ErrIssueNotExist { 0 , repoID , index }
2020-01-02 08:54:22 +01:00
}
2023-05-18 18:45:25 +08:00
return issue , nil
}
2020-01-02 08:54:22 +01:00
2023-05-18 18:45:25 +08:00
// GetIssueWithAttrsByIndex returns issue by index in a repository.
2023-07-22 22:14:27 +08:00
func GetIssueWithAttrsByIndex ( ctx context . Context , repoID , index int64 ) ( * Issue , error ) {
issue , err := GetIssueByIndex ( ctx , repoID , index )
2023-05-18 18:45:25 +08:00
if err != nil {
return nil , err
2019-09-20 02:45:38 -03:00
}
2023-07-22 22:14:27 +08:00
return issue , issue . LoadAttributes ( ctx )
2023-05-18 18:45:25 +08:00
}
2020-05-16 22:05:19 +01:00
2023-05-18 18:45:25 +08:00
// GetIssueByID returns an issue by given ID.
func GetIssueByID ( ctx context . Context , id int64 ) ( * Issue , error ) {
issue := new ( Issue )
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( issue )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrIssueNotExist { id , 0 , 0 }
2020-05-16 22:05:19 +01:00
}
2023-05-18 18:45:25 +08:00
return issue , nil
2015-10-24 03:36:47 -04:00
}
2018-05-01 21:05:28 +02:00
2023-05-18 18:45:25 +08:00
// GetIssueWithAttrsByID returns an issue with attributes by given ID.
func GetIssueWithAttrsByID ( id int64 ) ( * Issue , error ) {
issue , err := GetIssueByID ( db . DefaultContext , id )
2021-11-19 21:39:57 +08:00
if err != nil {
2023-05-18 18:45:25 +08:00
return nil , err
2018-05-01 21:05:28 +02:00
}
2023-05-18 18:45:25 +08:00
return issue , issue . LoadAttributes ( db . DefaultContext )
}
2018-05-01 21:05:28 +02:00
2023-05-18 18:45:25 +08:00
// GetIssuesByIDs return issues with the given IDs.
2023-07-31 14:28:53 +08:00
// If keepOrder is true, the order of the returned issues will be the same as the given IDs.
func GetIssuesByIDs ( ctx context . Context , issueIDs [ ] int64 , keepOrder ... bool ) ( IssueList , error ) {
2023-07-20 15:18:52 +08:00
issues := make ( [ ] * Issue , 0 , len ( issueIDs ) )
2023-07-31 14:28:53 +08:00
if err := db . GetEngine ( ctx ) . In ( "id" , issueIDs ) . Find ( & issues ) ; err != nil {
return nil , err
}
if len ( keepOrder ) > 0 && keepOrder [ 0 ] {
m := make ( map [ int64 ] * Issue , len ( issues ) )
appended := container . Set [ int64 ] { }
for _ , issue := range issues {
m [ issue . ID ] = issue
}
issues = issues [ : 0 ]
for _ , id := range issueIDs {
if issue , ok := m [ id ] ; ok && ! appended . Contains ( id ) { // make sure the id is existed and not appended
appended . Add ( id )
issues = append ( issues , issue )
}
}
}
return issues , nil
2023-05-18 18:45:25 +08:00
}
2018-05-01 21:05:28 +02:00
2023-05-18 18:45:25 +08:00
// GetIssueIDsByRepoID returns all issue ids by repo id
func GetIssueIDsByRepoID ( ctx context . Context , repoID int64 ) ( [ ] int64 , error ) {
ids := make ( [ ] int64 , 0 , 10 )
err := db . GetEngine ( ctx ) . Table ( "issue" ) . Cols ( "id" ) . Where ( "repo_id = ?" , repoID ) . Find ( & ids )
return ids , err
}
2018-05-01 21:05:28 +02:00
2023-05-18 18:45:25 +08:00
// GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue,
// but skips joining with `user` for performance reasons.
// User permissions must be verified elsewhere if required.
func GetParticipantsIDsByIssueID ( ctx context . Context , issueID int64 ) ( [ ] int64 , error ) {
userIDs := make ( [ ] int64 , 0 , 5 )
return userIDs , db . GetEngine ( ctx ) .
Table ( "comment" ) .
Cols ( "poster_id" ) .
Where ( "issue_id = ?" , issueID ) .
And ( "type in (?,?,?)" , CommentTypeComment , CommentTypeCode , CommentTypeReview ) .
Distinct ( "poster_id" ) .
Find ( & userIDs )
2018-05-01 21:05:28 +02:00
}
2018-07-17 23:23:58 +02:00
2023-05-18 18:45:25 +08:00
// IsUserParticipantsOfIssue return true if user is participants of an issue
func IsUserParticipantsOfIssue ( user * user_model . User , issue * Issue ) bool {
userIDs , err := issue . GetParticipantIDsByIssue ( db . DefaultContext )
if err != nil {
log . Error ( err . Error ( ) )
return false
2022-03-01 01:20:15 +01:00
}
2023-09-07 17:37:47 +08:00
return slices . Contains ( userIDs , user . ID )
2022-03-01 01:20:15 +01:00
}
2019-10-31 00:06:10 -05:00
// DependencyInfo represents high level information about an issue which is a dependency of another issue.
type DependencyInfo struct {
2021-12-10 09:27:50 +08:00
Issue ` xorm:"extends" `
repo_model . Repository ` xorm:"extends" `
2019-10-31 00:06:10 -05:00
}
2022-06-13 17:37:59 +08:00
// GetParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author
func ( issue * Issue ) GetParticipantIDsByIssue ( ctx context . Context ) ( [ ] int64 , error ) {
2020-02-28 09:16:41 +01:00
if issue == nil {
return nil , nil
}
userIDs := make ( [ ] int64 , 0 , 5 )
2022-05-20 22:08:52 +08:00
if err := db . GetEngine ( ctx ) . Table ( "comment" ) . Cols ( "poster_id" ) .
2020-02-28 09:16:41 +01:00
Where ( "`comment`.issue_id = ?" , issue . ID ) .
And ( "`comment`.type in (?,?,?)" , CommentTypeComment , CommentTypeCode , CommentTypeReview ) .
And ( "`user`.is_active = ?" , true ) .
And ( "`user`.prohibit_login = ?" , false ) .
Join ( "INNER" , "`user`" , "`user`.id = `comment`.poster_id" ) .
Distinct ( "poster_id" ) .
Find ( & userIDs ) ; err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "get poster IDs: %w" , err )
2020-02-28 09:16:41 +01:00
}
2023-09-07 17:37:47 +08:00
if ! slices . Contains ( userIDs , issue . PosterID ) {
2020-02-28 09:16:41 +01:00
return append ( userIDs , issue . PosterID ) , nil
}
return userIDs , nil
}
2022-05-20 22:08:52 +08:00
// BlockedByDependencies finds all Dependencies an issue is blocked by
2023-03-28 19:23:25 +02:00
func ( issue * Issue ) BlockedByDependencies ( ctx context . Context , opts db . ListOptions ) ( issueDeps [ ] * DependencyInfo , err error ) {
sess := db . GetEngine ( ctx ) .
2019-10-31 00:06:10 -05:00
Table ( "issue" ) .
Join ( "INNER" , "repository" , "repository.id = issue.repo_id" ) .
Join ( "INNER" , "issue_dependency" , "issue_dependency.dependency_id = issue.id" ) .
2018-07-17 23:23:58 +02:00
Where ( "issue_id = ?" , issue . ID ) .
2021-03-15 02:52:12 +08:00
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
2023-03-28 19:23:25 +02:00
OrderBy ( "CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC" , issue . RepoID )
if opts . Page != 0 {
sess = db . SetSessionPagination ( sess , & opts )
}
err = sess . Find ( & issueDeps )
2021-11-18 08:18:12 +00:00
for _ , depInfo := range issueDeps {
depInfo . Issue . Repo = & depInfo . Repository
}
return issueDeps , err
2018-07-17 23:23:58 +02:00
}
2022-05-20 22:08:52 +08:00
// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
func ( issue * Issue ) BlockingDependencies ( ctx context . Context ) ( issueDeps [ ] * DependencyInfo , err error ) {
err = db . GetEngine ( ctx ) .
2019-10-31 00:06:10 -05:00
Table ( "issue" ) .
Join ( "INNER" , "repository" , "repository.id = issue.repo_id" ) .
Join ( "INNER" , "issue_dependency" , "issue_dependency.issue_id = issue.id" ) .
2018-07-17 23:23:58 +02:00
Where ( "dependency_id = ?" , issue . ID ) .
2021-03-15 02:52:12 +08:00
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
2022-06-05 03:18:50 +08:00
OrderBy ( "CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC" , issue . RepoID ) .
2018-07-17 23:23:58 +02:00
Find ( & issueDeps )
2021-11-18 08:18:12 +00:00
for _ , depInfo := range issueDeps {
depInfo . Issue . Repo = & depInfo . Repository
}
return issueDeps , err
2018-07-17 23:23:58 +02:00
}
2022-06-13 17:37:59 +08:00
func migratedIssueCond ( tp api . GitServiceType ) builder . Cond {
return builder . In ( "issue_id" ,
builder . Select ( "issue.id" ) .
From ( "issue" ) .
InnerJoin ( "repository" , "issue.repo_id = repository.id" ) .
Where ( builder . Eq {
"repository.original_service_type" : tp ,
} ) ,
)
}
2022-02-01 19:20:28 +01:00
// RemapExternalUser ExternalUserRemappable interface
func ( issue * Issue ) RemapExternalUser ( externalName string , externalID , userID int64 ) error {
issue . OriginalAuthor = externalName
issue . OriginalAuthorID = externalID
issue . PosterID = userID
return nil
}
// GetUserID ExternalUserRemappable interface
func ( issue * Issue ) GetUserID ( ) int64 { return issue . PosterID }
// GetExternalName ExternalUserRemappable interface
func ( issue * Issue ) GetExternalName ( ) string { return issue . OriginalAuthor }
// GetExternalID ExternalUserRemappable interface
func ( issue * Issue ) GetExternalID ( ) int64 { return issue . OriginalAuthorID }
2022-06-13 17:37:59 +08:00
2023-02-16 01:29:13 +08:00
// HasOriginalAuthor returns if an issue was migrated and has an original author.
func ( issue * Issue ) HasOriginalAuthor ( ) bool {
return issue . OriginalAuthor != "" && issue . OriginalAuthorID != 0
}
2023-05-25 15:17:19 +02:00
2023-05-30 17:26:51 +02:00
var ErrIssueMaxPinReached = util . NewInvalidArgumentErrorf ( "the max number of pinned issues has been readched" )
2023-05-25 15:17:19 +02:00
// IsPinned returns if a Issue is pinned
func ( issue * Issue ) IsPinned ( ) bool {
return issue . PinOrder != 0
}
// Pin pins a Issue
func ( issue * Issue ) Pin ( ctx context . Context , user * user_model . User ) error {
// If the Issue is already pinned, we don't need to pin it twice
if issue . IsPinned ( ) {
return nil
}
var maxPin int
_ , err := db . GetEngine ( ctx ) . SQL ( "SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?" , issue . RepoID , issue . IsPull ) . Get ( & maxPin )
if err != nil {
return err
}
// Check if the maximum allowed Pins reached
if maxPin >= setting . Repository . Issue . MaxPinned {
2023-05-30 17:26:51 +02:00
return ErrIssueMaxPinReached
2023-05-25 15:17:19 +02:00
}
_ , err = db . GetEngine ( ctx ) . Table ( "issue" ) .
Where ( "id = ?" , issue . ID ) .
2023-07-04 20:36:08 +02:00
Update ( map [ string ] any {
2023-05-25 15:17:19 +02:00
"pin_order" : maxPin + 1 ,
} )
if err != nil {
return err
}
// Add the pin event to the history
opts := & CreateCommentOptions {
Type : CommentTypePin ,
Doer : user ,
Repo : issue . Repo ,
Issue : issue ,
}
if _ , err = CreateComment ( ctx , opts ) ; err != nil {
return err
}
return nil
}
// UnpinIssue unpins a Issue
func ( issue * Issue ) Unpin ( ctx context . Context , user * user_model . User ) error {
// If the Issue is not pinned, we don't need to unpin it
if ! issue . IsPinned ( ) {
return nil
}
// This sets the Pin for all Issues that come after the unpined Issue to the correct value
_ , 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 )
if err != nil {
return err
}
_ , err = db . GetEngine ( ctx ) . Table ( "issue" ) .
Where ( "id = ?" , issue . ID ) .
2023-07-04 20:36:08 +02:00
Update ( map [ string ] any {
2023-05-25 15:17:19 +02:00
"pin_order" : 0 ,
} )
if err != nil {
return err
}
// Add the unpin event to the history
opts := & CreateCommentOptions {
Type : CommentTypeUnpin ,
Doer : user ,
Repo : issue . Repo ,
Issue : issue ,
}
if _ , err = CreateComment ( ctx , opts ) ; err != nil {
return err
}
return nil
}
// PinOrUnpin pins or unpins a Issue
func ( issue * Issue ) PinOrUnpin ( ctx context . Context , user * user_model . User ) error {
if ! issue . IsPinned ( ) {
return issue . Pin ( ctx , user )
}
return issue . Unpin ( ctx , user )
}
// MovePin moves a Pinned Issue to a new Position
func ( issue * Issue ) MovePin ( ctx context . Context , newPosition int ) error {
// If the Issue is not pinned, we can't move them
if ! issue . IsPinned ( ) {
return nil
}
if newPosition < 1 {
return fmt . Errorf ( "The Position can't be lower than 1" )
}
dbctx , committer , err := db . TxContext ( ctx )
if err != nil {
return err
}
defer committer . Close ( )
var maxPin int
_ , err = db . GetEngine ( dbctx ) . SQL ( "SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?" , issue . RepoID , issue . IsPull ) . Get ( & maxPin )
if err != nil {
return err
}
// If the new Position bigger than the current Maximum, set it to the Maximum
if newPosition > maxPin + 1 {
newPosition = maxPin + 1
}
// Lower the Position of all Pinned Issue that came after the current Position
_ , 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 )
if err != nil {
return err
}
// Higher the Position of all Pinned Issues that comes after the new Position
_ , 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 )
if err != nil {
return err
}
_ , err = db . GetEngine ( dbctx ) . Table ( "issue" ) .
Where ( "id = ?" , issue . ID ) .
2023-07-04 20:36:08 +02:00
Update ( map [ string ] any {
2023-05-25 15:17:19 +02:00
"pin_order" : newPosition ,
} )
if err != nil {
return err
}
return committer . Commit ( )
}
// GetPinnedIssues returns the pinned Issues for the given Repo and type
2023-08-08 03:26:40 +08:00
func GetPinnedIssues ( ctx context . Context , repoID int64 , isPull bool ) ( IssueList , error ) {
issues := make ( IssueList , 0 )
2023-05-25 15:17:19 +02:00
err := db . GetEngine ( ctx ) .
Table ( "issue" ) .
Where ( "repo_id = ?" , repoID ) .
And ( "is_pull = ?" , isPull ) .
And ( "pin_order > 0" ) .
OrderBy ( "pin_order" ) .
Find ( & issues )
if err != nil {
return nil , err
}
2023-08-08 03:26:40 +08:00
err = issues . LoadAttributes ( ctx )
2023-05-25 15:17:19 +02:00
if err != nil {
return nil , err
}
return issues , nil
}
// IsNewPinnedAllowed 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
2023-05-30 17:26:51 +02:00
_ , 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 )
2023-05-25 15:17:19 +02:00
if err != nil {
return false , err
}
return maxPin < setting . Repository . Issue . MaxPinned , nil
}
2023-05-30 17:26:51 +02:00
// IsErrIssueMaxPinReached returns if the error is, that the User can't pin more Issues
func IsErrIssueMaxPinReached ( err error ) bool {
return err == ErrIssueMaxPinReached
}
2023-09-09 05:09:23 +08:00
// InsertIssues insert issues to database
func InsertIssues ( issues ... * Issue ) error {
ctx , committer , err := db . TxContext ( db . DefaultContext )
if err != nil {
return err
}
defer committer . Close ( )
for _ , issue := range issues {
if err := insertIssue ( ctx , issue ) ; err != nil {
return err
}
}
return committer . Commit ( )
}
func insertIssue ( ctx context . Context , issue * Issue ) error {
sess := db . GetEngine ( ctx )
if _ , err := sess . NoAutoTime ( ) . Insert ( issue ) ; err != nil {
return err
}
issueLabels := make ( [ ] IssueLabel , 0 , len ( issue . Labels ) )
for _ , label := range issue . Labels {
issueLabels = append ( issueLabels , IssueLabel {
IssueID : issue . ID ,
LabelID : label . ID ,
} )
}
if len ( issueLabels ) > 0 {
if _ , err := sess . Insert ( issueLabels ) ; err != nil {
return err
}
}
for _ , reaction := range issue . Reactions {
reaction . IssueID = issue . ID
}
if len ( issue . Reactions ) > 0 {
if _ , err := sess . Insert ( issue . Reactions ) ; err != nil {
return err
}
}
return nil
}