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.
2014-03-20 16:04:56 -04:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
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"
2017-01-30 20:46:45 +08:00
"sort"
2019-10-31 00:06:10 -05:00
"strconv"
2014-03-22 13:50:50 -04:00
"strings"
2014-05-07 16:51:14 -04:00
2021-09-19 19:49:59 +08:00
"code.gitea.io/gitea/models/db"
2022-03-17 22:38:35 +05:30
"code.gitea.io/gitea/models/foreignreference"
2022-03-29 14:29:02 +08:00
"code.gitea.io/gitea/models/organization"
2021-11-28 19:58:28 +08:00
"code.gitea.io/gitea/models/perm"
2022-05-11 18:09:36 +08:00
access_model "code.gitea.io/gitea/models/perm/access"
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"
2022-10-17 07:29:26 +08:00
system_model "code.gitea.io/gitea/models/system"
2021-11-10 03:57:58 +08:00
"code.gitea.io/gitea/models/unit"
2021-11-24 17:49:20 +08:00
user_model "code.gitea.io/gitea/models/user"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/base"
2021-12-02 08:28:08 +01:00
"code.gitea.io/gitea/modules/git"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/log"
2021-01-02 18:04:02 +01:00
"code.gitea.io/gitea/modules/references"
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"
2019-10-17 17:26:49 +08:00
"xorm.io/xorm"
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
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-03-17 22:38:35 +05:30
Attachments [ ] * repo_model . Attachment ` xorm:"-" `
Comments [ ] * Comment ` xorm:"-" `
2022-06-13 17:37:59 +08:00
Reactions ReactionList ` xorm:"-" `
2022-03-17 22:38:35 +05:30
TotalTrackedTime int64 ` xorm:"-" `
Assignees [ ] * user_model . User ` xorm:"-" `
ForeignReference * foreignreference . ForeignReference ` 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 ) {
2016-08-26 13:40:53 -07:00
if issue . Repo == nil {
2021-12-10 09:27:50 +08:00
issue . Repo , err = repo_model . GetRepositoryByIDCtx ( 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
func ( issue * Issue ) IsTimetrackerEnabled ( ) bool {
2021-12-10 09:27:50 +08:00
return issue . isTimetrackerEnabled ( db . DefaultContext )
2019-01-14 02:29:58 +00:00
}
2021-12-10 09:27:50 +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
}
2021-12-13 22:59:39 +00:00
return issue . Repo . IsTimetrackerEnabledCtx ( 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
}
2020-02-29 03:49:50 +01:00
// LoadLabels loads labels
2022-04-28 13:48:48 +02:00
func ( issue * Issue ) LoadLabels ( ctx context . Context ) ( err error ) {
2017-01-30 20:46:45 +08:00
if issue . Labels == nil {
2022-05-20 22:08:52 +08:00
issue . Labels , err = GetLabelsByIssueID ( ctx , issue . ID )
2017-01-30 20:46:45 +08:00
if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getLabelsByIssueID [%d]: %w" , issue . ID , err )
2017-01-30 20:46:45 +08:00
}
2016-12-17 19:49:17 +08:00
}
2017-01-30 20:46:45 +08:00
return nil
}
2016-03-13 23:20:22 -04:00
2018-12-13 23:55:43 +08:00
// LoadPoster loads poster
func ( issue * Issue ) LoadPoster ( ) error {
2022-05-20 22:08:52 +08:00
return issue . loadPoster ( db . DefaultContext )
2018-12-13 23:55:43 +08:00
}
2022-05-20 22:08:52 +08:00
func ( issue * Issue ) loadPoster ( ctx context . Context ) ( err error ) {
2016-08-26 13:40:53 -07:00
if issue . Poster == nil {
2022-05-20 22:08:52 +08:00
issue . Poster , err = user_model . GetUserByIDCtx ( 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
}
2016-11-09 13:07:01 +08:00
err = nil
2016-03-13 23:20:22 -04:00
return
}
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-05-20 22:08:52 +08:00
func ( issue * Issue ) loadPullRequest ( ctx context . Context ) ( err error ) {
2017-07-26 00:16:45 -07:00
if issue . IsPull && issue . PullRequest == nil {
2022-05-20 22:08:52 +08:00
issue . PullRequest , err = GetPullRequestByIssueID ( ctx , issue . ID )
2017-07-26 00:16:45 -07:00
if err != nil {
if IsErrPullRequestNotExist ( err ) {
return err
}
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getPullRequestByIssueID [%d]: %w" , issue . ID , err )
2017-07-26 00:16:45 -07:00
}
2018-12-13 23:55:43 +08:00
issue . PullRequest . Issue = issue
2017-07-26 00:16:45 -07:00
}
return nil
}
2018-12-13 23:55:43 +08:00
// LoadPullRequest loads pull request info
func ( issue * Issue ) LoadPullRequest ( ) error {
2022-05-20 22:08:52 +08:00
return issue . loadPullRequest ( db . DefaultContext )
2018-12-13 23:55:43 +08:00
}
2022-05-20 22:08:52 +08:00
func ( issue * Issue ) loadComments ( ctx context . Context ) ( err error ) {
return issue . loadCommentsByType ( ctx , CommentTypeUnknown )
2019-02-19 22:39:39 +08:00
}
// LoadDiscussComments loads discuss comments
func ( issue * Issue ) LoadDiscussComments ( ) error {
2022-05-20 22:08:52 +08:00
return issue . loadCommentsByType ( db . DefaultContext , 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-03-17 22:38:35 +05:30
func ( issue * Issue ) loadForeignReference ( ctx context . Context ) ( err error ) {
if issue . ForeignReference != nil {
return nil
}
reference := & foreignreference . ForeignReference {
RepoID : issue . RepoID ,
LocalIndex : issue . Index ,
Type : foreignreference . TypeIssue ,
}
has , err := db . GetEngine ( ctx ) . Get ( reference )
if err != nil {
return err
} else if ! has {
return foreignreference . ErrForeignIndexNotExist {
RepoID : issue . RepoID ,
LocalIndex : issue . Index ,
Type : foreignreference . TypeIssue ,
}
}
issue . ForeignReference = reference
return nil
}
2022-04-08 17:11:15 +08:00
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 {
2017-01-30 20:46:45 +08:00
return
}
2022-05-20 22:08:52 +08:00
if err = issue . loadPoster ( ctx ) ; err != nil {
2017-01-30 20:46:45 +08:00
return
}
2022-04-28 13:48:48 +02:00
if err = issue . LoadLabels ( ctx ) ; err != nil {
2017-01-30 20:46:45 +08:00
return
2016-08-26 13:40:53 -07:00
}
2015-08-10 21:47:23 +08:00
2022-04-08 17:11:15 +08:00
if err = issue . loadMilestone ( ctx ) ; err != nil {
2020-01-01 23:51:10 +01:00
return
2015-08-05 20:23:08 +08:00
}
2022-05-20 22:08:52 +08:00
if err = issue . loadProject ( ctx ) ; err != nil {
2020-08-17 04:07:38 +01:00
return
}
2022-05-20 22:08:52 +08:00
if err = issue . LoadAssignees ( ctx ) ; err != nil {
2017-06-23 15:43:37 +02:00
return
2016-08-14 03:32:24 -07:00
}
2022-05-20 22:08:52 +08: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
2021-12-10 09:27:50 +08:00
if err = CommentList ( issue . Comments ) . loadAttributes ( ctx ) ; err != nil {
2019-04-18 13:00:03 +08:00
return err
}
2021-12-10 09:27:50 +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
2022-03-17 22:38:35 +05:30
if err = issue . loadForeignReference ( ctx ) ; err != nil && ! foreignreference . IsErrForeignIndexNotExist ( err ) {
return err
}
2021-12-10 09:27:50 +08:00
return issue . loadReactions ( ctx )
2016-08-14 03:32:24 -07:00
}
2020-01-01 23:51:10 +01:00
// LoadMilestone load milestone of this issue.
func ( issue * Issue ) LoadMilestone ( ) error {
2022-04-08 17:11:15 +08:00
return issue . loadMilestone ( db . DefaultContext )
2020-01-01 23:51:10 +01: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 )
}
2021-11-16 18:18:25 +00:00
// Link returns the Link URL to this issue.
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
}
2022-05-20 22:08:52 +08:00
func ( issue * Issue ) getLabels ( ctx context . Context ) ( err error ) {
2016-08-03 11:51:22 -07:00
if len ( issue . Labels ) > 0 {
2014-05-24 02:31:58 -04:00
return nil
}
2022-05-20 22:08:52 +08:00
issue . Labels , err = GetLabelsByIssueID ( ctx , issue . ID )
2015-08-10 14:42:50 +08:00
if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getLabelsByIssueID: %w" , err )
2014-05-24 02:31:58 -04:00
}
return nil
}
2022-03-29 22:57:33 +08:00
func clearIssueLabels ( ctx context . Context , issue * Issue , doer * user_model . User ) ( err error ) {
2022-05-20 22:08:52 +08:00
if err = issue . getLabels ( ctx ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getLabels: %w" , err )
2016-08-03 11:51:22 -07:00
}
for i := range issue . Labels {
2022-03-29 22:57:33 +08:00
if err = deleteIssueLabel ( ctx , issue , issue . Labels [ i ] , doer ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "removeLabel: %w" , err )
2016-08-03 11:51:22 -07:00
}
}
return nil
}
2022-03-29 22:57:33 +08:00
// ClearIssueLabels removes all issue labels as the given user.
2016-11-24 09:41:11 +01:00
// Triggers appropriate WebHooks, if any.
2022-03-29 22:57:33 +08:00
func ClearIssueLabels ( issue * Issue , doer * user_model . User ) ( err error ) {
2022-11-13 04:18:50 +08:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-09-19 19:49:59 +08:00
if err != nil {
2015-08-15 00:42:43 +08:00
return err
}
2021-09-19 19:49:59 +08:00
defer committer . Close ( )
2015-08-15 00:42:43 +08:00
2022-04-08 17:11:15 +08:00
if err := issue . LoadRepo ( ctx ) ; err != nil {
2016-12-17 19:49:17 +08:00
return err
2022-05-20 22:08:52 +08:00
} else if err = issue . loadPullRequest ( ctx ) ; err != nil {
2017-07-26 00:16:45 -07:00
return err
2016-12-17 19:49:17 +08:00
}
2022-05-11 18:09:36 +08:00
perm , err := access_model . GetUserRepoPermission ( ctx , issue . Repo , doer )
2018-11-28 19:26:14 +08:00
if err != nil {
2016-12-17 19:49:17 +08:00
return err
2018-11-28 19:26:14 +08:00
}
if ! perm . CanWriteIssuesOrPulls ( issue . IsPull ) {
2020-04-01 00:14:46 -04:00
return ErrRepoLabelNotExist { }
2016-12-17 19:49:17 +08:00
}
2022-03-29 22:57:33 +08:00
if err = clearIssueLabels ( ctx , issue , doer ) ; err != nil {
2015-08-15 00:42:43 +08:00
return err
}
2021-09-19 19:49:59 +08:00
if err = committer . Commit ( ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "Commit: %w" , err )
2016-08-14 03:32:24 -07:00
}
return nil
2015-08-15 00:42:43 +08:00
}
2017-01-30 20:46:45 +08:00
type labelSorter [ ] * Label
func ( ts labelSorter ) Len ( ) int {
return len ( [ ] * Label ( ts ) )
}
func ( ts labelSorter ) Less ( i , j int ) bool {
return [ ] * Label ( ts ) [ i ] . ID < [ ] * Label ( ts ) [ j ] . ID
}
func ( ts labelSorter ) Swap ( i , j int ) {
[ ] * Label ( ts ) [ i ] , [ ] * Label ( ts ) [ j ] = [ ] * Label ( ts ) [ j ] , [ ] * Label ( ts ) [ i ]
}
2022-03-29 22:57:33 +08:00
// ReplaceIssueLabels removes all current labels and add new labels to the issue.
2016-11-24 09:41:11 +01:00
// Triggers appropriate WebHooks, if any.
2022-03-29 22:57:33 +08:00
func ReplaceIssueLabels ( issue * Issue , labels [ ] * Label , doer * user_model . User ) ( err error ) {
2022-11-13 04:18:50 +08:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-09-19 19:49:59 +08:00
if err != nil {
2015-08-15 00:42:43 +08:00
return err
}
2021-09-19 19:49:59 +08:00
defer committer . Close ( )
2015-08-15 00:42:43 +08:00
2022-04-08 17:11:15 +08:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2021-03-12 17:45:49 +00:00
return err
}
2022-04-28 13:48:48 +02:00
if err = issue . LoadLabels ( ctx ) ; err != nil {
2017-01-30 20:46:45 +08:00
return err
}
sort . Sort ( labelSorter ( labels ) )
sort . Sort ( labelSorter ( issue . Labels ) )
var toAdd , toRemove [ ] * Label
2017-02-27 20:35:55 -05:00
addIndex , removeIndex := 0 , 0
for addIndex < len ( labels ) && removeIndex < len ( issue . Labels ) {
addLabel := labels [ addIndex ]
removeLabel := issue . Labels [ removeIndex ]
if addLabel . ID == removeLabel . ID {
2021-03-12 17:45:49 +00:00
// Silently drop invalid labels
if removeLabel . RepoID != issue . RepoID && removeLabel . OrgID != issue . Repo . OwnerID {
toRemove = append ( toRemove , removeLabel )
}
2017-02-27 20:35:55 -05:00
addIndex ++
removeIndex ++
} else if addLabel . ID < removeLabel . ID {
2021-03-12 17:45:49 +00:00
// Only add if the label is valid
if addLabel . RepoID == issue . RepoID || addLabel . OrgID == issue . Repo . OwnerID {
toAdd = append ( toAdd , addLabel )
}
2017-02-27 20:35:55 -05:00
addIndex ++
} else {
toRemove = append ( toRemove , removeLabel )
removeIndex ++
2017-01-30 20:46:45 +08:00
}
}
2017-02-27 20:35:55 -05:00
toAdd = append ( toAdd , labels [ addIndex : ] ... )
toRemove = append ( toRemove , issue . Labels [ removeIndex : ] ... )
2017-01-30 20:46:45 +08:00
if len ( toAdd ) > 0 {
2022-03-29 22:57:33 +08:00
if err = newIssueLabels ( ctx , issue , toAdd , doer ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "addLabels: %w" , err )
2017-01-30 20:46:45 +08:00
}
}
2017-02-27 20:35:55 -05:00
for _ , l := range toRemove {
2022-03-29 22:57:33 +08:00
if err = deleteIssueLabel ( ctx , issue , l , doer ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "removeLabel: %w" , err )
2017-01-30 20:46:45 +08:00
}
2015-08-15 00:42:43 +08:00
}
2020-10-05 07:49:11 +01:00
issue . Labels = nil
2022-04-28 13:48:48 +02:00
if err = issue . LoadLabels ( ctx ) ; err != nil {
2020-10-05 07:49:11 +01:00
return err
}
2021-09-19 19:49:59 +08:00
return committer . Commit ( )
2015-08-10 14:42:50 +08:00
}
2022-04-08 17:11:15 +08:00
// UpdateIssueCols updates cols of issue
func UpdateIssueCols ( ctx context . Context , issue * Issue , cols ... string ) error {
2022-01-17 18:31:58 +00:00
if _ , err := db . GetEngine ( ctx ) . ID ( issue . ID ) . Cols ( cols ... ) . Update ( issue ) ; err != nil {
2017-01-24 21:43:02 -05:00
return err
}
return nil
2016-08-14 03:32:24 -07:00
}
2022-03-29 22:57:33 +08:00
func changeIssueStatus ( ctx context . Context , issue * Issue , doer * user_model . User , isClosed , isMergePull bool ) ( * Comment , error ) {
2019-03-05 02:52:52 +00:00
// Reload the issue
2022-06-13 17:37:59 +08:00
currentIssue , err := GetIssueByID ( ctx , issue . ID )
2019-03-05 02:52:52 +00:00
if err != nil {
2019-12-16 05:57:34 +08:00
return nil , err
2019-03-05 02:52:52 +00:00
}
2016-03-05 12:58:51 -05:00
// Nothing should be performed if current status is same as target status
2019-03-05 02:52:52 +00:00
if currentIssue . IsClosed == isClosed {
2019-12-16 05:57:34 +08:00
if ! issue . IsPull {
return nil , ErrIssueWasClosed {
ID : issue . ID ,
}
}
return nil , ErrPullWasClosed {
ID : issue . ID ,
}
2015-08-13 16:07:11 +08:00
}
2018-07-17 23:23:58 +02:00
2020-05-16 22:05:19 +01:00
issue . IsClosed = isClosed
2022-03-29 22:57:33 +08:00
return doChangeIssueStatus ( ctx , issue , doer , isMergePull )
2020-05-16 22:05:19 +01:00
}
2022-03-29 22:57:33 +08:00
func doChangeIssueStatus ( ctx context . Context , issue * Issue , doer * user_model . User , isMergePull bool ) ( * Comment , error ) {
2018-07-17 23:23:58 +02:00
// Check for open dependencies
2021-12-10 09:27:50 +08:00
if issue . IsClosed && issue . Repo . IsDependenciesEnabledCtx ( ctx ) {
2018-07-17 23:23:58 +02:00
// only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies
2022-05-03 21:46:28 +02:00
noDeps , err := IssueNoDependenciesLeft ( ctx , issue )
2018-07-17 23:23:58 +02:00
if err != nil {
2019-12-16 05:57:34 +08:00
return nil , err
2018-07-17 23:23:58 +02:00
}
if ! noDeps {
2019-12-16 05:57:34 +08:00
return nil , ErrDependenciesLeft { issue . ID }
2018-07-17 23:23:58 +02:00
}
}
2020-05-16 22:05:19 +01:00
if issue . IsClosed {
2019-08-15 22:46:21 +08:00
issue . ClosedUnix = timeutil . TimeStampNow ( )
2018-02-19 04:39:26 +02:00
} else {
issue . ClosedUnix = 0
}
2015-08-13 16:07:11 +08:00
2022-04-08 17:11:15 +08:00
if err := UpdateIssueCols ( ctx , issue , "is_closed" , "closed_unix" ) ; err != nil {
2019-12-16 05:57:34 +08:00
return nil , err
2015-08-13 16:07:11 +08:00
}
2016-03-05 12:58:51 -05:00
// Update issue count of labels
2022-05-20 22:08:52 +08:00
if err := issue . getLabels ( ctx ) ; err != nil {
2019-12-16 05:57:34 +08:00
return nil , err
2015-08-13 16:07:11 +08:00
}
2016-11-22 12:24:39 +01:00
for idx := range issue . Labels {
2022-05-20 22:08:52 +08:00
if err := updateLabelCols ( ctx , issue . Labels [ idx ] , "num_issues" , "num_closed_issue" ) ; err != nil {
2019-12-16 05:57:34 +08:00
return nil , err
2015-08-13 16:07:11 +08:00
}
}
2016-03-05 12:58:51 -05:00
// Update issue count of milestone
2021-06-21 20:34:58 +02:00
if issue . MilestoneID > 0 {
2022-06-13 17:37:59 +08:00
if err := UpdateMilestoneCounters ( ctx , issue . MilestoneID ) ; err != nil {
2021-06-21 20:34:58 +02:00
return nil , err
}
2015-08-13 16:07:11 +08:00
}
2022-10-25 20:47:46 +08:00
// update repository's issue closed number
if err := repo_model . UpdateRepoIssueNumbers ( ctx , issue . RepoID , issue . IsPull , true ) ; err != nil {
2020-02-20 15:46:46 +08:00
return nil , err
}
2016-03-05 12:58:51 -05:00
// New action comment
2019-11-16 02:18:09 +08:00
cmtType := CommentTypeClose
if ! issue . IsClosed {
cmtType = CommentTypeReopen
2020-04-14 09:06:23 +08:00
} else if isMergePull {
cmtType = CommentTypeMergePull
2019-11-16 02:18:09 +08:00
}
2019-12-01 10:44:39 +08:00
2022-04-08 17:11:15 +08:00
return CreateCommentCtx ( ctx , & CreateCommentOptions {
2019-11-16 02:18:09 +08:00
Type : cmtType ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
2019-12-16 05:57:34 +08:00
} )
2015-08-13 16:07:11 +08:00
}
2022-03-29 22:57:33 +08:00
// ChangeIssueStatus changes issue status to open or closed.
2022-05-03 21:46:28 +02:00
func ChangeIssueStatus ( ctx context . Context , issue * Issue , doer * user_model . User , isClosed bool ) ( * Comment , error ) {
2022-04-08 17:11:15 +08:00
if err := issue . LoadRepo ( ctx ) ; err != nil {
2019-12-16 05:57:34 +08:00
return nil , err
2018-12-13 23:55:43 +08:00
}
2022-05-20 22:08:52 +08:00
if err := issue . loadPoster ( ctx ) ; err != nil {
2019-12-16 05:57:34 +08:00
return nil , err
2018-12-13 23:55:43 +08:00
}
2022-05-03 21:46:28 +02:00
return changeIssueStatus ( ctx , issue , doer , isClosed , false )
2016-08-14 03:32:24 -07:00
}
2022-03-29 22:57:33 +08:00
// ChangeIssueTitle changes the title of this issue, as the given user.
func ChangeIssueTitle ( issue * Issue , doer * user_model . User , oldTitle string ) ( err error ) {
2022-11-13 04:18:50 +08:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-09-19 19:49:59 +08:00
if err != nil {
2017-02-05 22:36:00 +08:00
return err
}
2021-09-19 19:49:59 +08:00
defer committer . Close ( )
2017-02-05 22:36:00 +08:00
2022-04-08 17:11:15 +08:00
if err = UpdateIssueCols ( ctx , issue , "name" ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "updateIssueCols: %w" , err )
2017-02-05 22:36:00 +08:00
}
2022-04-08 17:11:15 +08:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "loadRepo: %w" , err )
2018-12-13 23:55:43 +08:00
}
2021-03-15 02:52:12 +08:00
opts := & CreateCommentOptions {
2019-11-16 02:18:09 +08:00
Type : CommentTypeChangeTitle ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
OldTitle : oldTitle ,
NewTitle : issue . Title ,
2019-12-01 10:44:39 +08:00
}
2022-04-08 17:11:15 +08:00
if _ , err = CreateCommentCtx ( ctx , opts ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "createComment: %w" , err )
2017-02-05 22:36:00 +08:00
}
2022-06-13 17:37:59 +08:00
if err = issue . AddCrossReferences ( ctx , doer , true ) ; err != nil {
2019-09-20 02:45:38 -03:00
return err
}
2021-09-19 19:49:59 +08:00
return committer . Commit ( )
2015-08-13 16:07:11 +08:00
}
2022-03-29 22:57:33 +08:00
// ChangeIssueRef changes the branch of this issue, as the given user.
func ChangeIssueRef ( issue * Issue , doer * user_model . User , oldRef string ) ( err error ) {
2022-11-13 04:18:50 +08:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-09-19 19:49:59 +08:00
if err != nil {
2020-09-08 18:29:51 +02:00
return err
}
2021-09-19 19:49:59 +08:00
defer committer . Close ( )
2020-09-08 18:29:51 +02:00
2022-04-08 17:11:15 +08:00
if err = UpdateIssueCols ( ctx , issue , "ref" ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "updateIssueCols: %w" , err )
2020-09-08 18:29:51 +02:00
}
2022-04-08 17:11:15 +08:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "loadRepo: %w" , err )
2021-11-19 09:54:31 +00:00
}
2021-12-02 08:28:08 +01:00
oldRefFriendly := strings . TrimPrefix ( oldRef , git . BranchPrefix )
newRefFriendly := strings . TrimPrefix ( issue . Ref , git . BranchPrefix )
2021-11-19 09:54:31 +00:00
opts := & CreateCommentOptions {
Type : CommentTypeChangeIssueRef ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
OldRef : oldRefFriendly ,
NewRef : newRefFriendly ,
}
2022-04-08 17:11:15 +08:00
if _ , err = CreateCommentCtx ( ctx , opts ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "createComment: %w" , err )
2021-11-19 09:54:31 +00:00
}
2021-09-19 19:49:59 +08:00
return committer . Commit ( )
2020-09-08 18:29:51 +02:00
}
2017-02-11 12:00:29 +08:00
// AddDeletePRBranchComment adds delete branch comment for pull request issue
2022-05-03 21:46:28 +02:00
func AddDeletePRBranchComment ( ctx context . Context , doer * user_model . User , repo * repo_model . Repository , issueID int64 , branchName string ) error {
2022-06-13 17:37:59 +08:00
issue , err := GetIssueByID ( ctx , issueID )
2021-09-19 19:49:59 +08:00
if err != nil {
2017-02-11 12:00:29 +08:00
return err
}
2021-03-15 02:52:12 +08:00
opts := & CreateCommentOptions {
2021-02-08 03:09:14 +00:00
Type : CommentTypeDeleteBranch ,
Doer : doer ,
Repo : repo ,
Issue : issue ,
OldRef : branchName ,
2019-12-01 10:44:39 +08:00
}
2022-05-03 21:46:28 +02:00
_ , err = CreateCommentCtx ( ctx , opts )
return err
2017-02-11 12:00:29 +08:00
}
2022-03-29 22:57:33 +08:00
// UpdateIssueAttachments update attachments by UUIDs for the issue
func UpdateIssueAttachments ( issueID int64 , uuids [ ] string ) ( err error ) {
2022-11-13 04:18:50 +08:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-09-19 19:49:59 +08:00
if err != nil {
2019-10-15 20:19:32 +08:00
return err
}
2021-09-19 19:49:59 +08:00
defer committer . Close ( )
2021-11-19 21:39:57 +08:00
attachments , err := repo_model . GetAttachmentsByUUIDs ( ctx , uuids )
2019-10-15 20:19:32 +08:00
if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getAttachmentsByUUIDs [uuids: %v]: %w" , uuids , err )
2019-10-15 20:19:32 +08:00
}
for i := 0 ; i < len ( attachments ) ; i ++ {
2022-03-29 22:57:33 +08:00
attachments [ i ] . IssueID = issueID
2022-05-20 22:08:52 +08:00
if err := repo_model . UpdateAttachment ( ctx , attachments [ i ] ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "update attachment [id: %d]: %w" , attachments [ i ] . ID , err )
2019-10-15 20:19:32 +08:00
}
}
2021-09-19 19:49:59 +08:00
return committer . Commit ( )
2019-10-15 20:19:32 +08:00
}
2022-03-29 22:57:33 +08:00
// ChangeIssueContent changes issue content, as the given user.
func ChangeIssueContent ( issue * Issue , doer * user_model . User , content string ) ( err error ) {
2022-11-13 04:18:50 +08:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-09-19 19:49:59 +08:00
if err != nil {
2019-09-20 02:45:38 -03:00
return err
}
2021-09-19 19:49:59 +08:00
defer committer . Close ( )
2019-09-20 02:45:38 -03:00
2022-06-13 17:37:59 +08:00
hasContentHistory , err := HasIssueContentHistory ( ctx , issue . ID , 0 )
2021-11-22 13:20:16 +01:00
if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "HasIssueContentHistory: %w" , err )
2021-11-22 13:20:16 +01:00
}
if ! hasContentHistory {
2022-06-13 17:37:59 +08:00
if err = SaveIssueContentHistory ( ctx , issue . PosterID , issue . ID , 0 ,
2021-11-22 13:20:16 +01:00
issue . CreatedUnix , issue . Content , true ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "SaveIssueContentHistory: %w" , err )
2021-11-22 13:20:16 +01:00
}
}
issue . Content = content
2022-04-08 17:11:15 +08:00
if err = UpdateIssueCols ( ctx , issue , "content" ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "UpdateIssueCols: %w" , err )
2016-08-14 03:32:24 -07:00
}
2019-11-18 20:43:03 -03:00
2022-06-13 17:37:59 +08:00
if err = SaveIssueContentHistory ( ctx , doer . ID , issue . ID , 0 ,
2021-10-11 06:40:03 +08:00
timeutil . TimeStampNow ( ) , issue . Content , false ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "SaveIssueContentHistory: %w" , err )
2021-10-11 06:40:03 +08:00
}
2022-06-13 17:37:59 +08:00
if err = issue . AddCrossReferences ( ctx , doer , true ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "addCrossReferences: %w" , err )
2019-09-20 02:45:38 -03:00
}
2021-09-19 19:49:59 +08:00
return committer . Commit ( )
2016-08-14 03:32:24 -07: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 ) )
}
2019-02-13 09:14:17 +01:00
// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close.
2019-08-15 22:46:21 +08:00
func ( issue * Issue ) GetLastEventTimestamp ( ) timeutil . TimeStamp {
2019-02-13 09:14:17 +01:00
if issue . IsClosed {
return issue . ClosedUnix
}
return issue . CreatedUnix
}
// 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"
}
return "repo.issues.opened_by"
}
2020-01-09 12:56:32 +01:00
// GetLastComment return last comment for the current issue.
func ( issue * Issue ) GetLastComment ( ) ( * Comment , error ) {
var c Comment
2021-09-23 16:45:36 +01:00
exist , err := db . GetEngine ( db . DefaultContext ) . Where ( "type = ?" , CommentTypeComment ) .
2021-10-06 22:36:24 +02:00
And ( "issue_id = ?" , issue . ID ) . Desc ( "created_unix" ) . Get ( & c )
2020-01-09 12:56:32 +01:00
if err != nil {
return nil , err
}
if ! exist {
return nil , nil
}
return & c , nil
}
2019-03-27 23:22:39 +01: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"
}
return "repo.issues.opened_by_fake"
}
2016-11-24 09:41:11 +01:00
// NewIssueOptions represents the options of a new issue.
2016-08-15 18:40:32 -07:00
type NewIssueOptions struct {
2021-12-10 09:27:50 +08:00
Repo * repo_model . Repository
2016-08-15 18:40:32 -07:00
Issue * Issue
2017-02-28 20:08:45 -05:00
LabelIDs [ ] int64
2016-08-15 18:40:32 -07:00
Attachments [ ] string // In UUID format.
IsPull bool
}
2016-03-13 23:20:22 -04:00
2022-06-13 17:37:59 +08:00
// NewIssueWithIndex creates issue with given index
func NewIssueWithIndex ( ctx context . Context , doer * user_model . User , opts NewIssueOptions ) ( err error ) {
2021-11-19 21:39:57 +08:00
e := db . GetEngine ( ctx )
2016-08-15 18:40:32 -07:00
opts . Issue . Title = strings . TrimSpace ( opts . Issue . Title )
2019-05-18 10:37:49 +08:00
2016-08-16 10:19:09 -07:00
if opts . Issue . MilestoneID > 0 {
2022-06-13 17:37:59 +08:00
milestone , err := GetMilestoneByRepoID ( ctx , opts . Issue . RepoID , opts . Issue . MilestoneID )
if err != nil && ! IsErrMilestoneNotExist ( err ) {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getMilestoneByID: %w" , err )
2016-08-16 10:19:09 -07:00
}
// Assume milestone is invalid and drop silently.
opts . Issue . MilestoneID = 0
if milestone != nil {
opts . Issue . MilestoneID = milestone . ID
opts . Issue . Milestone = milestone
}
}
2021-06-14 10:22:55 +08:00
if opts . Issue . Index <= 0 {
return fmt . Errorf ( "no issue index provided" )
}
if opts . Issue . ID > 0 {
return fmt . Errorf ( "issue exist" )
2019-08-26 23:17:23 -03:00
}
2021-06-14 10:22:55 +08:00
if _ , err := e . Insert ( opts . Issue ) ; err != nil {
2014-05-07 12:09:30 -04:00
return err
2015-09-02 16:18:09 -04:00
}
2017-02-01 10:36:08 +08:00
if opts . Issue . MilestoneID > 0 {
2022-06-13 17:37:59 +08:00
if err := UpdateMilestoneCounters ( ctx , opts . Issue . MilestoneID ) ; err != nil {
2017-02-01 10:36:08 +08:00
return err
}
2019-10-25 10:19:04 +03:00
2021-03-15 02:52:12 +08:00
opts := & CreateCommentOptions {
2019-12-01 10:44:39 +08:00
Type : CommentTypeMilestone ,
Doer : doer ,
Repo : opts . Repo ,
Issue : opts . Issue ,
OldMilestoneID : 0 ,
MilestoneID : opts . Issue . MilestoneID ,
}
2022-04-08 17:11:15 +08:00
if _ , err = CreateCommentCtx ( ctx , opts ) ; err != nil {
2019-10-25 10:19:04 +03:00
return err
}
2017-02-01 10:36:08 +08:00
}
2016-08-15 18:40:32 -07:00
if opts . IsPull {
_ , err = e . Exec ( "UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?" , opts . Issue . RepoID )
2015-09-02 16:18:09 -04:00
} else {
2016-08-15 18:40:32 -07:00
_ , err = e . Exec ( "UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?" , opts . Issue . RepoID )
2015-09-02 16:18:09 -04:00
}
if err != nil {
2014-05-07 12:09:30 -04:00
return err
2014-03-27 12:48:29 -04:00
}
2014-07-22 13:50:34 +02:00
2017-02-28 20:08:45 -05:00
if len ( opts . LabelIDs ) > 0 {
2016-11-21 20:08:21 +01:00
// During the session, SQLite3 driver cannot handle retrieve objects after update something.
2016-03-05 20:45:23 -05:00
// So we have to get all needed labels first.
2017-02-28 20:08:45 -05:00
labels := make ( [ ] * Label , 0 , len ( opts . LabelIDs ) )
if err = e . In ( "id" , opts . LabelIDs ) . Find ( & labels ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "find all labels [label_ids: %v]: %w" , opts . LabelIDs , err )
2016-03-05 20:45:23 -05:00
}
2015-08-15 00:42:43 +08:00
2022-05-20 22:08:52 +08:00
if err = opts . Issue . loadPoster ( ctx ) ; err != nil {
2017-01-30 20:46:45 +08:00
return err
}
2016-03-05 20:45:23 -05:00
for _ , label := range labels {
2016-08-15 18:40:32 -07:00
// Silently drop invalid labels.
2020-04-01 00:14:46 -04:00
if label . RepoID != opts . Repo . ID && label . OrgID != opts . Repo . OwnerID {
2016-03-13 23:20:22 -04:00
continue
}
2022-03-29 22:57:33 +08:00
if err = newIssueLabel ( ctx , opts . Issue , label , opts . Issue . Poster ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "addLabel [id: %d]: %w" , label . ID , err )
2016-03-05 20:45:23 -05:00
}
2015-08-10 16:52:08 +08:00
}
}
2022-06-13 17:37:59 +08:00
if err = NewIssueUsers ( ctx , opts . Repo , opts . Issue ) ; err != nil {
2015-08-10 21:47:23 +08:00
return err
}
2016-08-15 18:40:32 -07:00
if len ( opts . Attachments ) > 0 {
2021-11-19 21:39:57 +08:00
attachments , err := repo_model . GetAttachmentsByUUIDs ( ctx , opts . Attachments )
2015-09-01 19:07:02 -04:00
if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getAttachmentsByUUIDs [uuids: %v]: %w" , opts . Attachments , err )
2015-09-01 19:07:02 -04:00
}
2016-08-15 18:40:32 -07:00
for i := 0 ; i < len ( attachments ) ; i ++ {
attachments [ i ] . IssueID = opts . Issue . ID
2017-10-04 21:43:04 -07:00
if _ , err = e . ID ( attachments [ i ] . ID ) . Update ( attachments [ i ] ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "update attachment [id: %d]: %w" , attachments [ i ] . ID , err )
2016-08-15 18:40:32 -07:00
}
2015-08-11 23:24:40 +08:00
}
}
2022-06-13 17:37:59 +08:00
if err = opts . Issue . LoadAttributes ( ctx ) ; err != nil {
2019-09-20 02:45:38 -03:00
return err
}
2021-10-11 06:40:03 +08:00
2022-06-13 17:37:59 +08:00
return opts . Issue . AddCrossReferences ( ctx , doer , false )
2021-08-13 15:06:18 +02:00
}
2015-09-01 19:07:02 -04:00
// NewIssue creates new issue with labels for repository.
2021-12-10 09:27:50 +08:00
func NewIssue ( repo * repo_model . Repository , issue * Issue , labelIDs [ ] int64 , uuids [ ] string ) ( err error ) {
2022-11-13 04:18:50 +08:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-09-19 19:49:59 +08:00
if err != nil {
2015-09-01 19:07:02 -04:00
return err
}
2021-09-19 19:49:59 +08:00
defer committer . Close ( )
2015-09-01 19:07:02 -04:00
2022-10-16 18:44:16 +08:00
idx , err := db . GetNextResourceIndex ( ctx , "issue_index" , repo . ID )
if err != nil {
return fmt . Errorf ( "generate issue index failed: %w" , err )
}
issue . Index = idx
2022-06-13 17:37:59 +08:00
if err = NewIssueWithIndex ( ctx , issue . Poster , NewIssueOptions {
2016-08-15 18:40:32 -07:00
Repo : repo ,
Issue : issue ,
2017-02-28 20:08:45 -05:00
LabelIDs : labelIDs ,
2016-08-15 18:40:32 -07:00
Attachments : uuids ,
} ) ; err != nil {
2022-06-13 17:37:59 +08:00
if repo_model . IsErrUserDoesNotHaveAccessToRepo ( err ) || IsErrNewIssueInsert ( err ) {
2018-05-09 18:29:04 +02:00
return err
}
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "newIssue: %w" , err )
2015-09-01 19:07:02 -04:00
}
2021-09-19 19:49:59 +08:00
if err = committer . Commit ( ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "Commit: %w" , err )
2016-03-13 23:20:22 -04:00
}
return nil
2014-03-20 16:04:56 -04:00
}
2018-12-13 23:55:43 +08:00
// GetIssueByIndex returns raw issue without loading attributes by index in a repository.
func GetIssueByIndex ( repoID , index int64 ) ( * Issue , error ) {
2021-07-12 20:22:27 +00:00
if index < 1 {
return nil , ErrIssueNotExist { }
}
2015-08-12 17:04:23 +08:00
issue := & Issue {
RepoID : repoID ,
Index : index ,
}
2021-09-23 16:45:36 +01:00
has , err := db . GetEngine ( db . DefaultContext ) . Get ( issue )
2014-03-22 16:00:46 -04:00
if err != nil {
return nil , err
} else if ! has {
2015-08-12 17:04:23 +08:00
return nil , ErrIssueNotExist { 0 , repoID , index }
2014-03-22 16:00:46 -04:00
}
2016-08-26 13:40:53 -07:00
return issue , nil
}
2022-03-17 22:38:35 +05:30
// GetIssueByForeignIndex returns raw issue by foreign ID
func GetIssueByForeignIndex ( ctx context . Context , repoID , foreignIndex int64 ) ( * Issue , error ) {
reference := & foreignreference . ForeignReference {
RepoID : repoID ,
ForeignIndex : strconv . FormatInt ( foreignIndex , 10 ) ,
Type : foreignreference . TypeIssue ,
}
has , err := db . GetEngine ( ctx ) . Get ( reference )
if err != nil {
return nil , err
} else if ! has {
return nil , foreignreference . ErrLocalIndexNotExist {
RepoID : repoID ,
ForeignIndex : foreignIndex ,
Type : foreignreference . TypeIssue ,
}
}
return GetIssueByIndex ( repoID , reference . LocalIndex )
}
2018-12-13 23:55:43 +08:00
// GetIssueWithAttrsByIndex returns issue by index in a repository.
func GetIssueWithAttrsByIndex ( repoID , index int64 ) ( * Issue , error ) {
issue , err := GetIssueByIndex ( repoID , index )
2016-08-26 13:40:53 -07:00
if err != nil {
return nil , err
}
2022-06-13 17:37:59 +08:00
return issue , issue . LoadAttributes ( db . DefaultContext )
2014-03-22 16:00:46 -04:00
}
2022-06-13 17:37:59 +08:00
// GetIssueByID returns an issue by given ID.
func GetIssueByID ( ctx context . Context , id int64 ) ( * Issue , error ) {
2015-08-12 17:04:23 +08:00
issue := new ( Issue )
2022-05-20 22:08:52 +08:00
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( issue )
2014-05-07 16:51:14 -04:00
if err != nil {
return nil , err
} else if ! has {
2015-08-12 17:04:23 +08:00
return nil , ErrIssueNotExist { id , 0 , 0 }
2014-05-07 16:51:14 -04:00
}
2018-12-13 23:55:43 +08:00
return issue , nil
}
// GetIssueWithAttrsByID returns an issue with attributes by given ID.
func GetIssueWithAttrsByID ( id int64 ) ( * Issue , error ) {
2022-06-13 17:37:59 +08:00
issue , err := GetIssueByID ( db . DefaultContext , id )
2018-12-13 23:55:43 +08:00
if err != nil {
return nil , err
}
2022-06-13 17:37:59 +08:00
return issue , issue . LoadAttributes ( db . DefaultContext )
2016-08-14 03:32:24 -07:00
}
2022-05-20 22:08:52 +08:00
// GetIssuesByIDs return issues with the given IDs.
func GetIssuesByIDs ( ctx context . Context , issueIDs [ ] int64 ) ( [ ] * Issue , error ) {
2017-03-14 21:10:35 -04:00
issues := make ( [ ] * Issue , 0 , 10 )
2022-05-20 22:08:52 +08:00
return issues , db . GetEngine ( ctx ) . In ( "id" , issueIDs ) . Find ( & issues )
2017-03-14 21:10:35 -04:00
}
2022-05-20 22:08:52 +08:00
// GetIssueIDsByRepoID returns all issue ids by repo id
func GetIssueIDsByRepoID ( ctx context . Context , repoID int64 ) ( [ ] int64 , error ) {
2021-03-15 02:52:12 +08:00
ids := make ( [ ] int64 , 0 , 10 )
2022-05-20 22:08:52 +08:00
err := db . GetEngine ( ctx ) . Table ( "issue" ) . Cols ( "id" ) . Where ( "repo_id = ?" , repoID ) . Find ( & ids )
2019-02-19 22:39:39 +08:00
return ids , err
}
2016-11-24 09:41:11 +01:00
// IssuesOptions represents options of an issue.
2022-06-13 17:37:59 +08:00
type IssuesOptions struct { //nolint
2021-09-24 19:32:56 +08:00
db . ListOptions
2022-04-25 16:06:24 +02:00
RepoID int64 // overwrites RepoCond if not 0
RepoCond builder . Cond
2020-03-30 13:30:39 +08:00
AssigneeID int64
PosterID int64
MentionedID int64
2021-01-17 17:34:19 +01:00
ReviewRequestedID int64
2022-09-29 21:09:14 +02:00
SubscriberID int64
2020-04-30 06:15:39 +02:00
MilestoneIDs [ ] int64
2020-08-17 04:07:38 +01:00
ProjectID int64
ProjectBoardID int64
2020-03-30 13:30:39 +08:00
IsClosed util . OptionalBool
IsPull util . OptionalBool
LabelIDs [ ] int64
IncludedLabelNames [ ] string
ExcludedLabelNames [ ] string
2021-06-17 08:40:59 +02:00
IncludeMilestones [ ] string
2020-03-30 13:30:39 +08:00
SortType string
IssueIDs [ ] int64
2020-11-23 21:49:36 +01:00
UpdatedAfterUnix int64
UpdatedBeforeUnix int64
2019-10-31 00:06:10 -05:00
// prioritize issues from this repo
PriorityRepoID int64
2021-01-13 05:19:17 +01:00
IsArchived util . OptionalBool
2022-03-29 14:29:02 +08:00
Org * organization . Organization // issues permission scope
Team * organization . Team // issues permission scope
User * user_model . User // issues permission scope
2015-09-02 16:18:09 -04:00
}
2017-01-01 13:15:09 -05:00
// sortIssuesSession sort an issues-related session based on the provided
// sortType string
2019-10-31 00:06:10 -05:00
func sortIssuesSession ( sess * xorm . Session , sortType string , priorityRepoID int64 ) {
2017-01-01 13:15:09 -05:00
switch sortType {
case "oldest" :
2021-12-28 19:12:19 -08:00
sess . Asc ( "issue.created_unix" ) . Asc ( "issue.id" )
2017-01-01 13:15:09 -05:00
case "recentupdate" :
2021-12-28 19:12:19 -08:00
sess . Desc ( "issue.updated_unix" ) . Desc ( "issue.created_unix" ) . Desc ( "issue.id" )
2017-01-01 13:15:09 -05:00
case "leastupdate" :
2021-12-28 19:12:19 -08:00
sess . Asc ( "issue.updated_unix" ) . Asc ( "issue.created_unix" ) . Asc ( "issue.id" )
2017-01-01 13:15:09 -05:00
case "mostcomment" :
2021-12-28 19:12:19 -08:00
sess . Desc ( "issue.num_comments" ) . Desc ( "issue.created_unix" ) . Desc ( "issue.id" )
2017-01-01 13:15:09 -05:00
case "leastcomment" :
2021-12-28 19:12:19 -08:00
sess . Asc ( "issue.num_comments" ) . Desc ( "issue.created_unix" ) . Desc ( "issue.id" )
2017-01-01 13:15:09 -05:00
case "priority" :
2021-12-28 19:12:19 -08:00
sess . Desc ( "issue.priority" ) . Desc ( "issue.created_unix" ) . Desc ( "issue.id" )
2019-03-05 15:39:41 +01:00
case "nearduedate" :
2019-11-09 10:09:01 -03:00
// 253370764800 is 01/01/9999 @ 12:00am (UTC)
2021-06-15 03:15:56 +02:00
sess . Join ( "LEFT" , "milestone" , "issue.milestone_id = milestone.id" ) .
OrderBy ( "CASE " +
"WHEN issue.deadline_unix = 0 AND (milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL) THEN 253370764800 " +
"WHEN milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL THEN issue.deadline_unix " +
"WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " +
2021-12-28 19:12:19 -08:00
"ELSE issue.deadline_unix END ASC" ) .
Desc ( "issue.created_unix" ) .
Desc ( "issue.id" )
2019-03-05 15:39:41 +01:00
case "farduedate" :
2021-06-15 03:15:56 +02:00
sess . Join ( "LEFT" , "milestone" , "issue.milestone_id = milestone.id" ) .
OrderBy ( "CASE " +
"WHEN milestone.deadline_unix IS NULL THEN issue.deadline_unix " +
"WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " +
2021-12-28 19:12:19 -08:00
"ELSE issue.deadline_unix END DESC" ) .
Desc ( "issue.created_unix" ) .
Desc ( "issue.id" )
2019-10-31 00:06:10 -05:00
case "priorityrepo" :
2022-06-05 03:18:50 +08:00
sess . OrderBy ( "CASE " +
"WHEN issue.repo_id = ? THEN 1 " +
"ELSE 2 END ASC" , priorityRepoID ) .
2021-12-28 19:12:19 -08:00
Desc ( "issue.created_unix" ) .
Desc ( "issue.id" )
2021-12-08 07:57:18 +01:00
case "project-column-sorting" :
2021-12-28 19:12:19 -08:00
sess . Asc ( "project_issue.sorting" ) . Desc ( "issue.created_unix" ) . Desc ( "issue.id" )
2017-01-01 13:15:09 -05:00
default :
2021-12-28 19:12:19 -08:00
sess . Desc ( "issue.created_unix" ) . Desc ( "issue.id" )
2017-01-01 13:15:09 -05:00
}
}
2022-04-08 04:39:03 +02:00
func ( opts * IssuesOptions ) setupSessionWithLimit ( sess * xorm . Session ) {
2017-08-02 22:09:16 -07:00
if opts . Page >= 0 && opts . PageSize > 0 {
2017-01-24 21:43:02 -05:00
var start int
if opts . Page == 0 {
start = 0
} else {
2017-08-02 22:09:16 -07:00
start = ( opts . Page - 1 ) * opts . PageSize
2017-01-24 21:43:02 -05:00
}
2017-08-02 22:09:16 -07:00
sess . Limit ( opts . PageSize , start )
2016-03-13 23:20:22 -04:00
}
2022-04-08 04:39:03 +02:00
opts . setupSessionNoLimit ( sess )
}
2016-03-13 23:20:22 -04:00
2022-04-08 04:39:03 +02:00
func ( opts * IssuesOptions ) setupSessionNoLimit ( sess * xorm . Session ) {
2017-01-24 21:43:02 -05:00
if len ( opts . IssueIDs ) > 0 {
sess . In ( "issue.id" , opts . IssueIDs )
}
2014-03-22 16:00:46 -04:00
2022-04-25 16:06:24 +02:00
if opts . RepoID != 0 {
opts . RepoCond = builder . Eq { "issue.repo_id" : opts . RepoID }
}
if opts . RepoCond != nil {
sess . And ( opts . RepoCond )
2014-03-22 16:00:46 -04:00
}
2017-01-24 21:43:02 -05:00
2022-04-25 16:06:24 +02:00
if ! opts . IsClosed . IsNone ( ) {
sess . And ( "issue.is_closed=?" , opts . IsClosed . IsTrue ( ) )
2017-01-24 21:43:02 -05:00
}
2014-03-22 16:00:46 -04:00
2015-09-02 16:18:09 -04:00
if opts . AssigneeID > 0 {
2021-01-17 17:34:19 +01:00
applyAssigneeCondition ( sess , opts . AssigneeID )
2016-12-24 05:33:21 -05:00
}
if opts . PosterID > 0 {
2021-01-17 17:34:19 +01:00
applyPosterCondition ( sess , opts . PosterID )
2014-03-22 13:50:50 -04:00
}
2016-12-24 05:33:21 -05:00
if opts . MentionedID > 0 {
2021-01-17 17:34:19 +01:00
applyMentionedCondition ( sess , opts . MentionedID )
}
if opts . ReviewRequestedID > 0 {
applyReviewRequestedCondition ( sess , opts . ReviewRequestedID )
2016-12-24 05:33:21 -05:00
}
2022-09-29 21:09:14 +02:00
if opts . SubscriberID > 0 {
applySubscribedCondition ( sess , opts . SubscriberID )
}
2020-04-30 06:15:39 +02:00
if len ( opts . MilestoneIDs ) > 0 {
sess . In ( "issue.milestone_id" , opts . MilestoneIDs )
2014-03-22 13:50:50 -04:00
}
2020-11-23 21:49:36 +01:00
if opts . UpdatedAfterUnix != 0 {
sess . And ( builder . Gte { "issue.updated_unix" : opts . UpdatedAfterUnix } )
}
if opts . UpdatedBeforeUnix != 0 {
sess . And ( builder . Lte { "issue.updated_unix" : opts . UpdatedBeforeUnix } )
}
2020-08-17 04:07:38 +01:00
if opts . ProjectID > 0 {
sess . Join ( "INNER" , "project_issue" , "issue.id = project_issue.issue_id" ) .
And ( "project_issue.project_id=?" , opts . ProjectID )
}
if opts . ProjectBoardID != 0 {
if opts . ProjectBoardID > 0 {
sess . In ( "issue.id" , builder . Select ( "issue_id" ) . From ( "project_issue" ) . Where ( builder . Eq { "project_board_id" : opts . ProjectBoardID } ) )
} else {
sess . In ( "issue.id" , builder . Select ( "issue_id" ) . From ( "project_issue" ) . Where ( builder . Eq { "project_board_id" : 0 } ) )
}
}
2017-01-24 21:43:02 -05:00
switch opts . IsPull {
case util . OptionalBoolTrue :
2017-01-28 14:01:07 -02:00
sess . And ( "issue.is_pull=?" , true )
2017-01-24 21:43:02 -05:00
case util . OptionalBoolFalse :
2017-01-28 14:01:07 -02:00
sess . And ( "issue.is_pull=?" , false )
2017-01-24 21:43:02 -05:00
}
2015-09-02 16:18:09 -04:00
2021-01-13 05:19:17 +01:00
if opts . IsArchived != util . OptionalBoolNone {
2021-01-27 06:47:42 +08:00
sess . And ( builder . Eq { "repository.is_archived" : opts . IsArchived . IsTrue ( ) } )
2021-01-13 05:19:17 +01:00
}
2019-01-23 06:10:38 +02:00
if opts . LabelIDs != nil {
for i , labelID := range opts . LabelIDs {
2019-10-23 19:29:14 +03:00
if labelID > 0 {
sess . Join ( "INNER" , fmt . Sprintf ( "issue_label il%d" , i ) ,
fmt . Sprintf ( "issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d" , i , labelID ) )
} else {
sess . Where ( "issue.id not in (select issue_id from issue_label where label_id = ?)" , - labelID )
}
2016-04-26 00:22:03 -04:00
}
2015-08-10 21:47:23 +08:00
}
2020-03-30 13:30:39 +08:00
if len ( opts . IncludedLabelNames ) > 0 {
sess . In ( "issue.id" , BuildLabelNamesIssueIDsCondition ( opts . IncludedLabelNames ) )
}
if len ( opts . ExcludedLabelNames ) > 0 {
sess . And ( builder . NotIn ( "issue.id" , BuildLabelNamesIssueIDsCondition ( opts . ExcludedLabelNames ) ) )
}
2021-06-17 08:40:59 +02:00
if len ( opts . IncludeMilestones ) > 0 {
sess . In ( "issue.milestone_id" ,
builder . Select ( "id" ) .
From ( "milestone" ) .
Where ( builder . In ( "name" , opts . IncludeMilestones ) ) )
}
2021-12-29 21:02:12 +08:00
if opts . User != nil {
2022-05-16 09:49:17 +00:00
sess . And ( issuePullAccessibleRepoCond ( "issue.repo_id" , opts . User . ID , opts . Org , opts . Team , opts . IsPull . IsTrue ( ) ) )
2021-12-29 21:02:12 +08:00
}
}
2022-06-06 16:01:49 +08:00
// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access
func teamUnitsRepoCond ( id string , userID , orgID , teamID int64 , units ... unit . Type ) builder . Cond {
return builder . In ( id ,
builder . Select ( "repo_id" ) . From ( "team_repo" ) . Where (
builder . Eq {
"team_id" : teamID ,
} . And (
builder . Or (
// Check if the user is member of the team.
builder . In (
"team_id" , builder . Select ( "team_id" ) . From ( "team_user" ) . Where (
builder . Eq {
"uid" : userID ,
} ,
) ,
) ,
// Check if the user is in the owner team of the organisation.
builder . Exists ( builder . Select ( "team_id" ) . From ( "team_user" ) .
Where ( builder . Eq {
"org_id" : orgID ,
"team_id" : builder . Select ( "id" ) . From ( "team" ) . Where (
builder . Eq {
"org_id" : orgID ,
"lower_name" : strings . ToLower ( organization . OwnerTeamName ) ,
} ) ,
"uid" : userID ,
} ) ,
) ,
) ) . And (
builder . In (
"team_id" , builder . Select ( "team_id" ) . From ( "team_unit" ) . Where (
builder . Eq {
"`team_unit`.org_id" : orgID ,
} . And (
builder . In ( "`team_unit`.type" , units ) ,
) ,
) ,
) ,
) ,
) )
}
2021-12-29 21:02:12 +08:00
// issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table
2022-03-29 14:29:02 +08:00
func issuePullAccessibleRepoCond ( repoIDstr string , userID int64 , org * organization . Organization , team * organization . Team , isPull bool ) builder . Cond {
2022-01-05 11:37:00 +08:00
cond := builder . NewCond ( )
unitType := unit . TypeIssues
2021-12-29 21:02:12 +08:00
if isPull {
unitType = unit . TypePullRequests
}
if org != nil {
if team != nil {
cond = cond . And ( teamUnitsRepoCond ( repoIDstr , userID , org . ID , team . ID , unitType ) ) // special team member repos
} else {
cond = cond . And (
builder . Or (
2022-06-06 16:01:49 +08:00
repo_model . UserOrgUnitRepoCond ( repoIDstr , userID , org . ID , unitType ) , // team member repos
repo_model . UserOrgPublicUnitRepoCond ( userID , org . ID ) , // user org public non-member repos, TODO: check repo has issues
2021-12-29 21:02:12 +08:00
) ,
)
}
} else {
cond = cond . And (
builder . Or (
2022-06-06 16:01:49 +08:00
repo_model . UserOwnedRepoCond ( userID ) , // owned repos
2022-06-16 01:24:10 +02:00
repo_model . UserAccessRepoCond ( repoIDstr , userID ) , // user can access repo in a unit independent way
2022-06-06 16:01:49 +08:00
repo_model . UserAssignedRepoCond ( repoIDstr , userID ) , // user has been assigned accessible public repos
repo_model . UserMentionedRepoCond ( repoIDstr , userID ) , // user has been mentioned accessible public repos
repo_model . UserCreateIssueRepoCond ( repoIDstr , userID , isPull ) , // user has created issue/pr accessible public repos
2021-12-29 21:02:12 +08:00
) ,
)
}
return cond
2017-08-02 22:09:16 -07:00
}
2021-01-17 17:34:19 +01:00
func applyAssigneeCondition ( sess * xorm . Session , assigneeID int64 ) * xorm . Session {
return sess . Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , assigneeID )
}
func applyPosterCondition ( sess * xorm . Session , posterID int64 ) * xorm . Session {
return sess . And ( "issue.poster_id=?" , posterID )
}
func applyMentionedCondition ( sess * xorm . Session , mentionedID int64 ) * xorm . Session {
return sess . Join ( "INNER" , "issue_user" , "issue.id = issue_user.issue_id" ) .
And ( "issue_user.is_mentioned = ?" , true ) .
And ( "issue_user.uid = ?" , mentionedID )
}
func applyReviewRequestedCondition ( sess * xorm . Session , reviewRequestedID int64 ) * xorm . Session {
return sess . Join ( "INNER" , [ ] string { "review" , "r" } , "issue.id = r.issue_id" ) .
2021-01-18 06:13:15 +01:00
And ( "issue.poster_id <> ?" , reviewRequestedID ) .
2021-01-17 17:34:19 +01:00
And ( "r.type = ?" , ReviewTypeRequest ) .
And ( "r.reviewer_id = ? and r.id in (select max(id) from review where issue_id = r.issue_id and reviewer_id = r.reviewer_id and type in (?, ?, ?))" +
" or r.reviewer_team_id in (select team_id from team_user where uid = ?)" ,
reviewRequestedID , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest , reviewRequestedID )
}
2022-09-29 21:09:14 +02:00
func applySubscribedCondition ( sess * xorm . Session , subscriberID int64 ) * xorm . Session {
return sess . And (
builder .
NotIn ( "issue.id" ,
builder . Select ( "issue_id" ) .
From ( "issue_watch" ) .
Where ( builder . Eq { "is_watching" : false , "user_id" : subscriberID } ) ,
) ,
) . And (
builder . Or (
builder . In ( "issue.id" , builder .
Select ( "issue_id" ) .
From ( "issue_watch" ) .
Where ( builder . Eq { "is_watching" : true , "user_id" : subscriberID } ) ,
) ,
builder . In ( "issue.id" , builder .
Select ( "issue_id" ) .
From ( "comment" ) .
Where ( builder . Eq { "poster_id" : subscriberID } ) ,
) ,
builder . Eq { "issue.poster_id" : subscriberID } ,
builder . In ( "issue.repo_id" , builder .
Select ( "id" ) .
From ( "watch" ) .
2022-10-18 10:01:02 +02:00
Where ( builder . And ( builder . Eq { "user_id" : subscriberID } ,
builder . In ( "mode" , repo_model . WatchModeNormal , repo_model . WatchModeAuto ) ) ) ,
2022-09-29 21:09:14 +02:00
) ,
) ,
)
}
2017-08-02 22:09:16 -07:00
// CountIssuesByRepo map from repoID to number of issues matching the options
func CountIssuesByRepo ( opts * IssuesOptions ) ( map [ int64 ] int64 , error ) {
2021-11-21 23:41:00 +08:00
e := db . GetEngine ( db . DefaultContext )
2017-08-02 22:09:16 -07:00
2021-11-21 23:41:00 +08:00
sess := e . Join ( "INNER" , "repository" , "`issue`.repo_id = `repository`.id" )
2021-01-27 06:47:42 +08:00
2022-04-08 04:39:03 +02:00
opts . setupSessionNoLimit ( sess )
2017-08-02 22:09:16 -07:00
countsSlice := make ( [ ] * struct {
RepoID int64
Count int64
} , 0 , 10 )
if err := sess . GroupBy ( "issue.repo_id" ) .
Select ( "issue.repo_id AS repo_id, COUNT(*) AS count" ) .
Table ( "issue" ) .
Find ( & countsSlice ) ; err != nil {
2022-04-08 04:39:03 +02:00
return nil , fmt . Errorf ( "unable to CountIssuesByRepo: %w" , err )
2017-08-02 22:09:16 -07:00
}
countMap := make ( map [ int64 ] int64 , len ( countsSlice ) )
for _ , c := range countsSlice {
countMap [ c . RepoID ] = c . Count
}
return countMap , nil
}
2020-02-29 00:52:05 -06:00
// GetRepoIDsForIssuesOptions find all repo ids for the given options
2021-11-24 17:49:20 +08:00
func GetRepoIDsForIssuesOptions ( opts * IssuesOptions , user * user_model . User ) ( [ ] int64 , error ) {
2020-02-29 00:52:05 -06:00
repoIDs := make ( [ ] int64 , 0 , 5 )
2021-11-21 23:41:00 +08:00
e := db . GetEngine ( db . DefaultContext )
2020-02-29 00:52:05 -06:00
2021-11-21 23:41:00 +08:00
sess := e . Join ( "INNER" , "repository" , "`issue`.repo_id = `repository`.id" )
2021-01-27 06:47:42 +08:00
2022-04-08 04:39:03 +02:00
opts . setupSessionNoLimit ( sess )
2020-02-29 00:52:05 -06:00
2022-06-16 01:24:10 +02:00
accessCond := repo_model . AccessibleRepositoryCondition ( user , unit . TypeInvalid )
2020-02-29 00:52:05 -06:00
if err := sess . Where ( accessCond ) .
Distinct ( "issue.repo_id" ) .
Table ( "issue" ) .
Find ( & repoIDs ) ; err != nil {
2022-04-08 04:39:03 +02:00
return nil , fmt . Errorf ( "unable to GetRepoIDsForIssuesOptions: %w" , err )
2020-02-29 00:52:05 -06:00
}
return repoIDs , nil
}
2017-08-02 22:09:16 -07:00
// Issues returns a list of issues by given conditions.
func Issues ( opts * IssuesOptions ) ( [ ] * Issue , error ) {
2021-11-21 23:41:00 +08:00
e := db . GetEngine ( db . DefaultContext )
2017-08-02 22:09:16 -07:00
2021-11-21 23:41:00 +08:00
sess := e . Join ( "INNER" , "repository" , "`issue`.repo_id = `repository`.id" )
2022-04-08 04:39:03 +02:00
opts . setupSessionWithLimit ( sess )
2022-05-16 09:49:17 +00:00
2019-10-31 00:06:10 -05:00
sortIssuesSession ( sess , opts . SortType , opts . PriorityRepoID )
2015-08-10 21:47:23 +08:00
2020-09-25 08:30:40 +09:00
issues := make ( [ ] * Issue , 0 , opts . ListOptions . PageSize )
2016-08-26 13:40:53 -07:00
if err := sess . Find ( & issues ) ; err != nil {
2022-04-08 04:39:03 +02:00
return nil , fmt . Errorf ( "unable to query Issues: %w" , err )
2016-08-26 13:40:53 -07:00
}
2017-02-22 22:03:59 +08:00
if err := IssueList ( issues ) . LoadAttributes ( ) ; err != nil {
2022-04-08 04:39:03 +02:00
return nil , fmt . Errorf ( "unable to LoadAttributes for Issues: %w" , err )
2016-08-26 13:40:53 -07:00
}
return issues , nil
2020-09-25 08:30:40 +09:00
}
// CountIssues number return of issues by given conditions.
func CountIssues ( opts * IssuesOptions ) ( int64 , error ) {
2021-11-21 23:41:00 +08:00
e := db . GetEngine ( db . DefaultContext )
2020-09-25 08:30:40 +09:00
2021-11-21 23:41:00 +08:00
sess := e . Select ( "COUNT(issue.id) AS count" ) . Table ( "issue" )
2021-01-27 06:47:42 +08:00
sess . Join ( "INNER" , "repository" , "`issue`.repo_id = `repository`.id" )
2022-04-08 04:39:03 +02:00
opts . setupSessionNoLimit ( sess )
2022-05-16 09:49:17 +00:00
2022-04-25 15:04:42 +08:00
return sess . Count ( )
2014-03-22 13:50:50 -04:00
}
2019-11-18 05:08:20 -03: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 ( issueID int64 ) ( [ ] int64 , error ) {
userIDs := make ( [ ] int64 , 0 , 5 )
2021-09-23 16:45:36 +01:00
return userIDs , db . GetEngine ( db . DefaultContext ) . Table ( "comment" ) .
2019-11-18 05:08:20 -03:00
Cols ( "poster_id" ) .
Where ( "issue_id = ?" , issueID ) .
And ( "type in (?,?,?)" , CommentTypeComment , CommentTypeCode , CommentTypeReview ) .
Distinct ( "poster_id" ) .
Find ( & userIDs )
}
2020-02-28 09:16:41 +01:00
// IsUserParticipantsOfIssue return true if user is participants of an issue
2021-11-24 17:49:20 +08:00
func IsUserParticipantsOfIssue ( user * user_model . User , issue * Issue ) bool {
2022-06-13 17:37:59 +08:00
userIDs , err := issue . GetParticipantIDsByIssue ( db . DefaultContext )
2020-02-28 09:16:41 +01:00
if err != nil {
log . Error ( err . Error ( ) )
return false
2017-03-16 02:34:24 +01:00
}
2020-02-28 09:16:41 +01:00
return util . IsInt64InSlice ( user . ID , userIDs )
2017-03-16 02:34:24 +01:00
}
2019-10-10 13:45:11 -03:00
// UpdateIssueMentions updates issue-user relations for mentioned users.
2021-11-24 17:49:20 +08:00
func UpdateIssueMentions ( ctx context . Context , issueID int64 , mentions [ ] * user_model . User ) error {
2016-07-16 00:36:39 +08:00
if len ( mentions ) == 0 {
return nil
2015-12-21 04:24:11 -08:00
}
2019-10-10 13:45:11 -03:00
ids := make ( [ ] int64 , len ( mentions ) )
for i , u := range mentions {
ids [ i ] = u . ID
2015-12-21 04:24:11 -08:00
}
2019-09-24 13:02:49 +08:00
if err := UpdateIssueUsersByMentions ( ctx , issueID , ids ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "UpdateIssueUsersByMentions: %w" , err )
2015-12-21 04:24:11 -08:00
}
return nil
}
2014-05-07 12:09:30 -04:00
// IssueStats represents issue statistic information.
type IssueStats struct {
OpenCount , ClosedCount int64
2017-02-14 22:15:18 +08:00
YourRepositoriesCount int64
2014-05-07 12:09:30 -04:00
AssignCount int64
CreateCount int64
MentionCount int64
2021-01-17 17:34:19 +01:00
ReviewRequestedCount int64
2014-05-07 12:09:30 -04:00
}
// Filter modes.
const (
2016-11-07 17:24:59 +01:00
FilterModeAll = iota
FilterModeAssign
FilterModeCreate
FilterModeMention
2021-01-17 17:34:19 +01:00
FilterModeReviewRequested
2022-03-23 23:57:09 +01:00
FilterModeYourRepositories
2014-05-07 12:09:30 -04:00
)
2016-11-24 09:41:11 +01:00
// IssueStatsOptions contains parameters accepted by GetIssueStats.
2015-09-02 16:18:09 -04:00
type IssueStatsOptions struct {
2021-01-17 17:34:19 +01:00
RepoID int64
Labels string
MilestoneID int64
AssigneeID int64
MentionedID int64
PosterID int64
ReviewRequestedID int64
IsPull util . OptionalBool
IssueIDs [ ] int64
2015-09-02 16:18:09 -04:00
}
2021-09-19 19:49:59 +08:00
const (
2022-06-13 17:37:59 +08:00
// MaxQueryParameters represents the max query parameters
2021-09-19 19:49:59 +08:00
// When queries are broken down in parts because of the number
// of parameters, attempt to break by this amount
2022-06-13 17:37:59 +08:00
MaxQueryParameters = 300
2021-09-19 19:49:59 +08:00
)
2014-05-07 16:51:14 -04:00
// GetIssueStats returns issue statistic information by given conditions.
2017-01-24 21:43:02 -05:00
func GetIssueStats ( opts * IssueStatsOptions ) ( * IssueStats , error ) {
2022-06-13 17:37:59 +08:00
if len ( opts . IssueIDs ) <= MaxQueryParameters {
2020-02-15 07:51:25 -03:00
return getIssueStatsChunk ( opts , opts . IssueIDs )
}
// If too long a list of IDs is provided, we get the statistics in
// smaller chunks and get accumulates. Note: this could potentially
// get us invalid results. The alternative is to insert the list of
// ids in a temporary table and join from them.
accum := & IssueStats { }
for i := 0 ; i < len ( opts . IssueIDs ) ; {
2022-06-13 17:37:59 +08:00
chunk := i + MaxQueryParameters
2020-02-15 07:51:25 -03:00
if chunk > len ( opts . IssueIDs ) {
chunk = len ( opts . IssueIDs )
}
stats , err := getIssueStatsChunk ( opts , opts . IssueIDs [ i : chunk ] )
if err != nil {
return nil , err
}
accum . OpenCount += stats . OpenCount
accum . ClosedCount += stats . ClosedCount
accum . YourRepositoriesCount += stats . YourRepositoriesCount
accum . AssignCount += stats . AssignCount
accum . CreateCount += stats . CreateCount
accum . OpenCount += stats . MentionCount
2021-01-17 17:34:19 +01:00
accum . ReviewRequestedCount += stats . ReviewRequestedCount
2020-02-15 07:51:25 -03:00
i = chunk
}
return accum , nil
}
func getIssueStatsChunk ( opts * IssueStatsOptions , issueIDs [ ] int64 ) ( * IssueStats , error ) {
2014-05-07 12:09:30 -04:00
stats := & IssueStats { }
2015-07-25 02:52:25 +08:00
2021-11-08 22:14:46 +01:00
countSession := func ( opts * IssueStatsOptions , issueIDs [ ] int64 ) * xorm . Session {
2021-09-23 16:45:36 +01:00
sess := db . GetEngine ( db . DefaultContext ) .
2018-11-29 09:46:30 +08:00
Where ( "issue.repo_id = ?" , opts . RepoID )
2015-08-10 21:47:23 +08:00
2021-11-08 22:14:46 +01:00
if len ( issueIDs ) > 0 {
sess . In ( "issue.id" , issueIDs )
2017-01-24 21:43:02 -05:00
}
2016-05-06 15:40:41 -04:00
if len ( opts . Labels ) > 0 && opts . Labels != "0" {
2016-12-22 03:58:04 -05:00
labelIDs , err := base . StringsToInt64s ( strings . Split ( opts . Labels , "," ) )
if err != nil {
log . Warn ( "Malformed Labels argument: %s" , opts . Labels )
2019-01-23 06:10:38 +02:00
} else {
for i , labelID := range labelIDs {
2019-11-05 09:48:51 +03:00
if labelID > 0 {
sess . Join ( "INNER" , fmt . Sprintf ( "issue_label il%d" , i ) ,
fmt . Sprintf ( "issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d" , i , labelID ) )
} else {
sess . Where ( "issue.id NOT IN (SELECT issue_id FROM issue_label WHERE label_id = ?)" , - labelID )
}
2019-01-23 06:10:38 +02:00
}
2016-04-26 00:22:03 -04:00
}
2016-04-26 06:07:49 +02:00
}
if opts . MilestoneID > 0 {
sess . And ( "issue.milestone_id = ?" , opts . MilestoneID )
}
if opts . AssigneeID > 0 {
2021-01-17 17:34:19 +01:00
applyAssigneeCondition ( sess , opts . AssigneeID )
2016-04-26 06:07:49 +02:00
}
2016-12-24 05:33:21 -05:00
if opts . PosterID > 0 {
2021-01-17 17:34:19 +01:00
applyPosterCondition ( sess , opts . PosterID )
2016-12-24 05:33:21 -05:00
}
2015-07-25 02:52:25 +08:00
2016-12-24 05:33:21 -05:00
if opts . MentionedID > 0 {
2021-01-17 17:34:19 +01:00
applyMentionedCondition ( sess , opts . MentionedID )
}
if opts . ReviewRequestedID > 0 {
applyReviewRequestedCondition ( sess , opts . ReviewRequestedID )
2016-12-24 05:33:21 -05:00
}
2016-04-26 06:07:49 +02:00
2018-11-29 09:46:30 +08:00
switch opts . IsPull {
case util . OptionalBoolTrue :
sess . And ( "issue.is_pull=?" , true )
case util . OptionalBoolFalse :
sess . And ( "issue.is_pull=?" , false )
}
2016-12-24 05:33:21 -05:00
return sess
2015-07-25 02:52:25 +08:00
}
2016-12-24 05:33:21 -05:00
2017-01-24 21:43:02 -05:00
var err error
2021-11-08 22:14:46 +01:00
stats . OpenCount , err = countSession ( opts , issueIDs ) .
2017-06-14 23:09:03 -04:00
And ( "issue.is_closed = ?" , false ) .
Count ( new ( Issue ) )
if err != nil {
return stats , err
2017-01-24 21:43:02 -05:00
}
2021-11-08 22:14:46 +01:00
stats . ClosedCount , err = countSession ( opts , issueIDs ) .
2017-06-14 23:09:03 -04:00
And ( "issue.is_closed = ?" , true ) .
Count ( new ( Issue ) )
2017-02-14 22:15:18 +08:00
return stats , err
2014-05-07 12:09:30 -04:00
}
2017-12-25 18:25:16 -05:00
// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats.
type UserIssueStatsOptions struct {
2021-12-29 21:02:12 +08:00
UserID int64
RepoIDs [ ] int64
FilterMode int
IsPull bool
IsClosed bool
IssueIDs [ ] int64
IsArchived util . OptionalBool
LabelIDs [ ] int64
2022-03-23 23:57:09 +01:00
RepoCond builder . Cond
2022-03-29 14:29:02 +08:00
Org * organization . Organization
Team * organization . Team
2017-12-25 18:25:16 -05:00
}
2014-05-07 16:51:14 -04:00
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
2017-12-25 18:25:16 -05:00
func GetUserIssueStats ( opts UserIssueStatsOptions ) ( * IssueStats , error ) {
var err error
2014-05-07 12:09:30 -04:00
stats := & IssueStats { }
2015-08-25 22:58:34 +08:00
2017-12-25 18:25:16 -05:00
cond := builder . NewCond ( )
cond = cond . And ( builder . Eq { "issue.is_pull" : opts . IsPull } )
2019-12-02 04:50:36 +01:00
if len ( opts . RepoIDs ) > 0 {
cond = cond . And ( builder . In ( "issue.repo_id" , opts . RepoIDs ) )
2015-09-02 16:18:09 -04:00
}
2020-02-29 00:52:05 -06:00
if len ( opts . IssueIDs ) > 0 {
cond = cond . And ( builder . In ( "issue.id" , opts . IssueIDs ) )
}
2022-03-23 23:57:09 +01:00
if opts . RepoCond != nil {
cond = cond . And ( opts . RepoCond )
}
2015-09-02 16:18:09 -04:00
2021-12-29 21:02:12 +08:00
if opts . UserID > 0 {
cond = cond . And ( issuePullAccessibleRepoCond ( "issue.repo_id" , opts . UserID , opts . Org , opts . Team , opts . IsPull ) )
}
2021-01-02 01:49:42 +08:00
sess := func ( cond builder . Cond ) * xorm . Session {
2021-09-23 16:45:36 +01:00
s := db . GetEngine ( db . DefaultContext ) . Where ( cond )
2021-01-02 01:49:42 +08:00
if len ( opts . LabelIDs ) > 0 {
s . Join ( "INNER" , "issue_label" , "issue_label.issue_id = issue.id" ) .
In ( "issue_label.label_id" , opts . LabelIDs )
}
2021-12-29 21:02:12 +08:00
if opts . UserID > 0 || opts . IsArchived != util . OptionalBoolNone {
s . Join ( "INNER" , "repository" , "issue.repo_id = repository.id" )
if opts . IsArchived != util . OptionalBoolNone {
s . And ( builder . Eq { "repository.is_archived" : opts . IsArchived . IsTrue ( ) } )
}
2021-01-13 05:19:17 +01:00
}
2021-01-02 01:49:42 +08:00
return s
}
2017-12-25 18:25:16 -05:00
switch opts . FilterMode {
2022-03-23 23:57:09 +01:00
case FilterModeAll , FilterModeYourRepositories :
2021-12-29 21:02:12 +08:00
stats . OpenCount , err = sess ( cond ) .
2021-01-17 17:34:19 +01:00
And ( "issue.is_closed = ?" , false ) .
2017-02-14 22:15:18 +08:00
Count ( new ( Issue ) )
2017-12-25 18:25:16 -05:00
if err != nil {
return nil , err
}
2021-12-29 21:02:12 +08:00
stats . ClosedCount , err = sess ( cond ) .
2021-01-17 17:34:19 +01:00
And ( "issue.is_closed = ?" , true ) .
2017-02-14 22:15:18 +08:00
Count ( new ( Issue ) )
2017-12-25 18:25:16 -05:00
if err != nil {
return nil , err
}
2016-11-07 17:24:59 +01:00
case FilterModeAssign :
2021-01-17 17:34:19 +01:00
stats . OpenCount , err = applyAssigneeCondition ( sess ( cond ) , opts . UserID ) .
And ( "issue.is_closed = ?" , false ) .
2017-02-14 22:15:18 +08:00
Count ( new ( Issue ) )
2017-12-25 18:25:16 -05:00
if err != nil {
return nil , err
}
2021-01-17 17:34:19 +01:00
stats . ClosedCount , err = applyAssigneeCondition ( sess ( cond ) , opts . UserID ) .
And ( "issue.is_closed = ?" , true ) .
2017-02-14 22:15:18 +08:00
Count ( new ( Issue ) )
2017-12-25 18:25:16 -05:00
if err != nil {
return nil , err
}
2016-11-07 17:24:59 +01:00
case FilterModeCreate :
2021-01-17 17:34:19 +01:00
stats . OpenCount , err = applyPosterCondition ( sess ( cond ) , opts . UserID ) .
And ( "issue.is_closed = ?" , false ) .
2017-02-14 22:15:18 +08:00
Count ( new ( Issue ) )
2017-12-25 18:25:16 -05:00
if err != nil {
return nil , err
}
2021-01-17 17:34:19 +01:00
stats . ClosedCount , err = applyPosterCondition ( sess ( cond ) , opts . UserID ) .
And ( "issue.is_closed = ?" , true ) .
2017-02-14 22:15:18 +08:00
Count ( new ( Issue ) )
2017-12-25 18:25:16 -05:00
if err != nil {
return nil , err
}
2019-09-18 10:24:44 +03:00
case FilterModeMention :
2021-01-17 17:34:19 +01:00
stats . OpenCount , err = applyMentionedCondition ( sess ( cond ) , opts . UserID ) .
And ( "issue.is_closed = ?" , false ) .
2019-09-18 10:24:44 +03:00
Count ( new ( Issue ) )
if err != nil {
return nil , err
}
2021-01-17 17:34:19 +01:00
stats . ClosedCount , err = applyMentionedCondition ( sess ( cond ) , opts . UserID ) .
And ( "issue.is_closed = ?" , true ) .
Count ( new ( Issue ) )
if err != nil {
return nil , err
}
case FilterModeReviewRequested :
stats . OpenCount , err = applyReviewRequestedCondition ( sess ( cond ) , opts . UserID ) .
And ( "issue.is_closed = ?" , false ) .
Count ( new ( Issue ) )
if err != nil {
return nil , err
}
stats . ClosedCount , err = applyReviewRequestedCondition ( sess ( cond ) , opts . UserID ) .
And ( "issue.is_closed = ?" , true ) .
2019-09-18 10:24:44 +03:00
Count ( new ( Issue ) )
if err != nil {
return nil , err
}
2017-12-25 18:25:16 -05:00
}
cond = cond . And ( builder . Eq { "issue.is_closed" : opts . IsClosed } )
2021-01-17 17:34:19 +01:00
stats . AssignCount , err = applyAssigneeCondition ( sess ( cond ) , opts . UserID ) . Count ( new ( Issue ) )
2017-12-25 18:25:16 -05:00
if err != nil {
return nil , err
}
2021-01-17 17:34:19 +01:00
stats . CreateCount , err = applyPosterCondition ( sess ( cond ) , opts . UserID ) . Count ( new ( Issue ) )
2017-12-25 18:25:16 -05:00
if err != nil {
return nil , err
}
2021-01-17 17:34:19 +01:00
stats . MentionCount , err = applyMentionedCondition ( sess ( cond ) , opts . UserID ) . Count ( new ( Issue ) )
2019-09-18 10:24:44 +03:00
if err != nil {
return nil , err
}
2021-12-29 21:02:12 +08:00
stats . YourRepositoriesCount , err = sess ( cond ) . Count ( new ( Issue ) )
2021-01-17 17:34:19 +01:00
if err != nil {
return nil , err
}
stats . ReviewRequestedCount , err = applyReviewRequestedCondition ( sess ( cond ) , opts . UserID ) . Count ( new ( Issue ) )
2017-12-25 18:25:16 -05:00
if err != nil {
return nil , err
2015-08-25 22:58:34 +08:00
}
2017-12-25 18:25:16 -05:00
return stats , nil
2014-05-07 12:09:30 -04:00
}
2015-08-25 22:58:34 +08:00
// GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
2021-03-15 02:52:12 +08:00
func GetRepoIssueStats ( repoID , uid int64 , filterMode int , isPull bool ) ( numOpen , numClosed int64 ) {
2016-04-26 06:07:49 +02:00
countSession := func ( isClosed , isPull bool , repoID int64 ) * xorm . Session {
2021-09-23 16:45:36 +01:00
sess := db . GetEngine ( db . DefaultContext ) .
2017-02-09 17:59:57 +08:00
Where ( "is_closed = ?" , isClosed ) .
2016-04-26 00:22:03 -04:00
And ( "is_pull = ?" , isPull ) .
And ( "repo_id = ?" , repoID )
2015-09-02 16:18:09 -04:00
2016-04-26 06:07:49 +02:00
return sess
2015-09-02 16:18:09 -04:00
}
2016-04-26 06:07:49 +02:00
openCountSession := countSession ( false , isPull , repoID )
closedCountSession := countSession ( true , isPull , repoID )
2015-08-25 22:58:34 +08:00
switch filterMode {
2016-11-07 17:24:59 +01:00
case FilterModeAssign :
2021-01-17 17:34:19 +01:00
applyAssigneeCondition ( openCountSession , uid )
applyAssigneeCondition ( closedCountSession , uid )
2016-11-07 17:24:59 +01:00
case FilterModeCreate :
2021-01-17 17:34:19 +01:00
applyPosterCondition ( openCountSession , uid )
applyPosterCondition ( closedCountSession , uid )
2015-08-25 22:58:34 +08:00
}
2017-02-14 22:15:18 +08:00
openResult , _ := openCountSession . Count ( new ( Issue ) )
closedResult , _ := closedCountSession . Count ( new ( Issue ) )
2016-04-26 06:07:49 +02:00
return openResult , closedResult
2015-08-25 22:58:34 +08:00
}
2019-02-21 13:01:28 +08:00
// SearchIssueIDsByKeyword search issues on database
2022-01-27 10:30:51 +02:00
func SearchIssueIDsByKeyword ( ctx context . Context , kw string , repoIDs [ ] int64 , limit , start int ) ( int64 , [ ] int64 , error ) {
2021-03-15 02:52:12 +08:00
repoCond := builder . In ( "repo_id" , repoIDs )
subQuery := builder . Select ( "id" ) . From ( "issue" ) . Where ( repoCond )
cond := builder . And (
2019-02-21 13:01:28 +08:00
repoCond ,
builder . Or (
2022-07-29 00:58:04 +08:00
db . BuildCaseInsensitiveLike ( "name" , kw ) ,
db . BuildCaseInsensitiveLike ( "content" , kw ) ,
2019-02-21 13:01:28 +08:00
builder . In ( "id" , builder . Select ( "issue_id" ) .
From ( "comment" ) .
Where ( builder . And (
builder . Eq { "type" : CommentTypeComment } ,
builder . In ( "issue_id" , subQuery ) ,
2022-07-29 00:58:04 +08:00
db . BuildCaseInsensitiveLike ( "content" , kw ) ,
2019-02-21 13:01:28 +08:00
) ) ,
) ,
) ,
)
2021-03-15 02:52:12 +08:00
ids := make ( [ ] int64 , 0 , limit )
res := make ( [ ] struct {
2021-03-06 23:11:12 +08:00
ID int64
UpdatedUnix int64
} , 0 , limit )
2022-01-27 10:30:51 +02:00
err := db . GetEngine ( ctx ) . Distinct ( "id" , "updated_unix" ) . Table ( "issue" ) . Where ( cond ) .
2021-03-06 23:11:12 +08:00
OrderBy ( "`updated_unix` DESC" ) . Limit ( limit , start ) .
Find ( & res )
2019-02-21 13:01:28 +08:00
if err != nil {
return 0 , nil , err
}
2021-03-06 23:11:12 +08:00
for _ , r := range res {
ids = append ( ids , r . ID )
}
2019-02-21 13:01:28 +08:00
2022-01-27 10:30:51 +02:00
total , err := db . GetEngine ( ctx ) . Distinct ( "id" ) . Table ( "issue" ) . Where ( cond ) . Count ( )
2019-02-21 13:01:28 +08:00
if err != nil {
return 0 , nil , err
}
return total , ids , nil
}
2020-01-01 23:51:10 +01:00
// UpdateIssueByAPI updates all allowed fields of given issue.
2020-05-16 22:05:19 +01:00
// If the issue status is changed a statusChangeComment is returned
// similarly if the title is changed the titleChanged bool is set to true
2021-11-24 17:49:20 +08:00
func UpdateIssueByAPI ( issue * Issue , doer * user_model . User ) ( statusChangeComment * Comment , titleChanged bool , err error ) {
2022-11-13 04:18:50 +08:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-11-19 21:39:57 +08:00
if err != nil {
2020-05-16 22:05:19 +01:00
return nil , false , err
}
2021-11-19 21:39:57 +08:00
defer committer . Close ( )
2020-05-16 22:05:19 +01:00
2022-04-08 17:11:15 +08:00
if err := issue . LoadRepo ( ctx ) ; err != nil {
2022-10-24 21:29:17 +02:00
return nil , false , fmt . Errorf ( "loadRepo: %w" , err )
2020-05-16 22:05:19 +01:00
}
// Reload the issue
2022-06-13 17:37:59 +08:00
currentIssue , err := GetIssueByID ( ctx , issue . ID )
2020-05-16 22:05:19 +01:00
if err != nil {
return nil , false , err
2019-09-20 02:45:38 -03:00
}
2020-01-01 23:51:10 +01:00
2022-05-20 22:08:52 +08:00
if _ , err := db . GetEngine ( ctx ) . ID ( issue . ID ) . Cols (
2020-05-16 22:05:19 +01:00
"name" , "content" , "milestone_id" , "priority" ,
"deadline_unix" , "updated_unix" , "is_locked" ) .
2020-01-01 23:51:10 +01:00
Update ( issue ) ; err != nil {
2020-05-16 22:05:19 +01:00
return nil , false , err
2019-09-20 02:45:38 -03:00
}
2020-01-02 08:54:22 +01:00
2020-05-16 22:05:19 +01:00
titleChanged = currentIssue . Title != issue . Title
if titleChanged {
2021-03-15 02:52:12 +08:00
opts := & CreateCommentOptions {
2020-05-16 22:05:19 +01:00
Type : CommentTypeChangeTitle ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
OldTitle : currentIssue . Title ,
NewTitle : issue . Title ,
}
2022-04-08 17:11:15 +08:00
_ , err := CreateCommentCtx ( ctx , opts )
2020-05-16 22:05:19 +01:00
if err != nil {
2022-10-24 21:29:17 +02:00
return nil , false , fmt . Errorf ( "createComment: %w" , err )
2020-05-16 22:05:19 +01:00
}
2020-01-02 08:54:22 +01:00
}
2020-05-16 22:05:19 +01:00
if currentIssue . IsClosed != issue . IsClosed {
2022-03-29 22:57:33 +08:00
statusChangeComment , err = doChangeIssueStatus ( ctx , issue , doer , false )
2020-05-16 22:05:19 +01:00
if err != nil {
return nil , false , err
}
2019-09-20 02:45:38 -03:00
}
2020-05-16 22:05:19 +01:00
2022-06-13 17:37:59 +08:00
if err := issue . AddCrossReferences ( ctx , doer , true ) ; err != nil {
2020-05-16 22:05:19 +01:00
return nil , false , err
}
2021-11-19 21:39:57 +08:00
return statusChangeComment , titleChanged , committer . Commit ( )
2015-10-24 03:36:47 -04:00
}
2018-05-01 21:05:28 +02:00
// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
2021-11-24 17:49:20 +08:00
func UpdateIssueDeadline ( issue * Issue , deadlineUnix timeutil . TimeStamp , doer * user_model . User ) ( err error ) {
2018-05-01 21:05:28 +02:00
// if the deadline hasn't changed do nothing
if issue . DeadlineUnix == deadlineUnix {
return nil
}
2022-11-13 04:18:50 +08:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-11-19 21:39:57 +08:00
if err != nil {
2018-05-01 21:05:28 +02:00
return err
}
2021-11-19 21:39:57 +08:00
defer committer . Close ( )
2018-05-01 21:05:28 +02:00
// Update the deadline
2022-04-08 17:11:15 +08:00
if err = UpdateIssueCols ( ctx , & Issue { ID : issue . ID , DeadlineUnix : deadlineUnix } , "deadline_unix" ) ; err != nil {
2018-05-01 21:05:28 +02:00
return err
}
// Make the comment
2021-11-19 21:39:57 +08:00
if _ , err = createDeadlineComment ( ctx , doer , issue , deadlineUnix ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "createRemovedDueDateComment: %w" , err )
2018-05-01 21:05:28 +02:00
}
2021-11-19 21:39:57 +08:00
return committer . Commit ( )
2018-05-01 21:05:28 +02:00
}
2018-07-17 23:23:58 +02:00
2022-06-13 17:37:59 +08:00
// DeleteInIssue delete records in beans with external key issue_id = ?
func DeleteInIssue ( ctx context . Context , issueID int64 , beans ... interface { } ) error {
2022-05-20 22:08:52 +08:00
e := db . GetEngine ( ctx )
2022-03-01 01:20:15 +01:00
for _ , bean := range beans {
if _ , err := e . In ( "issue_id" , issueID ) . Delete ( bean ) ; err != nil {
return err
}
}
return nil
}
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
}
if ! util . IsInt64InSlice ( issue . PosterID , userIDs ) {
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
func ( issue * Issue ) BlockedByDependencies ( 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.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
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-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
}
2021-01-02 18:04:02 +01:00
// FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database.
2022-03-29 22:57:33 +08:00
func FindAndUpdateIssueMentions ( ctx context . Context , issue * Issue , doer * user_model . User , content string ) ( mentions [ ] * user_model . User , err error ) {
2021-01-02 18:04:02 +01:00
rawMentions := references . FindAllMentionsMarkdown ( content )
2022-03-29 22:57:33 +08:00
mentions , err = ResolveIssueMentionsByVisibility ( ctx , issue , doer , rawMentions )
2021-01-02 18:04:02 +01:00
if err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "UpdateIssueMentions [%d]: %w" , issue . ID , err )
2021-01-02 18:04:02 +01:00
}
if err = UpdateIssueMentions ( ctx , issue . ID , mentions ) ; err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "UpdateIssueMentions [%d]: %w" , issue . ID , err )
2021-01-02 18:04:02 +01:00
}
2022-06-20 12:02:49 +02:00
return mentions , err
2021-01-02 18:04:02 +01:00
}
2022-03-29 22:57:33 +08:00
// ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that
2019-10-10 13:45:11 -03:00
// don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
2022-03-29 22:57:33 +08:00
func ResolveIssueMentionsByVisibility ( ctx context . Context , issue * Issue , doer * user_model . User , mentions [ ] string ) ( users [ ] * user_model . User , err error ) {
2019-10-10 13:45:11 -03:00
if len ( mentions ) == 0 {
return
}
2022-04-08 17:11:15 +08:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2019-10-10 13:45:11 -03:00
return
}
2020-12-21 23:39:28 +08:00
resolved := make ( map [ string ] bool , 10 )
var mentionTeams [ ] string
2021-12-10 09:27:50 +08:00
if err := issue . Repo . GetOwner ( ctx ) ; err != nil {
2020-12-21 23:39:28 +08:00
return nil , err
}
repoOwnerIsOrg := issue . Repo . Owner . IsOrganization ( )
if repoOwnerIsOrg {
mentionTeams = make ( [ ] string , 0 , 5 )
}
2019-10-10 13:45:11 -03:00
resolved [ doer . LowerName ] = true
for _ , name := range mentions {
name := strings . ToLower ( name )
if _ , ok := resolved [ name ] ; ok {
continue
}
2020-12-21 23:39:28 +08:00
if repoOwnerIsOrg && strings . Contains ( name , "/" ) {
names := strings . Split ( name , "/" )
if len ( names ) < 2 || names [ 0 ] != issue . Repo . Owner . LowerName {
continue
}
mentionTeams = append ( mentionTeams , names [ 1 ] )
resolved [ name ] = true
} else {
resolved [ name ] = false
}
2019-10-10 13:45:11 -03:00
}
2020-12-21 23:39:28 +08:00
if issue . Repo . Owner . IsOrganization ( ) && len ( mentionTeams ) > 0 {
2022-03-29 14:29:02 +08:00
teams := make ( [ ] * organization . Team , 0 , len ( mentionTeams ) )
2021-09-23 16:45:36 +01:00
if err := db . GetEngine ( ctx ) .
2019-10-10 13:45:11 -03:00
Join ( "INNER" , "team_repo" , "team_repo.team_id = team.id" ) .
Where ( "team_repo.repo_id=?" , issue . Repo . ID ) .
2020-12-21 23:39:28 +08:00
In ( "team.lower_name" , mentionTeams ) .
2019-10-10 13:45:11 -03:00
Find ( & teams ) ; err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "find mentioned teams: %w" , err )
2019-10-10 13:45:11 -03:00
}
if len ( teams ) != 0 {
checked := make ( [ ] int64 , 0 , len ( teams ) )
2021-11-10 03:57:58 +08:00
unittype := unit . TypeIssues
2019-10-10 13:45:11 -03:00
if issue . IsPull {
2021-11-10 03:57:58 +08:00
unittype = unit . TypePullRequests
2019-10-10 13:45:11 -03:00
}
for _ , team := range teams {
2022-01-05 11:37:00 +08:00
if team . AccessMode >= perm . AccessModeAdmin {
2019-10-10 13:45:11 -03:00
checked = append ( checked , team . ID )
2020-12-21 23:39:28 +08:00
resolved [ issue . Repo . Owner . LowerName + "/" + team . LowerName ] = true
2019-10-10 13:45:11 -03:00
continue
}
2022-03-29 14:29:02 +08:00
has , err := db . GetEngine ( ctx ) . Get ( & organization . TeamUnit { OrgID : issue . Repo . Owner . ID , TeamID : team . ID , Type : unittype } )
2019-10-10 13:45:11 -03:00
if err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "get team units (%d): %w" , team . ID , err )
2019-10-10 13:45:11 -03:00
}
if has {
checked = append ( checked , team . ID )
2020-12-21 23:39:28 +08:00
resolved [ issue . Repo . Owner . LowerName + "/" + team . LowerName ] = true
2019-10-10 13:45:11 -03:00
}
}
if len ( checked ) != 0 {
2021-11-24 17:49:20 +08:00
teamusers := make ( [ ] * user_model . User , 0 , 20 )
2021-09-23 16:45:36 +01:00
if err := db . GetEngine ( ctx ) .
2019-10-10 13:45:11 -03:00
Join ( "INNER" , "team_user" , "team_user.uid = `user`.id" ) .
In ( "`team_user`.team_id" , checked ) .
And ( "`user`.is_active = ?" , true ) .
And ( "`user`.prohibit_login = ?" , false ) .
Find ( & teamusers ) ; err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "get teams users: %w" , err )
2019-10-10 13:45:11 -03:00
}
if len ( teamusers ) > 0 {
2021-11-24 17:49:20 +08:00
users = make ( [ ] * user_model . User , 0 , len ( teamusers ) )
2019-10-10 13:45:11 -03:00
for _ , user := range teamusers {
if already , ok := resolved [ user . LowerName ] ; ! ok || ! already {
users = append ( users , user )
resolved [ user . LowerName ] = true
}
}
}
}
}
2020-12-21 23:39:28 +08:00
}
2019-10-10 13:45:11 -03:00
2020-12-21 23:39:28 +08:00
// Remove names already in the list to avoid querying the database if pending names remain
mentionUsers := make ( [ ] string , 0 , len ( resolved ) )
for name , already := range resolved {
if ! already {
mentionUsers = append ( mentionUsers , name )
2019-10-10 13:45:11 -03:00
}
}
2020-12-21 23:39:28 +08:00
if len ( mentionUsers ) == 0 {
return
}
if users == nil {
2021-11-24 17:49:20 +08:00
users = make ( [ ] * user_model . User , 0 , len ( mentionUsers ) )
2020-12-21 23:39:28 +08:00
}
2019-10-10 13:45:11 -03:00
2021-11-24 17:49:20 +08:00
unchecked := make ( [ ] * user_model . User , 0 , len ( mentionUsers ) )
2021-09-23 16:45:36 +01:00
if err := db . GetEngine ( ctx ) .
2019-10-10 13:45:11 -03:00
Where ( "`user`.is_active = ?" , true ) .
And ( "`user`.prohibit_login = ?" , false ) .
2020-12-21 23:39:28 +08:00
In ( "`user`.lower_name" , mentionUsers ) .
2019-10-10 13:45:11 -03:00
Find ( & unchecked ) ; err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "find mentioned users: %w" , err )
2019-10-10 13:45:11 -03:00
}
for _ , user := range unchecked {
if already := resolved [ user . LowerName ] ; already || user . IsOrganization ( ) {
continue
}
// Normal users must have read access to the referencing issue
2022-05-11 18:09:36 +08:00
perm , err := access_model . GetUserRepoPermission ( ctx , issue . Repo , user )
2019-10-10 13:45:11 -03:00
if err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "GetUserRepoPermission [%d]: %w" , user . ID , err )
2019-10-10 13:45:11 -03:00
}
if ! perm . CanReadIssuesOrPulls ( issue . IsPull ) {
continue
}
users = append ( users , user )
}
2022-06-20 12:02:49 +02:00
return users , err
2019-10-10 13:45:11 -03:00
}
2019-10-14 14:10:42 +08:00
// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID
2021-11-08 08:04:13 +01:00
func UpdateIssuesMigrationsByType ( gitServiceType api . GitServiceType , originalAuthorID string , posterID int64 ) error {
2021-09-23 16:45:36 +01:00
_ , err := db . GetEngine ( db . DefaultContext ) . Table ( "issue" ) .
2019-10-14 14:10:42 +08:00
Where ( "repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)" , gitServiceType ) .
And ( "original_author_id = ?" , originalAuthorID ) .
Update ( map [ string ] interface { } {
"poster_id" : posterID ,
"original_author" : "" ,
"original_author_id" : 0 ,
} )
return err
}
2020-01-15 19:14:07 +08: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 ,
} ) ,
)
}
2020-01-15 19:14:07 +08:00
// UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID
2021-11-08 08:04:13 +01:00
func UpdateReactionsMigrationsByType ( gitServiceType api . GitServiceType , originalAuthorID string , userID int64 ) error {
2021-09-23 16:45:36 +01:00
_ , err := db . GetEngine ( db . DefaultContext ) . Table ( "reaction" ) .
2020-02-18 08:42:13 +08:00
Where ( "original_author_id = ?" , originalAuthorID ) .
And ( migratedIssueCond ( gitServiceType ) ) .
2020-01-15 19:14:07 +08:00
Update ( map [ string ] interface { } {
"user_id" : userID ,
"original_author" : "" ,
"original_author_id" : 0 ,
} )
return err
}
2020-05-29 15:24:15 +02:00
2022-06-13 17:37:59 +08:00
// DeleteIssuesByRepoID deletes issues by repositories id
func DeleteIssuesByRepoID ( ctx context . Context , repoID int64 ) ( attachmentPaths [ ] string , err error ) {
2020-05-29 15:24:15 +02:00
deleteCond := builder . Select ( "id" ) . From ( "issue" ) . Where ( builder . Eq { "issue.repo_id" : repoID } )
2022-05-20 22:08:52 +08:00
sess := db . GetEngine ( ctx )
2021-10-11 06:40:03 +08:00
// Delete content histories
if _ , err = sess . In ( "issue_id" , deleteCond ) .
2022-06-13 17:37:59 +08:00
Delete ( & ContentHistory { } ) ; err != nil {
2021-10-11 06:40:03 +08:00
return
}
2020-05-29 15:24:15 +02:00
// Delete comments and attachments
if _ , err = sess . In ( "issue_id" , deleteCond ) .
Delete ( & Comment { } ) ; err != nil {
return
}
// Dependencies for issues in this repository
if _ , err = sess . In ( "issue_id" , deleteCond ) .
Delete ( & IssueDependency { } ) ; err != nil {
return
}
// Delete dependencies for issues in other repositories
if _ , err = sess . In ( "dependency_id" , deleteCond ) .
Delete ( & IssueDependency { } ) ; err != nil {
return
}
if _ , err = sess . In ( "issue_id" , deleteCond ) .
Delete ( & IssueUser { } ) ; err != nil {
return
}
if _ , err = sess . In ( "issue_id" , deleteCond ) .
2022-06-13 17:37:59 +08:00
Delete ( & Reaction { } ) ; err != nil {
2020-05-29 15:24:15 +02:00
return
}
if _ , err = sess . In ( "issue_id" , deleteCond ) .
Delete ( & IssueWatch { } ) ; err != nil {
return
}
if _ , err = sess . In ( "issue_id" , deleteCond ) .
Delete ( & Stopwatch { } ) ; err != nil {
return
}
if _ , err = sess . In ( "issue_id" , deleteCond ) .
Delete ( & TrackedTime { } ) ; err != nil {
return
}
2020-08-17 04:07:38 +01:00
if _ , err = sess . In ( "issue_id" , deleteCond ) .
2022-03-29 22:16:31 +08:00
Delete ( & project_model . ProjectIssue { } ) ; err != nil {
2020-08-17 04:07:38 +01:00
return
}
2020-09-04 03:36:56 +02:00
if _ , err = sess . In ( "dependent_issue_id" , deleteCond ) .
Delete ( & Comment { } ) ; err != nil {
return
}
2021-11-19 21:39:57 +08:00
var attachments [ ] * repo_model . Attachment
2020-05-29 15:24:15 +02:00
if err = sess . In ( "issue_id" , deleteCond ) .
Find ( & attachments ) ; err != nil {
return
}
2020-08-18 12:23:45 +08:00
2020-05-29 15:24:15 +02:00
for j := range attachments {
2020-08-18 12:23:45 +08:00
attachmentPaths = append ( attachmentPaths , attachments [ j ] . RelativePath ( ) )
2020-05-29 15:24:15 +02:00
}
if _ , err = sess . In ( "issue_id" , deleteCond ) .
2021-11-19 21:39:57 +08:00
Delete ( & repo_model . Attachment { } ) ; err != nil {
2020-05-29 15:24:15 +02:00
return
}
2022-05-20 22:08:52 +08:00
if _ , err = db . DeleteByBean ( ctx , & Issue { RepoID : repoID } ) ; err != nil {
2020-05-29 15:24:15 +02:00
return
}
2022-06-20 12:02:49 +02:00
return attachmentPaths , err
2020-05-29 15:24:15 +02:00
}
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
// CountOrphanedIssues count issues without a repo
func CountOrphanedIssues ( ) ( int64 , error ) {
return db . GetEngine ( db . DefaultContext ) . Table ( "issue" ) .
Join ( "LEFT" , "repository" , "issue.repo_id=repository.id" ) .
Where ( builder . IsNull { "repository.id" } ) .
Select ( "COUNT(`issue`.`id`)" ) .
Count ( )
}
// DeleteOrphanedIssues delete issues without a repo
func DeleteOrphanedIssues ( ) error {
2022-11-13 04:18:50 +08:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2022-06-13 17:37:59 +08:00
if err != nil {
return err
}
defer committer . Close ( )
var ids [ ] int64
if err := db . GetEngine ( ctx ) . Table ( "issue" ) . Distinct ( "issue.repo_id" ) .
Join ( "LEFT" , "repository" , "issue.repo_id=repository.id" ) .
Where ( builder . IsNull { "repository.id" } ) . GroupBy ( "issue.repo_id" ) .
Find ( & ids ) ; err != nil {
return err
}
var attachmentPaths [ ] string
for i := range ids {
paths , err := DeleteIssuesByRepoID ( ctx , ids [ i ] )
if err != nil {
return err
}
attachmentPaths = append ( attachmentPaths , paths ... )
}
if err := committer . Commit ( ) ; err != nil {
return err
}
committer . Close ( )
// Remove issue attachment files.
for i := range attachmentPaths {
2022-10-17 07:29:26 +08:00
system_model . RemoveAllWithNotice ( db . DefaultContext , "Delete issue attachment" , attachmentPaths [ i ] )
2022-06-13 17:37:59 +08:00
}
return nil
}