2014-03-21 01:34:56 +05:30
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
2014-03-22 23:20:50 +05:30
import (
2015-08-10 12:12:50 +05:30
"fmt"
2017-03-03 20:05:42 +05:30
"path"
2018-01-03 14:04:13 +05:30
"regexp"
2017-01-30 18:16:45 +05:30
"sort"
2014-03-22 23:20:50 +05:30
"strings"
2014-05-08 02:21:14 +05:30
2016-11-10 21:54:48 +05:30
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2019-05-11 15:51:34 +05:30
api "code.gitea.io/gitea/modules/structs"
2019-08-15 20:16:21 +05:30
"code.gitea.io/gitea/modules/timeutil"
2017-01-25 08:13:02 +05:30
"code.gitea.io/gitea/modules/util"
2017-12-26 04:55:16 +05:30
"github.com/go-xorm/xorm"
2019-08-23 22:10:30 +05:30
"github.com/unknwon/com"
2019-06-23 20:52:43 +05:30
"xorm.io/builder"
2014-03-22 23:20:50 +05:30
)
// Issue represents an issue or pull request of repository.
2014-03-21 01:34:56 +05:30
type Issue struct {
2019-07-08 07:44:12 +05:30
ID int64 ` xorm:"pk autoincr" `
RepoID int64 ` xorm:"INDEX UNIQUE(repo_index)" `
Repo * Repository ` xorm:"-" `
Index int64 ` xorm:"UNIQUE(repo_index)" ` // Index in one repository.
PosterID int64 ` xorm:"INDEX" `
Poster * User ` xorm:"-" `
OriginalAuthor string
OriginalAuthorID int64
Title string ` xorm:"name" `
Content string ` xorm:"TEXT" `
RenderedContent string ` xorm:"-" `
Labels [ ] * Label ` xorm:"-" `
MilestoneID int64 ` xorm:"INDEX" `
Milestone * Milestone ` xorm:"-" `
Priority int
AssigneeID int64 ` xorm:"-" `
Assignee * User ` xorm:"-" `
IsClosed bool ` xorm:"INDEX" `
IsRead bool ` xorm:"-" `
IsPull bool ` xorm:"INDEX" ` // Indicates whether is a pull request or not.
PullRequest * PullRequest ` xorm:"-" `
NumComments int
Ref string
2016-03-10 06:23:30 +05:30
2019-08-15 20:16:21 +05:30
DeadlineUnix timeutil . TimeStamp ` xorm:"INDEX" `
2018-05-02 00:35:28 +05:30
2019-08-15 20:16:21 +05:30
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
ClosedUnix timeutil . TimeStamp ` xorm:"INDEX" `
2015-08-12 14:34:23 +05:30
2018-04-29 11:28:47 +05:30
Attachments [ ] * Attachment ` xorm:"-" `
Comments [ ] * Comment ` xorm:"-" `
Reactions ReactionList ` xorm:"-" `
TotalTrackedTime int64 ` xorm:"-" `
2018-05-09 21:59:04 +05:30
Assignees [ ] * User ` xorm:"-" `
2019-02-19 02:25:04 +05:30
// IsLocked limits commenting abilities to users on an issue
// with write access
IsLocked bool ` xorm:"NOT NULL DEFAULT false" `
2015-08-12 14:34:23 +05:30
}
2018-01-03 14:04:13 +05:30
var (
issueTasksPat * regexp . Regexp
issueTasksDonePat * regexp . Regexp
)
2018-01-04 01:15:21 +05:30
const issueTasksRegexpStr = ` (^\s*[-*]\s\[[\sx]\]\s.)|(\n\s*[-*]\s\[[\sx]\]\s.) `
const issueTasksDoneRegexpStr = ` (^\s*[-*]\s\[[x]\]\s.)|(\n\s*[-*]\s\[[x]\]\s.) `
2019-08-27 07:47:23 +05:30
const issueMaxDupIndexAttempts = 3
2018-01-03 14:04:13 +05:30
func init ( ) {
issueTasksPat = regexp . MustCompile ( issueTasksRegexpStr )
issueTasksDonePat = regexp . MustCompile ( issueTasksDoneRegexpStr )
}
2018-04-29 11:28:47 +05:30
func ( issue * Issue ) loadTotalTimes ( e Engine ) ( err error ) {
opts := FindTrackedTimesOptions { IssueID : issue . ID }
issue . TotalTrackedTime , err = opts . ToSession ( e ) . SumInt ( & TrackedTime { } , "time" )
if err != nil {
return err
}
return nil
}
2018-05-02 00:35:28 +05:30
// IsOverdue checks if the issue is overdue
func ( issue * Issue ) IsOverdue ( ) bool {
2019-08-15 20:16:21 +05:30
return timeutil . TimeStampNow ( ) >= issue . DeadlineUnix
2018-05-02 00:35:28 +05:30
}
2018-12-13 21:25:43 +05:30
// LoadRepo loads issue's repository
func ( issue * Issue ) LoadRepo ( ) error {
return issue . loadRepo ( x )
}
2016-12-17 17:19:17 +05:30
func ( issue * Issue ) loadRepo ( e Engine ) ( err error ) {
2016-08-27 02:10:53 +05:30
if issue . Repo == nil {
issue . Repo , err = getRepositoryByID ( e , issue . RepoID )
2016-03-14 08:50:22 +05:30
if err != nil {
2016-08-27 02:10:53 +05:30
return fmt . Errorf ( "getRepositoryByID [%d]: %v" , issue . RepoID , err )
2016-03-14 08:50:22 +05:30
}
2016-08-27 02:10:53 +05:30
}
2016-12-17 17:19:17 +05:30
return nil
}
2018-04-29 11:28:47 +05:30
// IsTimetrackerEnabled returns true if the repo enables timetracking
func ( issue * Issue ) IsTimetrackerEnabled ( ) bool {
2019-01-14 07:59:58 +05:30
return issue . isTimetrackerEnabled ( x )
}
func ( issue * Issue ) isTimetrackerEnabled ( e Engine ) bool {
if err := issue . loadRepo ( e ) ; err != nil {
2019-04-02 13:18:31 +05:30
log . Error ( fmt . Sprintf ( "loadRepo: %v" , err ) )
2018-04-29 11:28:47 +05:30
return false
}
return issue . Repo . IsTimetrackerEnabled ( )
}
2017-01-28 21:31:07 +05:30
// 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" )
}
pr , err = getPullRequestByIssueID ( x , issue . ID )
2018-10-18 16:53:05 +05:30
if err != nil {
return nil , err
}
pr . Issue = issue
2017-01-28 21:31:07 +05:30
return
}
2017-01-30 18:16:45 +05:30
func ( issue * Issue ) loadLabels ( e Engine ) ( err error ) {
if issue . Labels == nil {
issue . Labels , err = getLabelsByIssueID ( e , issue . ID )
if err != nil {
return fmt . Errorf ( "getLabelsByIssueID [%d]: %v" , issue . ID , err )
}
2016-12-17 17:19:17 +05:30
}
2017-01-30 18:16:45 +05:30
return nil
}
2016-03-14 08:50:22 +05:30
2018-12-13 21:25:43 +05:30
// LoadPoster loads poster
func ( issue * Issue ) LoadPoster ( ) error {
return issue . loadPoster ( x )
}
2017-01-30 18:16:45 +05:30
func ( issue * Issue ) loadPoster ( e Engine ) ( err error ) {
2016-08-27 02:10:53 +05:30
if issue . Poster == nil {
issue . Poster , err = getUserByID ( e , issue . PosterID )
2016-03-14 08:50:22 +05:30
if err != nil {
2016-09-20 15:24:47 +05:30
issue . PosterID = - 1
issue . Poster = NewGhostUser ( )
2016-11-05 00:17:54 +05:30
if ! IsErrUserNotExist ( err ) {
2016-08-27 02:10:53 +05:30
return fmt . Errorf ( "getUserByID.(poster) [%d]: %v" , issue . PosterID , err )
2016-03-14 08:50:22 +05:30
}
2016-11-09 10:37:01 +05:30
err = nil
2016-03-14 08:50:22 +05:30
return
}
2016-08-27 02:10:53 +05:30
}
2017-01-30 18:16:45 +05:30
return
}
2016-03-14 08:50:22 +05:30
2017-07-26 12:46:45 +05:30
func ( issue * Issue ) loadPullRequest ( e Engine ) ( err error ) {
if issue . IsPull && issue . PullRequest == nil {
issue . PullRequest , err = getPullRequestByIssueID ( e , issue . ID )
if err != nil {
if IsErrPullRequestNotExist ( err ) {
return err
}
return fmt . Errorf ( "getPullRequestByIssueID [%d]: %v" , issue . ID , err )
}
2018-12-13 21:25:43 +05:30
issue . PullRequest . Issue = issue
2017-07-26 12:46:45 +05:30
}
return nil
}
2018-12-13 21:25:43 +05:30
// LoadPullRequest loads pull request info
func ( issue * Issue ) LoadPullRequest ( ) error {
return issue . loadPullRequest ( x )
}
2017-09-17 01:46:21 +05:30
func ( issue * Issue ) loadComments ( e Engine ) ( err error ) {
2019-02-19 20:09:39 +05:30
return issue . loadCommentsByType ( e , CommentTypeUnknown )
}
// LoadDiscussComments loads discuss comments
func ( issue * Issue ) LoadDiscussComments ( ) error {
return issue . loadCommentsByType ( x , CommentTypeComment )
}
func ( issue * Issue ) loadCommentsByType ( e Engine , tp CommentType ) ( err error ) {
2017-09-17 01:46:21 +05:30
if issue . Comments != nil {
return nil
}
issue . Comments , err = findComments ( e , FindCommentsOptions {
IssueID : issue . ID ,
2019-02-19 20:09:39 +05:30
Type : tp ,
2017-09-17 01:46:21 +05:30
} )
return err
}
2017-12-04 04:44:26 +05:30
func ( issue * Issue ) loadReactions ( e Engine ) ( err error ) {
if issue . Reactions != nil {
return nil
}
reactions , err := findReactions ( e , FindReactionsOptions {
IssueID : issue . ID ,
} )
if err != nil {
return err
}
// Load reaction user data
2019-01-14 07:59:58 +05:30
if _ , err := ReactionList ( reactions ) . loadUsers ( e ) ; err != nil {
2017-12-04 04:44:26 +05:30
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
}
2017-01-30 18:16:45 +05:30
func ( issue * Issue ) loadAttributes ( e Engine ) ( err error ) {
if err = issue . loadRepo ( e ) ; err != nil {
return
}
if err = issue . loadPoster ( e ) ; err != nil {
return
}
if err = issue . loadLabels ( e ) ; err != nil {
return
2016-08-27 02:10:53 +05:30
}
2015-08-10 19:17:23 +05:30
2016-08-27 02:10:53 +05:30
if issue . Milestone == nil && issue . MilestoneID > 0 {
issue . Milestone , err = getMilestoneByRepoID ( e , issue . RepoID , issue . MilestoneID )
2017-08-12 07:45:30 +05:30
if err != nil && ! IsErrMilestoneNotExist ( err ) {
2016-08-27 02:10:53 +05:30
return fmt . Errorf ( "getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v" , issue . RepoID , issue . MilestoneID , err )
2015-08-10 19:17:23 +05:30
}
2015-08-05 17:53:08 +05:30
}
2018-05-09 21:59:04 +05:30
if err = issue . loadAssignees ( e ) ; err != nil {
2017-06-23 19:13:37 +05:30
return
2016-08-14 16:02:24 +05:30
}
2017-07-26 12:46:45 +05:30
if err = issue . loadPullRequest ( e ) ; err != nil && ! IsErrPullRequestNotExist ( err ) {
2016-08-14 16:02:24 +05:30
// It is possible pull request is not yet created.
2017-07-26 12:46:45 +05:30
return err
2016-08-14 16:02:24 +05:30
}
2016-08-27 02:10:53 +05:30
if issue . Attachments == nil {
issue . Attachments , err = getAttachmentsByIssueID ( e , issue . ID )
if err != nil {
return fmt . Errorf ( "getAttachmentsByIssueID [%d]: %v" , issue . ID , err )
}
}
2017-09-17 01:46:21 +05:30
if err = issue . loadComments ( e ) ; err != nil {
2017-12-04 04:44:26 +05:30
return err
2016-08-27 02:10:53 +05:30
}
2019-04-18 10:30:03 +05:30
if err = CommentList ( issue . Comments ) . loadAttributes ( e ) ; err != nil {
return err
}
2019-01-14 07:59:58 +05:30
if issue . isTimetrackerEnabled ( e ) {
2018-04-29 11:28:47 +05:30
if err = issue . loadTotalTimes ( e ) ; err != nil {
return err
}
}
2016-08-27 02:10:53 +05:30
2017-12-04 04:44:26 +05:30
return issue . loadReactions ( e )
2016-08-14 16:02:24 +05:30
}
2016-11-24 14:11:11 +05:30
// LoadAttributes loads the attribute of this issue.
2016-08-14 16:02:24 +05:30
func ( issue * Issue ) LoadAttributes ( ) error {
return issue . loadAttributes ( x )
2015-08-19 21:42:43 +05:30
}
2017-02-03 12:52:39 +05:30
// GetIsRead load the `IsRead` field of the issue
func ( issue * Issue ) GetIsRead ( userID int64 ) error {
issueUser := & IssueUser { IssueID : issue . ID , UID : userID }
if has , err := x . Get ( issueUser ) ; err != nil {
return err
} else if ! has {
2017-02-09 09:17:24 +05:30
issue . IsRead = false
return nil
2017-02-03 12:52:39 +05:30
}
issue . IsRead = issueUser . IsRead
return nil
}
2017-03-03 20:05:42 +05:30
// APIURL returns the absolute APIURL to this issue.
func ( issue * Issue ) APIURL ( ) string {
2018-01-04 07:11:33 +05:30
return issue . Repo . APIURL ( ) + "/" + path . Join ( "issues" , fmt . Sprint ( issue . Index ) )
2017-03-03 20:05:42 +05:30
}
2016-11-24 14:11:11 +05:30
// HTMLURL returns the absolute URL to this issue.
2016-08-16 22:49:09 +05:30
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 )
}
2016-12-02 16:40:39 +05:30
// 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-14 08:50:22 +05:30
// State returns string representation of issue status.
2016-11-22 16:54:39 +05:30
func ( issue * Issue ) State ( ) api . StateType {
if issue . IsClosed {
2016-11-29 13:55:47 +05:30
return api . StateClosed
2016-03-14 08:50:22 +05:30
}
2016-11-29 13:55:47 +05:30
return api . StateOpen
2016-08-14 16:02:24 +05:30
}
2016-11-22 16:54:39 +05:30
// APIFormat assumes some fields assigned with values:
2016-08-14 16:02:24 +05:30
// Required - Poster, Labels,
// Optional - Milestone, Assignee, PullRequest
func ( issue * Issue ) APIFormat ( ) * api . Issue {
2018-12-13 21:25:43 +05:30
return issue . apiFormat ( x )
}
func ( issue * Issue ) apiFormat ( e Engine ) * api . Issue {
issue . loadLabels ( e )
2016-08-14 16:02:24 +05:30
apiLabels := make ( [ ] * api . Label , len ( issue . Labels ) )
for i := range issue . Labels {
apiLabels [ i ] = issue . Labels [ i ] . APIFormat ( )
}
2018-12-13 21:25:43 +05:30
issue . loadPoster ( e )
issue . loadRepo ( e )
2016-08-14 16:02:24 +05:30
apiIssue := & api . Issue {
ID : issue . ID ,
2017-03-03 20:05:42 +05:30
URL : issue . APIURL ( ) ,
2016-08-14 16:02:24 +05:30
Index : issue . Index ,
2016-08-16 22:49:09 +05:30
Poster : issue . Poster . APIFormat ( ) ,
2016-08-14 16:02:24 +05:30
Title : issue . Title ,
Body : issue . Content ,
Labels : apiLabels ,
2016-08-16 22:49:09 +05:30
State : issue . State ( ) ,
2016-08-14 16:02:24 +05:30
Comments : issue . NumComments ,
2017-12-11 10:07:04 +05:30
Created : issue . CreatedUnix . AsTime ( ) ,
Updated : issue . UpdatedUnix . AsTime ( ) ,
2016-08-14 16:02:24 +05:30
}
ensure that the `closed_at` is set for closed (#5449)
right now the `closed_at` field for json responses is not filled during
the `APIIssue` creation for api responses.
For a closed issue you get a result like:
```json
"state":"open","comments":0,"created_at":"2018-11-29T16:39:24+01:00",
"updated_at":"2018-11-30T10:49:19+01:00","closed_at":null,
"due_date":null,"pull_request":null}
```
which has no information about the closing date. (which exists in the
db and ui)
with this PR the result changes to this:
```json
:null,"assignee":null,"assignees":null,
"state":"closed",
"comments":0,"created_at":"2018-11-29T16:43:05+01:00",
"updated_at":"2018-12-02T19:17:05+01:00",
"closed_at":"2018-12-02T19:17:05+01:00",
"due_date":null,"pull_request":null}
```
fixes: https://github.com/go-gitea/gitea/issues/5446
Signed-off-by: Roman <romaaan.git@gmail.com>
2018-12-03 02:13:01 +05:30
if issue . ClosedUnix != 0 {
apiIssue . Closed = issue . ClosedUnix . AsTimePtr ( )
}
2016-08-14 16:02:24 +05:30
if issue . Milestone != nil {
apiIssue . Milestone = issue . Milestone . APIFormat ( )
}
2018-12-13 21:25:43 +05:30
issue . loadAssignees ( e )
2018-05-09 21:59:04 +05:30
if len ( issue . Assignees ) > 0 {
for _ , assignee := range issue . Assignees {
apiIssue . Assignees = append ( apiIssue . Assignees , assignee . APIFormat ( ) )
}
apiIssue . Assignee = issue . Assignees [ 0 ] . APIFormat ( ) // For compatibility, we're keeping the first assignee as `apiIssue.Assignee`
2016-08-14 16:02:24 +05:30
}
if issue . IsPull {
2018-12-13 21:25:43 +05:30
issue . loadPullRequest ( e )
2016-08-14 16:02:24 +05:30
apiIssue . PullRequest = & api . PullRequestMeta {
HasMerged : issue . PullRequest . HasMerged ,
}
if issue . PullRequest . HasMerged {
2017-12-11 10:07:04 +05:30
apiIssue . PullRequest . Merged = issue . PullRequest . MergedUnix . AsTimePtr ( )
2016-08-14 16:02:24 +05:30
}
}
2018-05-02 00:35:28 +05:30
if issue . DeadlineUnix != 0 {
apiIssue . Deadline = issue . DeadlineUnix . AsTimePtr ( )
}
2016-08-14 16:02:24 +05:30
return apiIssue
}
// HashTag returns unique hash tag for issue.
2016-11-22 16:54:39 +05:30
func ( issue * Issue ) HashTag ( ) string {
return "issue-" + com . ToStr ( issue . ID )
2016-03-14 08:50:22 +05:30
}
2015-08-13 13:37:11 +05:30
// IsPoster returns true if given user by ID is the poster.
2016-11-22 16:54:39 +05:30
func ( issue * Issue ) IsPoster ( uid int64 ) bool {
return issue . PosterID == uid
2015-08-13 13:37:11 +05:30
}
2016-11-22 16:54:39 +05:30
func ( issue * Issue ) hasLabel ( e Engine , labelID int64 ) bool {
return hasIssueLabel ( e , issue . ID , labelID )
2015-08-10 12:12:50 +05:30
}
// HasLabel returns true if issue has been labeled by given ID.
2016-11-22 16:54:39 +05:30
func ( issue * Issue ) HasLabel ( labelID int64 ) bool {
return issue . hasLabel ( x , labelID )
2015-08-10 12:12:50 +05:30
}
2016-08-14 16:02:24 +05:30
func ( issue * Issue ) sendLabelUpdatedWebhook ( doer * User ) {
var err error
2018-05-21 07:58:29 +05:30
if err = issue . loadRepo ( x ) ; err != nil {
2019-04-02 13:18:31 +05:30
log . Error ( "loadRepo: %v" , err )
2018-05-21 07:58:29 +05:30
return
}
if err = issue . loadPoster ( x ) ; err != nil {
2019-04-02 13:18:31 +05:30
log . Error ( "loadPoster: %v" , err )
2018-05-21 07:58:29 +05:30
return
}
2018-11-28 16:56:14 +05:30
mode , _ := AccessLevel ( issue . Poster , issue . Repo )
2016-08-14 16:02:24 +05:30
if issue . IsPull {
2017-07-26 12:46:45 +05:30
if err = issue . loadPullRequest ( x ) ; err != nil {
2019-04-02 13:18:31 +05:30
log . Error ( "loadPullRequest: %v" , err )
2017-07-26 12:46:45 +05:30
return
}
if err = issue . PullRequest . LoadIssue ( ) ; err != nil {
2019-04-02 13:18:31 +05:30
log . Error ( "LoadIssue: %v" , err )
2016-08-25 00:31:30 +05:30
return
}
2016-11-07 21:07:32 +05:30
err = PrepareWebhooks ( issue . Repo , HookEventPullRequest , & api . PullRequestPayload {
Action : api . HookIssueLabelUpdated ,
2016-08-14 16:02:24 +05:30
Index : issue . Index ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2016-12-06 05:18:51 +05:30
Repository : issue . Repo . APIFormat ( AccessModeNone ) ,
2016-08-14 16:02:24 +05:30
Sender : doer . APIFormat ( ) ,
} )
2018-05-21 07:58:29 +05:30
} else {
err = PrepareWebhooks ( issue . Repo , HookEventIssues , & api . IssuePayload {
Action : api . HookIssueLabelUpdated ,
Index : issue . Index ,
Issue : issue . APIFormat ( ) ,
Repository : issue . Repo . APIFormat ( mode ) ,
Sender : doer . APIFormat ( ) ,
} )
2016-08-14 16:02:24 +05:30
}
if err != nil {
2019-04-02 13:18:31 +05:30
log . Error ( "PrepareWebhooks [is_pull: %v]: %v" , issue . IsPull , err )
2016-08-14 16:02:24 +05:30
} else {
go HookQueue . Add ( issue . RepoID )
}
}
2019-07-18 00:32:42 +05:30
// ReplyReference returns tokenized address to use for email reply headers
func ( issue * Issue ) ReplyReference ( ) string {
var path string
if issue . IsPull {
path = "pulls"
} else {
path = "issues"
}
return fmt . Sprintf ( "%s/%s/%d@%s" , issue . Repo . FullName ( ) , path , issue . Index , setting . Domain )
}
2017-01-30 18:16:45 +05:30
func ( issue * Issue ) addLabel ( e * xorm . Session , label * Label , doer * User ) error {
return newIssueLabel ( e , issue , label , doer )
2015-08-10 12:12:50 +05:30
}
2016-08-04 00:21:22 +05:30
// AddLabel adds a new label to the issue.
2016-08-14 16:02:24 +05:30
func ( issue * Issue ) AddLabel ( doer * User , label * Label ) error {
2017-01-30 18:16:45 +05:30
if err := NewIssueLabel ( issue , label , doer ) ; err != nil {
2015-08-14 22:12:43 +05:30
return err
}
2016-08-14 16:02:24 +05:30
issue . sendLabelUpdatedWebhook ( doer )
return nil
2015-08-10 12:12:50 +05:30
}
2017-01-30 18:16:45 +05:30
func ( issue * Issue ) addLabels ( e * xorm . Session , labels [ ] * Label , doer * User ) error {
return newIssueLabels ( e , issue , labels , doer )
2016-08-04 00:21:22 +05:30
}
// AddLabels adds a list of new labels to the issue.
2016-08-14 16:02:24 +05:30
func ( issue * Issue ) AddLabels ( doer * User , labels [ ] * Label ) error {
2017-01-30 18:16:45 +05:30
if err := NewIssueLabels ( issue , labels , doer ) ; err != nil {
2016-08-14 16:02:24 +05:30
return err
}
issue . sendLabelUpdatedWebhook ( doer )
return nil
2016-08-04 00:21:22 +05:30
}
func ( issue * Issue ) getLabels ( e Engine ) ( err error ) {
if len ( issue . Labels ) > 0 {
2014-05-24 12:01:58 +05:30
return nil
}
2016-08-04 00:21:22 +05:30
issue . Labels , err = getLabelsByIssueID ( e , issue . ID )
2015-08-10 12:12:50 +05:30
if err != nil {
return fmt . Errorf ( "getLabelsByIssueID: %v" , err )
2014-05-24 12:01:58 +05:30
}
return nil
}
2017-01-30 18:16:45 +05:30
func ( issue * Issue ) removeLabel ( e * xorm . Session , doer * User , label * Label ) error {
2017-02-01 07:01:35 +05:30
return deleteIssueLabel ( e , issue , label , doer )
2015-08-10 12:12:50 +05:30
}
// RemoveLabel removes a label from issue by given ID.
2016-08-14 16:02:24 +05:30
func ( issue * Issue ) RemoveLabel ( doer * User , label * Label ) error {
2016-12-17 17:19:17 +05:30
if err := issue . loadRepo ( x ) ; err != nil {
return err
}
2018-11-28 16:56:14 +05:30
perm , err := GetUserRepoPermission ( issue . Repo , doer )
if err != nil {
2016-12-17 17:19:17 +05:30
return err
2018-11-28 16:56:14 +05:30
}
if ! perm . CanWriteIssuesOrPulls ( issue . IsPull ) {
2016-12-17 17:19:17 +05:30
return ErrLabelNotExist { }
}
2017-02-01 07:01:35 +05:30
if err := DeleteIssueLabel ( issue , label , doer ) ; err != nil {
2016-08-14 16:02:24 +05:30
return err
}
issue . sendLabelUpdatedWebhook ( doer )
return nil
2016-08-04 00:21:22 +05:30
}
2017-01-30 18:16:45 +05:30
func ( issue * Issue ) clearLabels ( e * xorm . Session , doer * User ) ( err error ) {
2016-08-04 00:21:22 +05:30
if err = issue . getLabels ( e ) ; err != nil {
return fmt . Errorf ( "getLabels: %v" , err )
}
for i := range issue . Labels {
2017-01-30 18:16:45 +05:30
if err = issue . removeLabel ( e , doer , issue . Labels [ i ] ) ; err != nil {
2016-08-04 00:21:22 +05:30
return fmt . Errorf ( "removeLabel: %v" , err )
}
}
return nil
}
2016-11-24 14:11:11 +05:30
// ClearLabels removes all issue labels as the given user.
// Triggers appropriate WebHooks, if any.
2016-08-14 16:02:24 +05:30
func ( issue * Issue ) ClearLabels ( doer * User ) ( err error ) {
2015-08-14 22:12:43 +05:30
sess := x . NewSession ( )
2017-06-21 06:27:05 +05:30
defer sess . Close ( )
2015-08-14 22:12:43 +05:30
if err = sess . Begin ( ) ; err != nil {
return err
}
2016-12-17 17:19:17 +05:30
if err := issue . loadRepo ( sess ) ; err != nil {
return err
2017-07-26 12:46:45 +05:30
} else if err = issue . loadPullRequest ( sess ) ; err != nil {
return err
2016-12-17 17:19:17 +05:30
}
2018-11-28 16:56:14 +05:30
perm , err := getUserRepoPermission ( sess , issue . Repo , doer )
if err != nil {
2016-12-17 17:19:17 +05:30
return err
2018-11-28 16:56:14 +05:30
}
if ! perm . CanWriteIssuesOrPulls ( issue . IsPull ) {
2016-12-17 17:19:17 +05:30
return ErrLabelNotExist { }
}
2017-01-30 18:16:45 +05:30
if err = issue . clearLabels ( sess , doer ) ; err != nil {
2015-08-14 22:12:43 +05:30
return err
}
2016-08-14 16:02:24 +05:30
if err = sess . Commit ( ) ; err != nil {
return fmt . Errorf ( "Commit: %v" , err )
}
2019-09-20 11:15:38 +05:30
sess . Close ( )
2016-08-14 16:02:24 +05:30
2019-09-20 11:15:38 +05:30
if err = issue . LoadPoster ( ) ; err != nil {
2018-05-21 07:58:29 +05:30
return fmt . Errorf ( "loadPoster: %v" , err )
}
2018-11-28 16:56:14 +05:30
mode , _ := AccessLevel ( issue . Poster , issue . Repo )
2016-08-14 16:02:24 +05:30
if issue . IsPull {
2016-08-25 00:31:30 +05:30
err = issue . PullRequest . LoadIssue ( )
if err != nil {
2019-04-02 13:18:31 +05:30
log . Error ( "LoadIssue: %v" , err )
2016-08-25 00:31:30 +05:30
return
}
2016-11-07 21:07:32 +05:30
err = PrepareWebhooks ( issue . Repo , HookEventPullRequest , & api . PullRequestPayload {
Action : api . HookIssueLabelCleared ,
2016-08-14 16:02:24 +05:30
Index : issue . Index ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2018-05-21 07:58:29 +05:30
Repository : issue . Repo . APIFormat ( mode ) ,
2016-08-14 16:02:24 +05:30
Sender : doer . APIFormat ( ) ,
} )
2018-05-21 07:58:29 +05:30
} else {
err = PrepareWebhooks ( issue . Repo , HookEventIssues , & api . IssuePayload {
Action : api . HookIssueLabelCleared ,
Index : issue . Index ,
Issue : issue . APIFormat ( ) ,
Repository : issue . Repo . APIFormat ( mode ) ,
Sender : doer . APIFormat ( ) ,
} )
2016-08-14 16:02:24 +05:30
}
if err != nil {
2019-04-02 13:18:31 +05:30
log . Error ( "PrepareWebhooks [is_pull: %v]: %v" , issue . IsPull , err )
2016-08-14 16:02:24 +05:30
} else {
go HookQueue . Add ( issue . RepoID )
}
return nil
2015-08-14 22:12:43 +05:30
}
2017-01-30 18:16:45 +05:30
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 ]
}
2016-08-04 00:21:22 +05:30
// ReplaceLabels removes all current labels and add new labels to the issue.
2016-11-24 14:11:11 +05:30
// Triggers appropriate WebHooks, if any.
2017-01-30 18:16:45 +05:30
func ( issue * Issue ) ReplaceLabels ( labels [ ] * Label , doer * User ) ( err error ) {
2015-08-14 22:12:43 +05:30
sess := x . NewSession ( )
2017-06-21 06:27:05 +05:30
defer sess . Close ( )
2015-08-14 22:12:43 +05:30
if err = sess . Begin ( ) ; err != nil {
return err
}
2017-01-30 18:16:45 +05:30
if err = issue . loadLabels ( sess ) ; err != nil {
return err
}
sort . Sort ( labelSorter ( labels ) )
sort . Sort ( labelSorter ( issue . Labels ) )
var toAdd , toRemove [ ] * Label
2017-02-28 07:05:55 +05:30
addIndex , removeIndex := 0 , 0
for addIndex < len ( labels ) && removeIndex < len ( issue . Labels ) {
addLabel := labels [ addIndex ]
removeLabel := issue . Labels [ removeIndex ]
if addLabel . ID == removeLabel . ID {
addIndex ++
removeIndex ++
} else if addLabel . ID < removeLabel . ID {
toAdd = append ( toAdd , addLabel )
addIndex ++
} else {
toRemove = append ( toRemove , removeLabel )
removeIndex ++
2017-01-30 18:16:45 +05:30
}
}
2017-02-28 07:05:55 +05:30
toAdd = append ( toAdd , labels [ addIndex : ] ... )
toRemove = append ( toRemove , issue . Labels [ removeIndex : ] ... )
2017-01-30 18:16:45 +05:30
if len ( toAdd ) > 0 {
if err = issue . addLabels ( sess , toAdd , doer ) ; err != nil {
return fmt . Errorf ( "addLabels: %v" , err )
}
}
2017-02-28 07:05:55 +05:30
for _ , l := range toRemove {
if err = issue . removeLabel ( sess , doer , l ) ; err != nil {
return fmt . Errorf ( "removeLabel: %v" , err )
2017-01-30 18:16:45 +05:30
}
2015-08-14 22:12:43 +05:30
}
return sess . Commit ( )
2015-08-10 12:12:50 +05:30
}
2015-08-12 16:14:09 +05:30
// ReadBy sets issue to be read by given user.
2016-12-30 22:14:54 +05:30
func ( issue * Issue ) ReadBy ( userID int64 ) error {
if err := UpdateIssueUserByRead ( userID , issue . ID ) ; err != nil {
return err
}
2017-09-19 13:38:30 +05:30
return setNotificationStatusReadIfUnread ( x , userID , issue . ID )
2014-07-24 00:45:47 +05:30
}
2016-08-14 16:02:24 +05:30
func updateIssueCols ( e Engine , issue * Issue , cols ... string ) error {
2017-10-05 10:13:04 +05:30
if _ , err := e . ID ( issue . ID ) . Cols ( cols ... ) . Update ( issue ) ; err != nil {
2017-01-25 08:13:02 +05:30
return err
}
return nil
2016-08-14 16:02:24 +05:30
}
2018-12-13 21:25:43 +05:30
func ( issue * Issue ) changeStatus ( e * xorm . Session , doer * User , isClosed bool ) ( err error ) {
2019-03-05 08:22:52 +05:30
// Reload the issue
currentIssue , err := getIssueByID ( e , issue . ID )
if err != nil {
return err
}
2016-03-05 23:28:51 +05:30
// Nothing should be performed if current status is same as target status
2019-03-05 08:22:52 +05:30
if currentIssue . IsClosed == isClosed {
2015-08-13 13:37:11 +05:30
return nil
}
2018-07-18 02:53:58 +05:30
// Check for open dependencies
2018-10-27 20:15:24 +05:30
if isClosed && issue . Repo . isDependenciesEnabled ( e ) {
2018-07-18 02:53:58 +05:30
// 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
2018-10-27 20:15:24 +05:30
noDeps , err := issueNoDependenciesLeft ( e , issue )
2018-07-18 02:53:58 +05:30
if err != nil {
return err
}
if ! noDeps {
return ErrDependenciesLeft { issue . ID }
}
}
2016-11-22 16:54:39 +05:30
issue . IsClosed = isClosed
2018-02-19 08:09:26 +05:30
if isClosed {
2019-08-15 20:16:21 +05:30
issue . ClosedUnix = timeutil . TimeStampNow ( )
2018-02-19 08:09:26 +05:30
} else {
issue . ClosedUnix = 0
}
2015-08-13 13:37:11 +05:30
2018-02-19 08:09:26 +05:30
if err = updateIssueCols ( e , issue , "is_closed" , "closed_unix" ) ; err != nil {
2015-08-13 13:37:11 +05:30
return err
}
2016-03-05 23:28:51 +05:30
// Update issue count of labels
2016-11-22 16:54:39 +05:30
if err = issue . getLabels ( e ) ; err != nil {
2015-08-13 13:37:11 +05:30
return err
}
2016-11-22 16:54:39 +05:30
for idx := range issue . Labels {
if err = updateLabel ( e , issue . Labels [ idx ] ) ; err != nil {
2015-08-13 13:37:11 +05:30
return err
}
}
2016-03-05 23:28:51 +05:30
// Update issue count of milestone
2019-10-07 02:56:19 +05:30
if err := updateMilestoneClosedNum ( e , issue . MilestoneID ) ; err != nil {
2015-08-13 13:37:11 +05:30
return err
}
2016-03-05 23:28:51 +05:30
// New action comment
2018-12-13 21:25:43 +05:30
if _ , err = createStatusComment ( e , doer , issue ) ; err != nil {
2015-08-13 13:37:11 +05:30
return err
}
return nil
}
2016-03-05 23:28:51 +05:30
// ChangeStatus changes issue status to open or closed.
2018-12-13 21:25:43 +05:30
func ( issue * Issue ) ChangeStatus ( doer * User , isClosed bool ) ( err error ) {
2015-08-13 13:37:11 +05:30
sess := x . NewSession ( )
2017-06-21 06:27:05 +05:30
defer sess . Close ( )
2015-08-13 13:37:11 +05:30
if err = sess . Begin ( ) ; err != nil {
return err
}
2018-12-13 21:25:43 +05:30
if err = issue . loadRepo ( sess ) ; err != nil {
return err
}
if err = issue . loadPoster ( sess ) ; err != nil {
return err
}
if err = issue . changeStatus ( sess , doer , isClosed ) ; err != nil {
2015-08-13 13:37:11 +05:30
return err
}
2016-08-14 16:02:24 +05:30
if err = sess . Commit ( ) ; err != nil {
return fmt . Errorf ( "Commit: %v" , err )
}
2018-10-27 20:15:24 +05:30
sess . Close ( )
2016-08-14 16:02:24 +05:30
2018-11-28 16:56:14 +05:30
mode , _ := AccessLevel ( issue . Poster , issue . Repo )
2016-08-14 16:02:24 +05:30
if issue . IsPull {
2018-12-13 21:25:43 +05:30
if err = issue . loadPullRequest ( sess ) ; err != nil {
return err
}
2016-08-14 16:02:24 +05:30
// Merge pull request calls issue.changeStatus so we need to handle separately.
apiPullRequest := & api . PullRequestPayload {
Index : issue . Index ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2018-12-13 21:25:43 +05:30
Repository : issue . Repo . APIFormat ( mode ) ,
2016-08-14 16:02:24 +05:30
Sender : doer . APIFormat ( ) ,
}
if isClosed {
2016-11-07 21:07:32 +05:30
apiPullRequest . Action = api . HookIssueClosed
2016-08-14 16:02:24 +05:30
} else {
2016-11-29 13:55:47 +05:30
apiPullRequest . Action = api . HookIssueReOpened
2016-08-14 16:02:24 +05:30
}
2018-12-13 21:25:43 +05:30
err = PrepareWebhooks ( issue . Repo , HookEventPullRequest , apiPullRequest )
2018-05-21 07:58:29 +05:30
} else {
apiIssue := & api . IssuePayload {
Index : issue . Index ,
Issue : issue . APIFormat ( ) ,
2018-12-13 21:25:43 +05:30
Repository : issue . Repo . APIFormat ( mode ) ,
2018-05-21 07:58:29 +05:30
Sender : doer . APIFormat ( ) ,
}
if isClosed {
apiIssue . Action = api . HookIssueClosed
} else {
apiIssue . Action = api . HookIssueReOpened
}
2018-12-13 21:25:43 +05:30
err = PrepareWebhooks ( issue . Repo , HookEventIssues , apiIssue )
2016-08-14 16:02:24 +05:30
}
if err != nil {
2019-04-02 13:18:31 +05:30
log . Error ( "PrepareWebhooks [is_pull: %v, is_closed: %v]: %v" , issue . IsPull , isClosed , err )
2016-08-14 16:02:24 +05:30
} else {
2018-12-13 21:25:43 +05:30
go HookQueue . Add ( issue . Repo . ID )
2016-08-14 16:02:24 +05:30
}
return nil
}
2016-11-24 14:11:11 +05:30
// ChangeTitle changes the title of this issue, as the given user.
2019-10-11 12:14:43 +05:30
func ( issue * Issue ) ChangeTitle ( doer * User , oldTitle string ) ( err error ) {
2017-02-05 20:06:00 +05:30
sess := x . NewSession ( )
defer sess . Close ( )
if err = sess . Begin ( ) ; err != nil {
return err
}
if err = updateIssueCols ( sess , issue , "name" ) ; err != nil {
return fmt . Errorf ( "updateIssueCols: %v" , err )
}
2018-12-13 21:25:43 +05:30
if err = issue . loadRepo ( sess ) ; err != nil {
return fmt . Errorf ( "loadRepo: %v" , err )
}
2019-10-11 12:14:43 +05:30
if _ , err = createChangeTitleComment ( sess , doer , issue . Repo , issue , oldTitle , issue . Title ) ; err != nil {
2017-02-05 20:06:00 +05:30
return fmt . Errorf ( "createChangeTitleComment: %v" , err )
}
2019-09-20 11:15:38 +05:30
if err = issue . neuterCrossReferences ( sess ) ; err != nil {
return err
}
if err = issue . addCrossReferences ( sess , doer ) ; err != nil {
return err
}
2019-10-11 12:14:43 +05:30
return sess . Commit ( )
2015-08-13 13:37:11 +05:30
}
2017-02-11 09:30:29 +05:30
// AddDeletePRBranchComment adds delete branch comment for pull request issue
func AddDeletePRBranchComment ( doer * User , repo * Repository , issueID int64 , branchName string ) error {
issue , err := getIssueByID ( x , issueID )
if err != nil {
return err
}
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return err
}
if _ , err := createDeleteBranchComment ( sess , doer , repo , issue , branchName ) ; err != nil {
return err
}
return sess . Commit ( )
}
2016-11-24 14:11:11 +05:30
// ChangeContent changes issue content, as the given user.
2016-08-14 16:02:24 +05:30
func ( issue * Issue ) ChangeContent ( doer * User , content string ) ( err error ) {
oldContent := issue . Content
issue . Content = content
2018-01-03 14:04:13 +05:30
2019-09-20 11:15:38 +05:30
sess := x . NewSession ( )
defer sess . Close ( )
if err = sess . Begin ( ) ; err != nil {
return err
}
if err = updateIssueCols ( sess , issue , "content" ) ; err != nil {
2016-08-14 16:02:24 +05:30
return fmt . Errorf ( "UpdateIssueCols: %v" , err )
}
2019-09-20 11:15:38 +05:30
if err = issue . neuterCrossReferences ( sess ) ; err != nil {
return err
}
if err = issue . addCrossReferences ( sess , doer ) ; err != nil {
return err
}
if err = sess . Commit ( ) ; err != nil {
return err
}
sess . Close ( )
2016-08-14 16:02:24 +05:30
2018-11-28 16:56:14 +05:30
mode , _ := AccessLevel ( issue . Poster , issue . Repo )
2016-08-14 16:02:24 +05:30
if issue . IsPull {
issue . PullRequest . Issue = issue
2016-11-07 21:07:32 +05:30
err = PrepareWebhooks ( issue . Repo , HookEventPullRequest , & api . PullRequestPayload {
Action : api . HookIssueEdited ,
2016-08-14 16:02:24 +05:30
Index : issue . Index ,
Changes : & api . ChangesPayload {
Body : & api . ChangesFromPayload {
From : oldContent ,
} ,
} ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2018-05-21 07:58:29 +05:30
Repository : issue . Repo . APIFormat ( mode ) ,
2016-08-14 16:02:24 +05:30
Sender : doer . APIFormat ( ) ,
} )
2018-05-21 07:58:29 +05:30
} else {
err = PrepareWebhooks ( issue . Repo , HookEventIssues , & api . IssuePayload {
Action : api . HookIssueEdited ,
Index : issue . Index ,
Changes : & api . ChangesPayload {
Body : & api . ChangesFromPayload {
From : oldContent ,
} ,
} ,
Issue : issue . APIFormat ( ) ,
Repository : issue . Repo . APIFormat ( mode ) ,
Sender : doer . APIFormat ( ) ,
} )
2016-08-14 16:02:24 +05:30
}
if err != nil {
2019-04-02 13:18:31 +05:30
log . Error ( "PrepareWebhooks [is_pull: %v]: %v" , issue . IsPull , err )
2016-08-14 16:02:24 +05:30
} else {
go HookQueue . Add ( issue . RepoID )
}
return nil
}
2018-01-03 14:04:13 +05:30
// 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 13:44:17 +05:30
// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close.
2019-08-15 20:16:21 +05:30
func ( issue * Issue ) GetLastEventTimestamp ( ) timeutil . TimeStamp {
2019-02-13 13:44:17 +05:30
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"
}
2019-03-28 03:52:39 +05:30
// 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 14:11:11 +05:30
// NewIssueOptions represents the options of a new issue.
2016-08-16 07:10:32 +05:30
type NewIssueOptions struct {
Repo * Repository
Issue * Issue
2017-03-01 06:38:45 +05:30
LabelIDs [ ] int64
2018-05-09 21:59:04 +05:30
AssigneeIDs [ ] int64
2016-08-16 07:10:32 +05:30
Attachments [ ] string // In UUID format.
IsPull bool
}
2016-03-14 08:50:22 +05:30
2017-02-01 08:06:08 +05:30
func newIssue ( e * xorm . Session , doer * User , opts NewIssueOptions ) ( err error ) {
2016-08-16 07:10:32 +05:30
opts . Issue . Title = strings . TrimSpace ( opts . Issue . Title )
2019-05-18 08:07:49 +05:30
2016-08-16 22:49:09 +05:30
if opts . Issue . MilestoneID > 0 {
2016-08-25 04:35:56 +05:30
milestone , err := getMilestoneByRepoID ( e , opts . Issue . RepoID , opts . Issue . MilestoneID )
2016-08-16 22:49:09 +05:30
if err != nil && ! IsErrMilestoneNotExist ( err ) {
return fmt . Errorf ( "getMilestoneByID: %v" , err )
}
// Assume milestone is invalid and drop silently.
opts . Issue . MilestoneID = 0
if milestone != nil {
opts . Issue . MilestoneID = milestone . ID
opts . Issue . Milestone = milestone
}
}
2018-05-09 21:59:04 +05:30
// Keep the old assignee id thingy for compatibility reasons
if opts . Issue . AssigneeID > 0 {
isAdded := false
// Check if the user has already been passed to issue.AssigneeIDs, if not, add it
for _ , aID := range opts . AssigneeIDs {
if aID == opts . Issue . AssigneeID {
isAdded = true
break
}
2016-08-16 22:49:09 +05:30
}
2018-05-09 21:59:04 +05:30
if ! isAdded {
opts . AssigneeIDs = append ( opts . AssigneeIDs , opts . Issue . AssigneeID )
}
}
// Check for and validate assignees
if len ( opts . AssigneeIDs ) > 0 {
for _ , assigneeID := range opts . AssigneeIDs {
2018-11-28 16:56:14 +05:30
user , err := getUserByID ( e , assigneeID )
if err != nil {
return fmt . Errorf ( "getUserByID [user_id: %d, repo_id: %d]: %v" , assigneeID , opts . Repo . ID , err )
}
valid , err := canBeAssigned ( e , user , opts . Repo )
2018-05-09 21:59:04 +05:30
if err != nil {
2018-11-28 16:56:14 +05:30
return fmt . Errorf ( "canBeAssigned [user_id: %d, repo_id: %d]: %v" , assigneeID , opts . Repo . ID , err )
2018-05-09 21:59:04 +05:30
}
if ! valid {
return ErrUserDoesNotHaveAccessToRepo { UserID : assigneeID , RepoName : opts . Repo . Name }
}
2016-03-14 08:50:22 +05:30
}
}
2016-08-16 22:49:09 +05:30
// Milestone and assignee validation should happen before insert actual object.
2019-10-03 03:58:30 +05:30
if _ , err := e . SetExpr ( "`index`" , "coalesce(MAX(`index`),0)+1" ) .
Where ( "repo_id=?" , opts . Issue . RepoID ) .
Insert ( opts . Issue ) ; err != nil {
return ErrNewIssueInsert { err }
2019-08-27 07:47:23 +05:30
}
inserted , err := getIssueByID ( e , opts . Issue . ID )
if err != nil {
2014-05-07 21:39:30 +05:30
return err
2015-09-03 01:48:09 +05:30
}
2019-08-27 07:47:23 +05:30
// Patch Index with the value calculated by the database
opts . Issue . Index = inserted . Index
2017-02-01 08:06:08 +05:30
if opts . Issue . MilestoneID > 0 {
2019-10-07 02:56:19 +05:30
if _ , err = e . Exec ( "UPDATE `milestone` SET num_issues=num_issues+1 WHERE id=?" , opts . Issue . MilestoneID ) ; err != nil {
2017-02-01 08:06:08 +05:30
return err
}
}
2018-05-09 21:59:04 +05:30
// Insert the assignees
for _ , assigneeID := range opts . AssigneeIDs {
2018-08-16 17:22:51 +05:30
err = opts . Issue . changeAssignee ( e , doer , assigneeID , true )
2018-05-09 21:59:04 +05:30
if err != nil {
2017-02-03 20:39:10 +05:30
return err
}
}
2016-08-16 07:10:32 +05:30
if opts . IsPull {
_ , err = e . Exec ( "UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?" , opts . Issue . RepoID )
2015-09-03 01:48:09 +05:30
} else {
2016-08-16 07:10:32 +05:30
_ , err = e . Exec ( "UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?" , opts . Issue . RepoID )
2015-09-03 01:48:09 +05:30
}
if err != nil {
2014-05-07 21:39:30 +05:30
return err
2014-03-27 22:18:29 +05:30
}
2014-07-22 17:20:34 +05:30
2017-03-01 06:38:45 +05:30
if len ( opts . LabelIDs ) > 0 {
2016-11-22 00:38:21 +05:30
// During the session, SQLite3 driver cannot handle retrieve objects after update something.
2016-03-06 07:15:23 +05:30
// So we have to get all needed labels first.
2017-03-01 06:38:45 +05:30
labels := make ( [ ] * Label , 0 , len ( opts . LabelIDs ) )
if err = e . In ( "id" , opts . LabelIDs ) . Find ( & labels ) ; err != nil {
return fmt . Errorf ( "find all labels [label_ids: %v]: %v" , opts . LabelIDs , err )
2016-03-06 07:15:23 +05:30
}
2015-08-14 22:12:43 +05:30
2017-01-30 18:16:45 +05:30
if err = opts . Issue . loadPoster ( e ) ; err != nil {
return err
}
2016-03-06 07:15:23 +05:30
for _ , label := range labels {
2016-08-16 07:10:32 +05:30
// Silently drop invalid labels.
if label . RepoID != opts . Repo . ID {
2016-03-14 08:50:22 +05:30
continue
}
2017-01-30 18:16:45 +05:30
if err = opts . Issue . addLabel ( e , label , opts . Issue . Poster ) ; err != nil {
2016-08-16 07:10:32 +05:30
return fmt . Errorf ( "addLabel [id: %d]: %v" , label . ID , err )
2016-03-06 07:15:23 +05:30
}
2015-08-10 14:22:08 +05:30
}
}
2016-08-16 07:10:32 +05:30
if err = newIssueUsers ( e , opts . Repo , opts . Issue ) ; err != nil {
2015-08-10 19:17:23 +05:30
return err
}
2016-08-16 07:10:32 +05:30
if len ( opts . Attachments ) > 0 {
attachments , err := getAttachmentsByUUIDs ( e , opts . Attachments )
2015-09-02 04:37:02 +05:30
if err != nil {
2016-08-16 07:10:32 +05:30
return fmt . Errorf ( "getAttachmentsByUUIDs [uuids: %v]: %v" , opts . Attachments , err )
2015-09-02 04:37:02 +05:30
}
2016-08-16 07:10:32 +05:30
for i := 0 ; i < len ( attachments ) ; i ++ {
attachments [ i ] . IssueID = opts . Issue . ID
2017-10-05 10:13:04 +05:30
if _ , err = e . ID ( attachments [ i ] . ID ) . Update ( attachments [ i ] ) ; err != nil {
2016-08-16 07:10:32 +05:30
return fmt . Errorf ( "update attachment [id: %d]: %v" , attachments [ i ] . ID , err )
}
2015-08-11 20:54:40 +05:30
}
}
2019-09-20 11:15:38 +05:30
if err = opts . Issue . loadAttributes ( e ) ; err != nil {
return err
}
return opts . Issue . addCrossReferences ( e , doer )
2015-09-02 04:37:02 +05:30
}
// NewIssue creates new issue with labels for repository.
2018-05-09 21:59:04 +05:30
func NewIssue ( repo * Repository , issue * Issue , labelIDs [ ] int64 , assigneeIDs [ ] int64 , uuids [ ] string ) ( err error ) {
2019-10-03 03:58:30 +05:30
// Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887
i := 0
for {
if err = newIssueAttempt ( repo , issue , labelIDs , assigneeIDs , uuids ) ; err == nil {
return nil
}
if ! IsErrNewIssueInsert ( err ) {
return err
}
if i ++ ; i == issueMaxDupIndexAttempts {
break
}
log . Error ( "NewIssue: error attempting to insert the new issue; will retry. Original error: %v" , err )
}
return fmt . Errorf ( "NewIssue: too many errors attempting to insert the new issue. Last error was: %v" , err )
}
func newIssueAttempt ( repo * Repository , issue * Issue , labelIDs [ ] int64 , assigneeIDs [ ] int64 , uuids [ ] string ) ( err error ) {
2015-09-02 04:37:02 +05:30
sess := x . NewSession ( )
2017-06-21 06:27:05 +05:30
defer sess . Close ( )
2015-09-02 04:37:02 +05:30
if err = sess . Begin ( ) ; err != nil {
return err
}
2017-02-01 08:06:08 +05:30
if err = newIssue ( sess , issue . Poster , NewIssueOptions {
2016-08-16 07:10:32 +05:30
Repo : repo ,
Issue : issue ,
2017-03-01 06:38:45 +05:30
LabelIDs : labelIDs ,
2016-08-16 07:10:32 +05:30
Attachments : uuids ,
2018-05-09 21:59:04 +05:30
AssigneeIDs : assigneeIDs ,
2016-08-16 07:10:32 +05:30
} ) ; err != nil {
2019-10-03 03:58:30 +05:30
if IsErrUserDoesNotHaveAccessToRepo ( err ) || IsErrNewIssueInsert ( err ) {
2018-05-09 21:59:04 +05:30
return err
}
2015-09-02 04:37:02 +05:30
return fmt . Errorf ( "newIssue: %v" , err )
}
2016-03-14 08:50:22 +05:30
if err = sess . Commit ( ) ; err != nil {
return fmt . Errorf ( "Commit: %v" , err )
}
return nil
2014-03-21 01:34:56 +05:30
}
2018-12-13 21:25:43 +05:30
// GetIssueByIndex returns raw issue without loading attributes by index in a repository.
func GetIssueByIndex ( repoID , index int64 ) ( * Issue , error ) {
2015-08-12 14:34:23 +05:30
issue := & Issue {
RepoID : repoID ,
Index : index ,
}
2014-06-21 10:21:41 +05:30
has , err := x . Get ( issue )
2014-03-23 01:30:46 +05:30
if err != nil {
return nil , err
} else if ! has {
2015-08-12 14:34:23 +05:30
return nil , ErrIssueNotExist { 0 , repoID , index }
2014-03-23 01:30:46 +05:30
}
2016-08-27 02:10:53 +05:30
return issue , nil
}
2018-12-13 21:25:43 +05:30
// GetIssueWithAttrsByIndex returns issue by index in a repository.
func GetIssueWithAttrsByIndex ( repoID , index int64 ) ( * Issue , error ) {
issue , err := GetIssueByIndex ( repoID , index )
2016-08-27 02:10:53 +05:30
if err != nil {
return nil , err
}
2016-07-21 11:56:30 +05:30
return issue , issue . LoadAttributes ( )
2014-03-23 01:30:46 +05:30
}
2016-08-14 16:02:24 +05:30
func getIssueByID ( e Engine , id int64 ) ( * Issue , error ) {
2015-08-12 14:34:23 +05:30
issue := new ( Issue )
2017-10-05 10:13:04 +05:30
has , err := e . ID ( id ) . Get ( issue )
2014-05-08 02:21:14 +05:30
if err != nil {
return nil , err
} else if ! has {
2015-08-12 14:34:23 +05:30
return nil , ErrIssueNotExist { id , 0 , 0 }
2014-05-08 02:21:14 +05:30
}
2018-12-13 21:25:43 +05:30
return issue , nil
}
// GetIssueWithAttrsByID returns an issue with attributes by given ID.
func GetIssueWithAttrsByID ( id int64 ) ( * Issue , error ) {
issue , err := getIssueByID ( x , id )
if err != nil {
return nil , err
}
return issue , issue . loadAttributes ( x )
2014-05-08 02:21:14 +05:30
}
2016-08-14 16:02:24 +05:30
// GetIssueByID returns an issue by given ID.
func GetIssueByID ( id int64 ) ( * Issue , error ) {
return getIssueByID ( x , id )
}
2017-03-15 06:40:35 +05:30
func getIssuesByIDs ( e Engine , issueIDs [ ] int64 ) ( [ ] * Issue , error ) {
issues := make ( [ ] * Issue , 0 , 10 )
return issues , e . In ( "id" , issueIDs ) . Find ( & issues )
}
2019-02-19 20:09:39 +05:30
func getIssueIDsByRepoID ( e Engine , repoID int64 ) ( [ ] int64 , error ) {
var ids = make ( [ ] int64 , 0 , 10 )
err := e . Table ( "issue" ) . Where ( "repo_id = ?" , repoID ) . Find ( & ids )
return ids , err
}
2019-02-21 06:24:05 +05:30
// GetIssueIDsByRepoID returns all issue ids by repo id
func GetIssueIDsByRepoID ( repoID int64 ) ( [ ] int64 , error ) {
return getIssueIDsByRepoID ( x , repoID )
}
2017-03-15 06:40:35 +05:30
// GetIssuesByIDs return issues with the given IDs.
func GetIssuesByIDs ( issueIDs [ ] int64 ) ( [ ] * Issue , error ) {
return getIssuesByIDs ( x , issueIDs )
}
2016-11-24 14:11:11 +05:30
// IssuesOptions represents options of an issue.
2015-09-03 01:48:09 +05:30
type IssuesOptions struct {
2019-10-08 23:25:16 +05:30
RepoIDs [ ] int64 // include all repos if empty
AssigneeID int64
PosterID int64
MentionedID int64
MilestoneID int64
Page int
PageSize int
IsClosed util . OptionalBool
IsPull util . OptionalBool
LabelIDs [ ] int64
SortType string
IssueIDs [ ] int64
2015-09-03 01:48:09 +05:30
}
2017-01-01 23:45:09 +05:30
// sortIssuesSession sort an issues-related session based on the provided
// sortType string
func sortIssuesSession ( sess * xorm . Session , sortType string ) {
switch sortType {
case "oldest" :
sess . Asc ( "issue.created_unix" )
case "recentupdate" :
sess . Desc ( "issue.updated_unix" )
case "leastupdate" :
sess . Asc ( "issue.updated_unix" )
case "mostcomment" :
sess . Desc ( "issue.num_comments" )
case "leastcomment" :
sess . Asc ( "issue.num_comments" )
case "priority" :
sess . Desc ( "issue.priority" )
2019-03-05 20:09:41 +05:30
case "nearduedate" :
sess . Asc ( "issue.deadline_unix" )
case "farduedate" :
sess . Desc ( "issue.deadline_unix" )
2017-01-01 23:45:09 +05:30
default :
sess . Desc ( "issue.created_unix" )
}
}
2019-06-13 01:11:28 +05:30
func ( opts * IssuesOptions ) setupSession ( sess * xorm . Session ) {
2017-08-03 10:39:16 +05:30
if opts . Page >= 0 && opts . PageSize > 0 {
2017-01-25 08:13:02 +05:30
var start int
if opts . Page == 0 {
start = 0
} else {
2017-08-03 10:39:16 +05:30
start = ( opts . Page - 1 ) * opts . PageSize
2017-01-25 08:13:02 +05:30
}
2017-08-03 10:39:16 +05:30
sess . Limit ( opts . PageSize , start )
2016-03-14 08:50:22 +05:30
}
2017-01-25 08:13:02 +05:30
if len ( opts . IssueIDs ) > 0 {
sess . In ( "issue.id" , opts . IssueIDs )
}
2014-03-23 01:30:46 +05:30
2019-10-08 23:25:16 +05:30
if len ( opts . RepoIDs ) > 0 {
2015-09-01 16:01:47 +05:30
// In case repository IDs are provided but actually no repository has issue.
2016-11-12 17:36:33 +05:30
sess . In ( "issue.repo_id" , opts . RepoIDs )
2014-03-23 01:30:46 +05:30
}
2017-01-25 08:13:02 +05:30
switch opts . IsClosed {
case util . OptionalBoolTrue :
2017-01-25 13:58:03 +05:30
sess . And ( "issue.is_closed=?" , true )
2017-01-25 08:13:02 +05:30
case util . OptionalBoolFalse :
2017-01-25 13:58:03 +05:30
sess . And ( "issue.is_closed=?" , false )
2017-01-25 08:13:02 +05:30
}
2014-03-23 01:30:46 +05:30
2015-09-03 01:48:09 +05:30
if opts . AssigneeID > 0 {
2018-05-09 21:59:04 +05:30
sess . Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , opts . AssigneeID )
2016-12-24 16:03:21 +05:30
}
if opts . PosterID > 0 {
2016-07-17 06:48:35 +05:30
sess . And ( "issue.poster_id=?" , opts . PosterID )
2014-03-22 23:20:50 +05:30
}
2016-12-24 16:03:21 +05:30
if opts . MentionedID > 0 {
sess . Join ( "INNER" , "issue_user" , "issue.id = issue_user.issue_id" ) .
And ( "issue_user.is_mentioned = ?" , true ) .
And ( "issue_user.uid = ?" , opts . MentionedID )
}
2015-09-03 01:48:09 +05:30
if opts . MilestoneID > 0 {
2016-07-17 06:48:35 +05:30
sess . And ( "issue.milestone_id=?" , opts . MilestoneID )
2014-03-22 23:20:50 +05:30
}
2017-01-25 08:13:02 +05:30
switch opts . IsPull {
case util . OptionalBoolTrue :
2017-01-28 21:31:07 +05:30
sess . And ( "issue.is_pull=?" , true )
2017-01-25 08:13:02 +05:30
case util . OptionalBoolFalse :
2017-01-28 21:31:07 +05:30
sess . And ( "issue.is_pull=?" , false )
2017-01-25 08:13:02 +05:30
}
2015-09-03 01:48:09 +05:30
2019-01-23 09:40:38 +05:30
if opts . LabelIDs != nil {
for i , labelID := range opts . LabelIDs {
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 ) )
2016-04-26 09:52:03 +05:30
}
2015-08-10 19:17:23 +05:30
}
2017-08-03 10:39:16 +05:30
}
// CountIssuesByRepo map from repoID to number of issues matching the options
func CountIssuesByRepo ( opts * IssuesOptions ) ( map [ int64 ] int64 , error ) {
sess := x . NewSession ( )
defer sess . Close ( )
2019-06-13 01:11:28 +05:30
opts . setupSession ( sess )
2017-08-03 10:39:16 +05:30
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 {
return nil , err
}
countMap := make ( map [ int64 ] int64 , len ( countsSlice ) )
for _ , c := range countsSlice {
countMap [ c . RepoID ] = c . Count
}
return countMap , nil
}
// Issues returns a list of issues by given conditions.
func Issues ( opts * IssuesOptions ) ( [ ] * Issue , error ) {
sess := x . NewSession ( )
defer sess . Close ( )
2019-06-13 01:11:28 +05:30
opts . setupSession ( sess )
2017-08-03 10:39:16 +05:30
sortIssuesSession ( sess , opts . SortType )
2015-08-10 19:17:23 +05:30
2016-07-23 21:53:54 +05:30
issues := make ( [ ] * Issue , 0 , setting . UI . IssuePagingNum )
2016-08-27 02:10:53 +05:30
if err := sess . Find ( & issues ) ; err != nil {
return nil , fmt . Errorf ( "Find: %v" , err )
}
2019-06-23 20:52:43 +05:30
sess . Close ( )
2016-08-27 02:10:53 +05:30
2017-02-22 19:33:59 +05:30
if err := IssueList ( issues ) . LoadAttributes ( ) ; err != nil {
return nil , fmt . Errorf ( "LoadAttributes: %v" , err )
2016-08-27 02:10:53 +05:30
}
return issues , nil
2014-03-22 23:20:50 +05:30
}
2017-03-16 07:04:24 +05:30
// GetParticipantsByIssueID returns all users who are participated in comments of an issue.
func GetParticipantsByIssueID ( issueID int64 ) ( [ ] * User , error ) {
2017-08-30 10:01:33 +05:30
return getParticipantsByIssueID ( x , issueID )
}
func getParticipantsByIssueID ( e Engine , issueID int64 ) ( [ ] * User , error ) {
2017-03-16 07:04:24 +05:30
userIDs := make ( [ ] int64 , 0 , 5 )
2017-08-30 10:01:33 +05:30
if err := e . Table ( "comment" ) . Cols ( "poster_id" ) .
2017-09-16 05:48:25 +05:30
Where ( "`comment`.issue_id = ?" , issueID ) .
2019-09-07 20:23:35 +05:30
And ( "`comment`.type in (?,?,?)" , CommentTypeComment , CommentTypeCode , CommentTypeReview ) .
2017-09-16 05:48:25 +05:30
And ( "`user`.is_active = ?" , true ) .
And ( "`user`.prohibit_login = ?" , false ) .
2018-07-20 07:40:17 +05:30
Join ( "INNER" , "`user`" , "`user`.id = `comment`.poster_id" ) .
2017-03-16 07:04:24 +05:30
Distinct ( "poster_id" ) .
Find ( & userIDs ) ; err != nil {
return nil , fmt . Errorf ( "get poster IDs: %v" , err )
}
if len ( userIDs ) == 0 {
return nil , nil
}
users := make ( [ ] * User , 0 , len ( userIDs ) )
2017-08-30 10:01:33 +05:30
return users , e . In ( "id" , userIDs ) . Find ( & users )
2017-03-16 07:04:24 +05:30
}
2019-10-10 22:15:11 +05:30
// UpdateIssueMentions updates issue-user relations for mentioned users.
func UpdateIssueMentions ( ctx DBContext , issueID int64 , mentions [ ] * User ) error {
2016-07-15 22:06:39 +05:30
if len ( mentions ) == 0 {
return nil
2015-12-21 17:54:11 +05:30
}
2019-10-10 22:15:11 +05:30
ids := make ( [ ] int64 , len ( mentions ) )
for i , u := range mentions {
ids [ i ] = u . ID
2015-12-21 17:54:11 +05:30
}
2019-09-24 10:32:49 +05:30
if err := UpdateIssueUsersByMentions ( ctx , issueID , ids ) ; err != nil {
2016-07-15 22:06:39 +05:30
return fmt . Errorf ( "UpdateIssueUsersByMentions: %v" , err )
2015-12-21 17:54:11 +05:30
}
return nil
}
2014-05-07 21:39:30 +05:30
// IssueStats represents issue statistic information.
type IssueStats struct {
OpenCount , ClosedCount int64
2017-02-14 19:45:18 +05:30
YourRepositoriesCount int64
2014-05-07 21:39:30 +05:30
AssignCount int64
CreateCount int64
MentionCount int64
}
// Filter modes.
const (
2016-11-07 21:54:59 +05:30
FilterModeAll = iota
FilterModeAssign
FilterModeCreate
FilterModeMention
2014-05-07 21:39:30 +05:30
)
2015-08-10 19:17:23 +05:30
func parseCountResult ( results [ ] map [ string ] [ ] byte ) int64 {
if len ( results ) == 0 {
return 0
}
for _ , result := range results [ 0 ] {
return com . StrTo ( string ( result ) ) . MustInt64 ( )
}
return 0
}
2016-11-24 14:11:11 +05:30
// IssueStatsOptions contains parameters accepted by GetIssueStats.
2015-09-03 01:48:09 +05:30
type IssueStatsOptions struct {
RepoID int64
2016-04-26 09:37:49 +05:30
Labels string
2015-09-03 01:48:09 +05:30
MilestoneID int64
AssigneeID int64
2016-12-24 16:03:21 +05:30
MentionedID int64
PosterID int64
2018-11-29 07:16:30 +05:30
IsPull util . OptionalBool
2017-01-25 08:13:02 +05:30
IssueIDs [ ] int64
2015-09-03 01:48:09 +05:30
}
2014-05-08 02:21:14 +05:30
// GetIssueStats returns issue statistic information by given conditions.
2017-01-25 08:13:02 +05:30
func GetIssueStats ( opts * IssueStatsOptions ) ( * IssueStats , error ) {
2014-05-07 21:39:30 +05:30
stats := & IssueStats { }
2015-07-25 00:22:25 +05:30
2016-04-26 09:37:49 +05:30
countSession := func ( opts * IssueStatsOptions ) * xorm . Session {
2016-11-10 20:46:32 +05:30
sess := x .
2018-11-29 07:16:30 +05:30
Where ( "issue.repo_id = ?" , opts . RepoID )
2015-08-10 19:17:23 +05:30
2017-01-25 08:13:02 +05:30
if len ( opts . IssueIDs ) > 0 {
sess . In ( "issue.id" , opts . IssueIDs )
}
2016-05-07 01:10:41 +05:30
if len ( opts . Labels ) > 0 && opts . Labels != "0" {
2016-12-22 14:28:04 +05:30
labelIDs , err := base . StringsToInt64s ( strings . Split ( opts . Labels , "," ) )
if err != nil {
log . Warn ( "Malformed Labels argument: %s" , opts . Labels )
2019-01-23 09:40:38 +05:30
} else {
for i , labelID := range labelIDs {
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 ) )
}
2016-04-26 09:52:03 +05:30
}
2016-04-26 09:37:49 +05:30
}
if opts . MilestoneID > 0 {
sess . And ( "issue.milestone_id = ?" , opts . MilestoneID )
}
if opts . AssigneeID > 0 {
2018-05-09 21:59:04 +05:30
sess . Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , opts . AssigneeID )
2016-04-26 09:37:49 +05:30
}
2016-12-24 16:03:21 +05:30
if opts . PosterID > 0 {
2017-06-15 08:39:03 +05:30
sess . And ( "issue.poster_id = ?" , opts . PosterID )
2016-12-24 16:03:21 +05:30
}
2015-07-25 00:22:25 +05:30
2016-12-24 16:03:21 +05:30
if opts . MentionedID > 0 {
sess . Join ( "INNER" , "issue_user" , "issue.id = issue_user.issue_id" ) .
2016-12-30 12:56:05 +05:30
And ( "issue_user.uid = ?" , opts . MentionedID ) .
And ( "issue_user.is_mentioned = ?" , true )
2016-12-24 16:03:21 +05:30
}
2016-04-26 09:37:49 +05:30
2018-11-29 07:16:30 +05:30
switch opts . IsPull {
case util . OptionalBoolTrue :
sess . And ( "issue.is_pull=?" , true )
case util . OptionalBoolFalse :
sess . And ( "issue.is_pull=?" , false )
}
2016-12-24 16:03:21 +05:30
return sess
2015-07-25 00:22:25 +05:30
}
2016-12-24 16:03:21 +05:30
2017-01-25 08:13:02 +05:30
var err error
2017-06-15 08:39:03 +05:30
stats . OpenCount , err = countSession ( opts ) .
And ( "issue.is_closed = ?" , false ) .
Count ( new ( Issue ) )
if err != nil {
return stats , err
2017-01-25 08:13:02 +05:30
}
2017-06-15 08:39:03 +05:30
stats . ClosedCount , err = countSession ( opts ) .
And ( "issue.is_closed = ?" , true ) .
Count ( new ( Issue ) )
2017-02-14 19:45:18 +05:30
return stats , err
2014-05-07 21:39:30 +05:30
}
2017-12-26 04:55:16 +05:30
// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats.
type UserIssueStatsOptions struct {
2019-10-08 23:25:16 +05:30
UserID int64
RepoID int64
UserRepoIDs [ ] int64
FilterMode int
IsPull bool
IsClosed bool
2017-12-26 04:55:16 +05:30
}
2014-05-08 02:21:14 +05:30
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
2017-12-26 04:55:16 +05:30
func GetUserIssueStats ( opts UserIssueStatsOptions ) ( * IssueStats , error ) {
var err error
2014-05-07 21:39:30 +05:30
stats := & IssueStats { }
2015-08-25 20:28:34 +05:30
2017-12-26 04:55:16 +05:30
cond := builder . NewCond ( )
cond = cond . And ( builder . Eq { "issue.is_pull" : opts . IsPull } )
if opts . RepoID > 0 {
cond = cond . And ( builder . Eq { "issue.repo_id" : opts . RepoID } )
2015-09-03 01:48:09 +05:30
}
2017-12-26 04:55:16 +05:30
switch opts . FilterMode {
2017-02-14 19:45:18 +05:30
case FilterModeAll :
2017-12-26 04:55:16 +05:30
stats . OpenCount , err = x . Where ( cond ) . And ( "is_closed = ?" , false ) .
2019-10-08 23:25:16 +05:30
And ( builder . In ( "issue.repo_id" , opts . UserRepoIDs ) ) .
2017-02-14 19:45:18 +05:30
Count ( new ( Issue ) )
2017-12-26 04:55:16 +05:30
if err != nil {
return nil , err
}
stats . ClosedCount , err = x . Where ( cond ) . And ( "is_closed = ?" , true ) .
2019-10-08 23:25:16 +05:30
And ( builder . In ( "issue.repo_id" , opts . UserRepoIDs ) ) .
2017-02-14 19:45:18 +05:30
Count ( new ( Issue ) )
2017-12-26 04:55:16 +05:30
if err != nil {
return nil , err
}
2016-11-07 21:54:59 +05:30
case FilterModeAssign :
2019-10-03 05:33:18 +05:30
stats . OpenCount , err = x . Where ( cond ) . And ( "issue.is_closed = ?" , false ) .
2018-05-09 21:59:04 +05:30
Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , opts . UserID ) .
2017-02-14 19:45:18 +05:30
Count ( new ( Issue ) )
2017-12-26 04:55:16 +05:30
if err != nil {
return nil , err
}
2019-10-03 05:33:18 +05:30
stats . ClosedCount , err = x . Where ( cond ) . And ( "issue.is_closed = ?" , true ) .
2018-05-09 21:59:04 +05:30
Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , opts . UserID ) .
2017-02-14 19:45:18 +05:30
Count ( new ( Issue ) )
2017-12-26 04:55:16 +05:30
if err != nil {
return nil , err
}
2016-11-07 21:54:59 +05:30
case FilterModeCreate :
2017-12-26 04:55:16 +05:30
stats . OpenCount , err = x . Where ( cond ) . And ( "is_closed = ?" , false ) .
And ( "poster_id = ?" , opts . UserID ) .
2017-02-14 19:45:18 +05:30
Count ( new ( Issue ) )
2017-12-26 04:55:16 +05:30
if err != nil {
return nil , err
}
stats . ClosedCount , err = x . Where ( cond ) . And ( "is_closed = ?" , true ) .
And ( "poster_id = ?" , opts . UserID ) .
2017-02-14 19:45:18 +05:30
Count ( new ( Issue ) )
2017-12-26 04:55:16 +05:30
if err != nil {
return nil , err
}
2019-09-18 12:54:44 +05:30
case FilterModeMention :
2019-10-03 05:33:18 +05:30
stats . OpenCount , err = x . Where ( cond ) . And ( "issue.is_closed = ?" , false ) .
2019-09-18 12:54:44 +05:30
Join ( "INNER" , "issue_user" , "issue.id = issue_user.issue_id and issue_user.is_mentioned = ?" , true ) .
And ( "issue_user.uid = ?" , opts . UserID ) .
Count ( new ( Issue ) )
if err != nil {
return nil , err
}
2019-10-03 05:33:18 +05:30
stats . ClosedCount , err = x . Where ( cond ) . And ( "issue.is_closed = ?" , true ) .
2019-09-18 12:54:44 +05:30
Join ( "INNER" , "issue_user" , "issue.id = issue_user.issue_id and issue_user.is_mentioned = ?" , true ) .
And ( "issue_user.uid = ?" , opts . UserID ) .
Count ( new ( Issue ) )
if err != nil {
return nil , err
}
2017-12-26 04:55:16 +05:30
}
cond = cond . And ( builder . Eq { "issue.is_closed" : opts . IsClosed } )
stats . AssignCount , err = x . Where ( cond ) .
2018-05-09 21:59:04 +05:30
Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , opts . UserID ) .
2017-12-26 04:55:16 +05:30
Count ( new ( Issue ) )
if err != nil {
return nil , err
}
stats . CreateCount , err = x . Where ( cond ) .
And ( "poster_id = ?" , opts . UserID ) .
Count ( new ( Issue ) )
if err != nil {
return nil , err
}
2019-09-18 12:54:44 +05:30
stats . MentionCount , err = x . Where ( cond ) .
Join ( "INNER" , "issue_user" , "issue.id = issue_user.issue_id and issue_user.is_mentioned = ?" , true ) .
And ( "issue_user.uid = ?" , opts . UserID ) .
Count ( new ( Issue ) )
if err != nil {
return nil , err
}
2017-12-26 04:55:16 +05:30
stats . YourRepositoriesCount , err = x . Where ( cond ) .
2019-10-08 23:25:16 +05:30
And ( builder . In ( "issue.repo_id" , opts . UserRepoIDs ) ) .
2017-12-26 04:55:16 +05:30
Count ( new ( Issue ) )
if err != nil {
return nil , err
2015-08-25 20:28:34 +05:30
}
2017-12-26 04:55:16 +05:30
return stats , nil
2014-05-07 21:39:30 +05:30
}
2015-08-25 20:28:34 +05:30
// GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
2015-09-03 01:48:09 +05:30
func GetRepoIssueStats ( repoID , uid int64 , filterMode int , isPull bool ) ( numOpen int64 , numClosed int64 ) {
2016-04-26 09:37:49 +05:30
countSession := func ( isClosed , isPull bool , repoID int64 ) * xorm . Session {
2016-11-10 20:46:32 +05:30
sess := x .
2017-02-09 15:29:57 +05:30
Where ( "is_closed = ?" , isClosed ) .
2016-04-26 09:52:03 +05:30
And ( "is_pull = ?" , isPull ) .
And ( "repo_id = ?" , repoID )
2015-09-03 01:48:09 +05:30
2016-04-26 09:37:49 +05:30
return sess
2015-09-03 01:48:09 +05:30
}
2016-04-26 09:37:49 +05:30
openCountSession := countSession ( false , isPull , repoID )
closedCountSession := countSession ( true , isPull , repoID )
2015-08-25 20:28:34 +05:30
switch filterMode {
2016-11-07 21:54:59 +05:30
case FilterModeAssign :
2018-05-09 21:59:04 +05:30
openCountSession . Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , uid )
closedCountSession . Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , uid )
2016-11-07 21:54:59 +05:30
case FilterModeCreate :
2016-04-26 09:37:49 +05:30
openCountSession . And ( "poster_id = ?" , uid )
closedCountSession . And ( "poster_id = ?" , uid )
2015-08-25 20:28:34 +05:30
}
2017-02-14 19:45:18 +05:30
openResult , _ := openCountSession . Count ( new ( Issue ) )
closedResult , _ := closedCountSession . Count ( new ( Issue ) )
2016-04-26 09:37:49 +05:30
return openResult , closedResult
2015-08-25 20:28:34 +05:30
}
2019-02-21 10:31:28 +05:30
// SearchIssueIDsByKeyword search issues on database
func SearchIssueIDsByKeyword ( kw string , repoID int64 , limit , start int ) ( int64 , [ ] int64 , error ) {
var repoCond = builder . Eq { "repo_id" : repoID }
var subQuery = builder . Select ( "id" ) . From ( "issue" ) . Where ( repoCond )
var cond = builder . And (
repoCond ,
builder . Or (
builder . Like { "name" , kw } ,
builder . Like { "content" , kw } ,
builder . In ( "id" , builder . Select ( "issue_id" ) .
From ( "comment" ) .
Where ( builder . And (
builder . Eq { "type" : CommentTypeComment } ,
builder . In ( "issue_id" , subQuery ) ,
builder . Like { "content" , kw } ,
) ) ,
) ,
) ,
)
var ids = make ( [ ] int64 , 0 , limit )
err := x . Distinct ( "id" ) . Table ( "issue" ) . Where ( cond ) . Limit ( limit , start ) . Find ( & ids )
if err != nil {
return 0 , nil , err
}
total , err := x . Distinct ( "id" ) . Table ( "issue" ) . Where ( cond ) . Count ( )
if err != nil {
return 0 , nil , err
}
return total , ids , nil
}
2015-08-10 16:27:57 +05:30
func updateIssue ( e Engine , issue * Issue ) error {
2017-10-05 10:13:04 +05:30
_ , err := e . ID ( issue . ID ) . AllCols ( ) . Update ( issue )
2017-01-25 08:13:02 +05:30
if err != nil {
return err
}
return nil
2015-08-10 16:27:57 +05:30
}
2015-10-24 13:06:47 +05:30
// UpdateIssue updates all fields of given issue.
func UpdateIssue ( issue * Issue ) error {
2019-09-20 11:15:38 +05:30
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return err
}
if err := updateIssue ( sess , issue ) ; err != nil {
return err
}
if err := issue . neuterCrossReferences ( sess ) ; err != nil {
return err
}
if err := issue . loadPoster ( sess ) ; err != nil {
return err
}
if err := issue . addCrossReferences ( sess , issue . Poster ) ; err != nil {
return err
}
return sess . Commit ( )
2015-10-24 13:06:47 +05:30
}
2018-05-02 00:35:28 +05:30
// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
2019-08-15 20:16:21 +05:30
func UpdateIssueDeadline ( issue * Issue , deadlineUnix timeutil . TimeStamp , doer * User ) ( err error ) {
2018-05-02 00:35:28 +05:30
// if the deadline hasn't changed do nothing
if issue . DeadlineUnix == deadlineUnix {
return nil
}
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return err
}
// Update the deadline
if err = updateIssueCols ( sess , & Issue { ID : issue . ID , DeadlineUnix : deadlineUnix } , "deadline_unix" ) ; err != nil {
return err
}
// Make the comment
if _ , err = createDeadlineComment ( sess , doer , issue , deadlineUnix ) ; err != nil {
return fmt . Errorf ( "createRemovedDueDateComment: %v" , err )
}
return sess . Commit ( )
}
2018-07-18 02:53:58 +05:30
// Get Blocked By Dependencies, aka all issues this issue is blocked by.
func ( issue * Issue ) getBlockedByDependencies ( e Engine ) ( issueDeps [ ] * Issue , err error ) {
return issueDeps , e .
Table ( "issue_dependency" ) .
Select ( "issue.*" ) .
Join ( "INNER" , "issue" , "issue.id = issue_dependency.dependency_id" ) .
Where ( "issue_id = ?" , issue . ID ) .
Find ( & issueDeps )
}
// Get Blocking Dependencies, aka all issues this issue blocks.
func ( issue * Issue ) getBlockingDependencies ( e Engine ) ( issueDeps [ ] * Issue , err error ) {
return issueDeps , e .
Table ( "issue_dependency" ) .
Select ( "issue.*" ) .
Join ( "INNER" , "issue" , "issue.id = issue_dependency.issue_id" ) .
Where ( "dependency_id = ?" , issue . ID ) .
Find ( & issueDeps )
}
// BlockedByDependencies finds all Dependencies an issue is blocked by
func ( issue * Issue ) BlockedByDependencies ( ) ( [ ] * Issue , error ) {
return issue . getBlockedByDependencies ( x )
}
// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
func ( issue * Issue ) BlockingDependencies ( ) ( [ ] * Issue , error ) {
return issue . getBlockingDependencies ( x )
}
2019-07-19 03:21:33 +05:30
func ( issue * Issue ) updateClosedNum ( e Engine ) ( err error ) {
if issue . IsPull {
_ , err = e . Exec ( "UPDATE `repository` SET num_closed_pulls=(SELECT count(*) FROM issue WHERE repo_id=? AND is_pull=? AND is_closed=?) WHERE id=?" ,
issue . RepoID ,
true ,
true ,
issue . RepoID ,
)
} else {
_ , err = e . Exec ( "UPDATE `repository` SET num_closed_issues=(SELECT count(*) FROM issue WHERE repo_id=? AND is_pull=? AND is_closed=?) WHERE id=?" ,
issue . RepoID ,
false ,
true ,
issue . RepoID ,
)
}
return
}
2019-10-10 22:15:11 +05:30
// ResolveMentionsByVisibility returns the users mentioned in an issue, removing those that
// don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
func ( issue * Issue ) ResolveMentionsByVisibility ( ctx DBContext , doer * User , mentions [ ] string ) ( users [ ] * User , err error ) {
if len ( mentions ) == 0 {
return
}
if err = issue . loadRepo ( ctx . e ) ; err != nil {
return
}
resolved := make ( map [ string ] bool , 20 )
names := make ( [ ] string , 0 , 20 )
resolved [ doer . LowerName ] = true
for _ , name := range mentions {
name := strings . ToLower ( name )
if _ , ok := resolved [ name ] ; ok {
continue
}
resolved [ name ] = false
names = append ( names , name )
}
if err := issue . Repo . getOwner ( ctx . e ) ; err != nil {
return nil , err
}
if issue . Repo . Owner . IsOrganization ( ) {
// Since there can be users with names that match the name of a team,
// if the team exists and can read the issue, the team takes precedence.
teams := make ( [ ] * Team , 0 , len ( names ) )
if err := ctx . e .
Join ( "INNER" , "team_repo" , "team_repo.team_id = team.id" ) .
Where ( "team_repo.repo_id=?" , issue . Repo . ID ) .
In ( "team.lower_name" , names ) .
Find ( & teams ) ; err != nil {
return nil , fmt . Errorf ( "find mentioned teams: %v" , err )
}
if len ( teams ) != 0 {
checked := make ( [ ] int64 , 0 , len ( teams ) )
unittype := UnitTypeIssues
if issue . IsPull {
unittype = UnitTypePullRequests
}
for _ , team := range teams {
if team . Authorize >= AccessModeOwner {
checked = append ( checked , team . ID )
resolved [ team . LowerName ] = true
continue
}
has , err := ctx . e . Get ( & TeamUnit { OrgID : issue . Repo . Owner . ID , TeamID : team . ID , Type : unittype } )
if err != nil {
return nil , fmt . Errorf ( "get team units (%d): %v" , team . ID , err )
}
if has {
checked = append ( checked , team . ID )
resolved [ team . LowerName ] = true
}
}
if len ( checked ) != 0 {
teamusers := make ( [ ] * User , 0 , 20 )
if err := ctx . e .
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 {
return nil , fmt . Errorf ( "get teams users: %v" , err )
}
if len ( teamusers ) > 0 {
users = make ( [ ] * User , 0 , len ( teamusers ) )
for _ , user := range teamusers {
if already , ok := resolved [ user . LowerName ] ; ! ok || ! already {
users = append ( users , user )
resolved [ user . LowerName ] = true
}
}
}
}
}
// Remove names already in the list to avoid querying the database if pending names remain
names = make ( [ ] string , 0 , len ( resolved ) )
for name , already := range resolved {
if ! already {
names = append ( names , name )
}
}
if len ( names ) == 0 {
return
}
}
unchecked := make ( [ ] * User , 0 , len ( names ) )
if err := ctx . e .
Where ( "`user`.is_active = ?" , true ) .
And ( "`user`.prohibit_login = ?" , false ) .
In ( "`user`.lower_name" , names ) .
Find ( & unchecked ) ; err != nil {
return nil , fmt . Errorf ( "find mentioned users: %v" , err )
}
for _ , user := range unchecked {
if already := resolved [ user . LowerName ] ; already || user . IsOrganization ( ) {
continue
}
// Normal users must have read access to the referencing issue
perm , err := getUserRepoPermission ( ctx . e , issue . Repo , user )
if err != nil {
return nil , fmt . Errorf ( "getUserRepoPermission [%d]: %v" , user . ID , err )
}
if ! perm . CanReadIssuesOrPulls ( issue . IsPull ) {
continue
}
users = append ( users , user )
}
return
}