2014-03-20 21:04:56 +01:00
// 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 18:50:50 +01:00
import (
2015-08-10 08:42:50 +02:00
"fmt"
2017-03-03 15:35:42 +01:00
"path"
2018-01-03 09:34:13 +01:00
"regexp"
2017-01-30 13:46:45 +01:00
"sort"
2014-03-22 18:50:50 +01:00
"strings"
2014-05-07 22:51:14 +02:00
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2019-10-14 08:10:42 +02:00
"code.gitea.io/gitea/modules/structs"
2019-05-11 12:21:34 +02:00
api "code.gitea.io/gitea/modules/structs"
2019-08-15 16:46:21 +02:00
"code.gitea.io/gitea/modules/timeutil"
2017-01-25 03:43:02 +01:00
"code.gitea.io/gitea/modules/util"
2017-12-26 00:25:16 +01:00
"github.com/go-xorm/xorm"
2019-08-23 18:40:30 +02:00
"github.com/unknwon/com"
2019-06-23 17:22:43 +02:00
"xorm.io/builder"
2014-03-22 18:50:50 +01:00
)
// Issue represents an issue or pull request of repository.
2014-03-20 21:04:56 +01:00
type Issue struct {
2019-07-08 04:14:12 +02:00
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
2019-10-14 08:10:42 +02:00
OriginalAuthorID int64 ` xorm:"index" `
2019-07-08 04:14:12 +02:00
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 01:53:30 +01:00
2019-08-15 16:46:21 +02:00
DeadlineUnix timeutil . TimeStamp ` xorm:"INDEX" `
2018-05-01 21:05:28 +02:00
2019-08-15 16:46:21 +02:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
ClosedUnix timeutil . TimeStamp ` xorm:"INDEX" `
2015-08-12 11:04:23 +02:00
2018-04-29 07:58:47 +02:00
Attachments [ ] * Attachment ` xorm:"-" `
Comments [ ] * Comment ` xorm:"-" `
Reactions ReactionList ` xorm:"-" `
TotalTrackedTime int64 ` xorm:"-" `
2018-05-09 18:29:04 +02:00
Assignees [ ] * User ` xorm:"-" `
2019-02-18 21:55:04 +01:00
// IsLocked limits commenting abilities to users on an issue
// with write access
IsLocked bool ` xorm:"NOT NULL DEFAULT false" `
2015-08-12 11:04:23 +02:00
}
2018-01-03 09:34:13 +01:00
var (
issueTasksPat * regexp . Regexp
issueTasksDonePat * regexp . Regexp
)
2018-01-03 20:45:21 +01:00
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 04:17:23 +02:00
const issueMaxDupIndexAttempts = 3
2018-01-03 09:34:13 +01:00
func init ( ) {
issueTasksPat = regexp . MustCompile ( issueTasksRegexpStr )
issueTasksDonePat = regexp . MustCompile ( issueTasksDoneRegexpStr )
}
2018-04-29 07:58:47 +02:00
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-01 21:05:28 +02:00
// IsOverdue checks if the issue is overdue
func ( issue * Issue ) IsOverdue ( ) bool {
2019-08-15 16:46:21 +02:00
return timeutil . TimeStampNow ( ) >= issue . DeadlineUnix
2018-05-01 21:05:28 +02:00
}
2018-12-13 16:55:43 +01:00
// LoadRepo loads issue's repository
func ( issue * Issue ) LoadRepo ( ) error {
return issue . loadRepo ( x )
}
2016-12-17 12:49:17 +01:00
func ( issue * Issue ) loadRepo ( e Engine ) ( err error ) {
2016-08-26 22:40:53 +02:00
if issue . Repo == nil {
issue . Repo , err = getRepositoryByID ( e , issue . RepoID )
2016-03-14 04:20:22 +01:00
if err != nil {
2016-08-26 22:40:53 +02:00
return fmt . Errorf ( "getRepositoryByID [%d]: %v" , issue . RepoID , err )
2016-03-14 04:20:22 +01:00
}
2016-08-26 22:40:53 +02:00
}
2016-12-17 12:49:17 +01:00
return nil
}
2018-04-29 07:58:47 +02:00
// IsTimetrackerEnabled returns true if the repo enables timetracking
func ( issue * Issue ) IsTimetrackerEnabled ( ) bool {
2019-01-14 03:29:58 +01:00
return issue . isTimetrackerEnabled ( x )
}
func ( issue * Issue ) isTimetrackerEnabled ( e Engine ) bool {
if err := issue . loadRepo ( e ) ; err != nil {
2019-04-02 09:48:31 +02:00
log . Error ( fmt . Sprintf ( "loadRepo: %v" , err ) )
2018-04-29 07:58:47 +02:00
return false
}
return issue . Repo . IsTimetrackerEnabled ( )
}
2017-01-28 17:01:07 +01: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" )
}
pr , err = getPullRequestByIssueID ( x , issue . ID )
2018-10-18 13:23:05 +02:00
if err != nil {
return nil , err
}
pr . Issue = issue
2017-01-28 17:01:07 +01:00
return
}
2017-01-30 13:46:45 +01:00
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 12:49:17 +01:00
}
2017-01-30 13:46:45 +01:00
return nil
}
2016-03-14 04:20:22 +01:00
2018-12-13 16:55:43 +01:00
// LoadPoster loads poster
func ( issue * Issue ) LoadPoster ( ) error {
return issue . loadPoster ( x )
}
2017-01-30 13:46:45 +01:00
func ( issue * Issue ) loadPoster ( e Engine ) ( err error ) {
2016-08-26 22:40:53 +02:00
if issue . Poster == nil {
issue . Poster , err = getUserByID ( e , issue . PosterID )
2016-03-14 04:20:22 +01:00
if err != nil {
2016-09-20 11:54:47 +02:00
issue . PosterID = - 1
issue . Poster = NewGhostUser ( )
2016-11-04 19:47:54 +01:00
if ! IsErrUserNotExist ( err ) {
2016-08-26 22:40:53 +02:00
return fmt . Errorf ( "getUserByID.(poster) [%d]: %v" , issue . PosterID , err )
2016-03-14 04:20:22 +01:00
}
2016-11-09 06:07:01 +01:00
err = nil
2016-03-14 04:20:22 +01:00
return
}
2016-08-26 22:40:53 +02:00
}
2017-01-30 13:46:45 +01:00
return
}
2016-03-14 04:20:22 +01:00
2017-07-26 09:16:45 +02:00
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 16:55:43 +01:00
issue . PullRequest . Issue = issue
2017-07-26 09:16:45 +02:00
}
return nil
}
2018-12-13 16:55:43 +01:00
// LoadPullRequest loads pull request info
func ( issue * Issue ) LoadPullRequest ( ) error {
return issue . loadPullRequest ( x )
}
2017-09-16 22:16:21 +02:00
func ( issue * Issue ) loadComments ( e Engine ) ( err error ) {
2019-02-19 15:39:39 +01:00
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-16 22:16:21 +02:00
if issue . Comments != nil {
return nil
}
issue . Comments , err = findComments ( e , FindCommentsOptions {
IssueID : issue . ID ,
2019-02-19 15:39:39 +01:00
Type : tp ,
2017-09-16 22:16:21 +02:00
} )
return err
}
2017-12-04 00:14:26 +01:00
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 03:29:58 +01:00
if _ , err := ReactionList ( reactions ) . loadUsers ( e ) ; err != nil {
2017-12-04 00:14:26 +01: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
}
2017-01-30 13:46:45 +01:00
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-26 22:40:53 +02:00
}
2015-08-10 15:47:23 +02:00
2016-08-26 22:40:53 +02:00
if issue . Milestone == nil && issue . MilestoneID > 0 {
issue . Milestone , err = getMilestoneByRepoID ( e , issue . RepoID , issue . MilestoneID )
2017-08-12 04:15:30 +02:00
if err != nil && ! IsErrMilestoneNotExist ( err ) {
2016-08-26 22:40:53 +02:00
return fmt . Errorf ( "getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v" , issue . RepoID , issue . MilestoneID , err )
2015-08-10 15:47:23 +02:00
}
2015-08-05 14:23:08 +02:00
}
2018-05-09 18:29:04 +02:00
if err = issue . loadAssignees ( e ) ; err != nil {
2017-06-23 15:43:37 +02:00
return
2016-08-14 12:32:24 +02:00
}
2017-07-26 09:16:45 +02:00
if err = issue . loadPullRequest ( e ) ; err != nil && ! IsErrPullRequestNotExist ( err ) {
2016-08-14 12:32:24 +02:00
// It is possible pull request is not yet created.
2017-07-26 09:16:45 +02:00
return err
2016-08-14 12:32:24 +02:00
}
2016-08-26 22:40:53 +02:00
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-16 22:16:21 +02:00
if err = issue . loadComments ( e ) ; err != nil {
2017-12-04 00:14:26 +01:00
return err
2016-08-26 22:40:53 +02:00
}
2019-04-18 07:00:03 +02:00
if err = CommentList ( issue . Comments ) . loadAttributes ( e ) ; err != nil {
return err
}
2019-01-14 03:29:58 +01:00
if issue . isTimetrackerEnabled ( e ) {
2018-04-29 07:58:47 +02:00
if err = issue . loadTotalTimes ( e ) ; err != nil {
return err
}
}
2016-08-26 22:40:53 +02:00
2017-12-04 00:14:26 +01:00
return issue . loadReactions ( e )
2016-08-14 12:32:24 +02:00
}
2016-11-24 09:41:11 +01:00
// LoadAttributes loads the attribute of this issue.
2016-08-14 12:32:24 +02:00
func ( issue * Issue ) LoadAttributes ( ) error {
return issue . loadAttributes ( x )
2015-08-19 18:12:43 +02:00
}
2017-02-03 08:22:39 +01:00
// 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 04:47:24 +01:00
issue . IsRead = false
return nil
2017-02-03 08:22:39 +01:00
}
issue . IsRead = issueUser . IsRead
return nil
}
2017-03-03 15:35:42 +01:00
// APIURL returns the absolute APIURL to this issue.
func ( issue * Issue ) APIURL ( ) string {
2018-01-04 02:41:33 +01:00
return issue . Repo . APIURL ( ) + "/" + path . Join ( "issues" , fmt . Sprint ( issue . Index ) )
2017-03-03 15:35:42 +01:00
}
2016-11-24 09:41:11 +01:00
// HTMLURL returns the absolute URL to this issue.
2016-08-16 19:19:09 +02: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 )
}
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-14 04:20:22 +01: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-14 04:20:22 +01:00
}
2016-11-29 09:25:47 +01:00
return api . StateOpen
2016-08-14 12:32:24 +02:00
}
2016-11-22 12:24:39 +01:00
// APIFormat assumes some fields assigned with values:
2016-08-14 12:32:24 +02:00
// Required - Poster, Labels,
// Optional - Milestone, Assignee, PullRequest
func ( issue * Issue ) APIFormat ( ) * api . Issue {
2018-12-13 16:55:43 +01:00
return issue . apiFormat ( x )
}
func ( issue * Issue ) apiFormat ( e Engine ) * api . Issue {
issue . loadLabels ( e )
2016-08-14 12:32:24 +02:00
apiLabels := make ( [ ] * api . Label , len ( issue . Labels ) )
for i := range issue . Labels {
apiLabels [ i ] = issue . Labels [ i ] . APIFormat ( )
}
2018-12-13 16:55:43 +01:00
issue . loadPoster ( e )
issue . loadRepo ( e )
2016-08-14 12:32:24 +02:00
apiIssue := & api . Issue {
ID : issue . ID ,
2017-03-03 15:35:42 +01:00
URL : issue . APIURL ( ) ,
2016-08-14 12:32:24 +02:00
Index : issue . Index ,
2016-08-16 19:19:09 +02:00
Poster : issue . Poster . APIFormat ( ) ,
2016-08-14 12:32:24 +02:00
Title : issue . Title ,
Body : issue . Content ,
Labels : apiLabels ,
2016-08-16 19:19:09 +02:00
State : issue . State ( ) ,
2016-08-14 12:32:24 +02:00
Comments : issue . NumComments ,
2017-12-11 05:37:04 +01:00
Created : issue . CreatedUnix . AsTime ( ) ,
Updated : issue . UpdatedUnix . AsTime ( ) ,
2016-08-14 12:32:24 +02:00
}
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-02 21:43:01 +01:00
if issue . ClosedUnix != 0 {
apiIssue . Closed = issue . ClosedUnix . AsTimePtr ( )
}
2016-08-14 12:32:24 +02:00
if issue . Milestone != nil {
apiIssue . Milestone = issue . Milestone . APIFormat ( )
}
2018-12-13 16:55:43 +01:00
issue . loadAssignees ( e )
2018-05-09 18:29:04 +02:00
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 12:32:24 +02:00
}
if issue . IsPull {
2018-12-13 16:55:43 +01:00
issue . loadPullRequest ( e )
2016-08-14 12:32:24 +02:00
apiIssue . PullRequest = & api . PullRequestMeta {
HasMerged : issue . PullRequest . HasMerged ,
}
if issue . PullRequest . HasMerged {
2017-12-11 05:37:04 +01:00
apiIssue . PullRequest . Merged = issue . PullRequest . MergedUnix . AsTimePtr ( )
2016-08-14 12:32:24 +02:00
}
}
2018-05-01 21:05:28 +02:00
if issue . DeadlineUnix != 0 {
apiIssue . Deadline = issue . DeadlineUnix . AsTimePtr ( )
}
2016-08-14 12:32:24 +02:00
return apiIssue
}
// HashTag returns unique hash tag for issue.
2016-11-22 12:24:39 +01:00
func ( issue * Issue ) HashTag ( ) string {
return "issue-" + com . ToStr ( issue . ID )
2016-03-14 04:20:22 +01:00
}
2015-08-13 10:07:11 +02: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 {
return issue . PosterID == uid
2015-08-13 10:07:11 +02:00
}
2016-11-22 12:24:39 +01:00
func ( issue * Issue ) hasLabel ( e Engine , labelID int64 ) bool {
return hasIssueLabel ( e , issue . ID , labelID )
2015-08-10 08:42:50 +02:00
}
// HasLabel returns true if issue has been labeled by given ID.
2016-11-22 12:24:39 +01:00
func ( issue * Issue ) HasLabel ( labelID int64 ) bool {
return issue . hasLabel ( x , labelID )
2015-08-10 08:42:50 +02:00
}
2016-08-14 12:32:24 +02:00
func ( issue * Issue ) sendLabelUpdatedWebhook ( doer * User ) {
var err error
2018-05-21 04:28:29 +02:00
if err = issue . loadRepo ( x ) ; err != nil {
2019-04-02 09:48:31 +02:00
log . Error ( "loadRepo: %v" , err )
2018-05-21 04:28:29 +02:00
return
}
if err = issue . loadPoster ( x ) ; err != nil {
2019-04-02 09:48:31 +02:00
log . Error ( "loadPoster: %v" , err )
2018-05-21 04:28:29 +02:00
return
}
2018-11-28 12:26:14 +01:00
mode , _ := AccessLevel ( issue . Poster , issue . Repo )
2016-08-14 12:32:24 +02:00
if issue . IsPull {
2017-07-26 09:16:45 +02:00
if err = issue . loadPullRequest ( x ) ; err != nil {
2019-04-02 09:48:31 +02:00
log . Error ( "loadPullRequest: %v" , err )
2017-07-26 09:16:45 +02:00
return
}
if err = issue . PullRequest . LoadIssue ( ) ; err != nil {
2019-04-02 09:48:31 +02:00
log . Error ( "LoadIssue: %v" , err )
2016-08-24 21:01:30 +02:00
return
}
2016-11-07 16:37:32 +01:00
err = PrepareWebhooks ( issue . Repo , HookEventPullRequest , & api . PullRequestPayload {
Action : api . HookIssueLabelUpdated ,
2016-08-14 12:32:24 +02:00
Index : issue . Index ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2016-12-06 00:48:51 +01:00
Repository : issue . Repo . APIFormat ( AccessModeNone ) ,
2016-08-14 12:32:24 +02:00
Sender : doer . APIFormat ( ) ,
} )
2018-05-21 04:28:29 +02:00
} 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 12:32:24 +02:00
}
if err != nil {
2019-04-02 09:48:31 +02:00
log . Error ( "PrepareWebhooks [is_pull: %v]: %v" , issue . IsPull , err )
2016-08-14 12:32:24 +02:00
} else {
go HookQueue . Add ( issue . RepoID )
}
}
2019-07-17 21:02:42 +02:00
// 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 13:46:45 +01:00
func ( issue * Issue ) addLabel ( e * xorm . Session , label * Label , doer * User ) error {
return newIssueLabel ( e , issue , label , doer )
2015-08-10 08:42:50 +02:00
}
2016-08-03 20:51:22 +02:00
// AddLabel adds a new label to the issue.
2016-08-14 12:32:24 +02:00
func ( issue * Issue ) AddLabel ( doer * User , label * Label ) error {
2017-01-30 13:46:45 +01:00
if err := NewIssueLabel ( issue , label , doer ) ; err != nil {
2015-08-14 18:42:43 +02:00
return err
}
2016-08-14 12:32:24 +02:00
issue . sendLabelUpdatedWebhook ( doer )
return nil
2015-08-10 08:42:50 +02:00
}
2017-01-30 13:46:45 +01:00
func ( issue * Issue ) addLabels ( e * xorm . Session , labels [ ] * Label , doer * User ) error {
return newIssueLabels ( e , issue , labels , doer )
2016-08-03 20:51:22 +02:00
}
// AddLabels adds a list of new labels to the issue.
2016-08-14 12:32:24 +02:00
func ( issue * Issue ) AddLabels ( doer * User , labels [ ] * Label ) error {
2017-01-30 13:46:45 +01:00
if err := NewIssueLabels ( issue , labels , doer ) ; err != nil {
2016-08-14 12:32:24 +02:00
return err
}
issue . sendLabelUpdatedWebhook ( doer )
return nil
2016-08-03 20:51:22 +02:00
}
func ( issue * Issue ) getLabels ( e Engine ) ( err error ) {
if len ( issue . Labels ) > 0 {
2014-05-24 08:31:58 +02:00
return nil
}
2016-08-03 20:51:22 +02:00
issue . Labels , err = getLabelsByIssueID ( e , issue . ID )
2015-08-10 08:42:50 +02:00
if err != nil {
return fmt . Errorf ( "getLabelsByIssueID: %v" , err )
2014-05-24 08:31:58 +02:00
}
return nil
}
2017-01-30 13:46:45 +01:00
func ( issue * Issue ) removeLabel ( e * xorm . Session , doer * User , label * Label ) error {
2017-02-01 02:31:35 +01:00
return deleteIssueLabel ( e , issue , label , doer )
2015-08-10 08:42:50 +02:00
}
// RemoveLabel removes a label from issue by given ID.
2016-08-14 12:32:24 +02:00
func ( issue * Issue ) RemoveLabel ( doer * User , label * Label ) error {
2016-12-17 12:49:17 +01:00
if err := issue . loadRepo ( x ) ; err != nil {
return err
}
2018-11-28 12:26:14 +01:00
perm , err := GetUserRepoPermission ( issue . Repo , doer )
if err != nil {
2016-12-17 12:49:17 +01:00
return err
2018-11-28 12:26:14 +01:00
}
if ! perm . CanWriteIssuesOrPulls ( issue . IsPull ) {
2016-12-17 12:49:17 +01:00
return ErrLabelNotExist { }
}
2017-02-01 02:31:35 +01:00
if err := DeleteIssueLabel ( issue , label , doer ) ; err != nil {
2016-08-14 12:32:24 +02:00
return err
}
issue . sendLabelUpdatedWebhook ( doer )
return nil
2016-08-03 20:51:22 +02:00
}
2017-01-30 13:46:45 +01:00
func ( issue * Issue ) clearLabels ( e * xorm . Session , doer * User ) ( err error ) {
2016-08-03 20:51:22 +02:00
if err = issue . getLabels ( e ) ; err != nil {
return fmt . Errorf ( "getLabels: %v" , err )
}
for i := range issue . Labels {
2017-01-30 13:46:45 +01:00
if err = issue . removeLabel ( e , doer , issue . Labels [ i ] ) ; err != nil {
2016-08-03 20:51:22 +02:00
return fmt . Errorf ( "removeLabel: %v" , err )
}
}
return nil
}
2016-11-24 09:41:11 +01:00
// ClearLabels removes all issue labels as the given user.
// Triggers appropriate WebHooks, if any.
2016-08-14 12:32:24 +02:00
func ( issue * Issue ) ClearLabels ( doer * User ) ( err error ) {
2015-08-14 18:42:43 +02:00
sess := x . NewSession ( )
2017-06-21 02:57:05 +02:00
defer sess . Close ( )
2015-08-14 18:42:43 +02:00
if err = sess . Begin ( ) ; err != nil {
return err
}
2016-12-17 12:49:17 +01:00
if err := issue . loadRepo ( sess ) ; err != nil {
return err
2017-07-26 09:16:45 +02:00
} else if err = issue . loadPullRequest ( sess ) ; err != nil {
return err
2016-12-17 12:49:17 +01:00
}
2018-11-28 12:26:14 +01:00
perm , err := getUserRepoPermission ( sess , issue . Repo , doer )
if err != nil {
2016-12-17 12:49:17 +01:00
return err
2018-11-28 12:26:14 +01:00
}
if ! perm . CanWriteIssuesOrPulls ( issue . IsPull ) {
2016-12-17 12:49:17 +01:00
return ErrLabelNotExist { }
}
2017-01-30 13:46:45 +01:00
if err = issue . clearLabels ( sess , doer ) ; err != nil {
2015-08-14 18:42:43 +02:00
return err
}
2016-08-14 12:32:24 +02:00
if err = sess . Commit ( ) ; err != nil {
return fmt . Errorf ( "Commit: %v" , err )
}
2019-09-20 07:45:38 +02:00
sess . Close ( )
2016-08-14 12:32:24 +02:00
2019-09-20 07:45:38 +02:00
if err = issue . LoadPoster ( ) ; err != nil {
2018-05-21 04:28:29 +02:00
return fmt . Errorf ( "loadPoster: %v" , err )
}
2018-11-28 12:26:14 +01:00
mode , _ := AccessLevel ( issue . Poster , issue . Repo )
2016-08-14 12:32:24 +02:00
if issue . IsPull {
2016-08-24 21:01:30 +02:00
err = issue . PullRequest . LoadIssue ( )
if err != nil {
2019-04-02 09:48:31 +02:00
log . Error ( "LoadIssue: %v" , err )
2016-08-24 21:01:30 +02:00
return
}
2016-11-07 16:37:32 +01:00
err = PrepareWebhooks ( issue . Repo , HookEventPullRequest , & api . PullRequestPayload {
Action : api . HookIssueLabelCleared ,
2016-08-14 12:32:24 +02:00
Index : issue . Index ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2018-05-21 04:28:29 +02:00
Repository : issue . Repo . APIFormat ( mode ) ,
2016-08-14 12:32:24 +02:00
Sender : doer . APIFormat ( ) ,
} )
2018-05-21 04:28:29 +02:00
} 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 12:32:24 +02:00
}
if err != nil {
2019-04-02 09:48:31 +02:00
log . Error ( "PrepareWebhooks [is_pull: %v]: %v" , issue . IsPull , err )
2016-08-14 12:32:24 +02:00
} else {
go HookQueue . Add ( issue . RepoID )
}
return nil
2015-08-14 18:42:43 +02:00
}
2017-01-30 13:46:45 +01: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 ]
}
2016-08-03 20:51:22 +02:00
// ReplaceLabels removes all current labels and add new labels to the issue.
2016-11-24 09:41:11 +01:00
// Triggers appropriate WebHooks, if any.
2017-01-30 13:46:45 +01:00
func ( issue * Issue ) ReplaceLabels ( labels [ ] * Label , doer * User ) ( err error ) {
2015-08-14 18:42:43 +02:00
sess := x . NewSession ( )
2017-06-21 02:57:05 +02:00
defer sess . Close ( )
2015-08-14 18:42:43 +02:00
if err = sess . Begin ( ) ; err != nil {
return err
}
2017-01-30 13:46:45 +01:00
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 02:35:55 +01: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 {
addIndex ++
removeIndex ++
} else if addLabel . ID < removeLabel . ID {
toAdd = append ( toAdd , addLabel )
addIndex ++
} else {
toRemove = append ( toRemove , removeLabel )
removeIndex ++
2017-01-30 13:46:45 +01:00
}
}
2017-02-28 02:35:55 +01:00
toAdd = append ( toAdd , labels [ addIndex : ] ... )
toRemove = append ( toRemove , issue . Labels [ removeIndex : ] ... )
2017-01-30 13:46:45 +01:00
if len ( toAdd ) > 0 {
if err = issue . addLabels ( sess , toAdd , doer ) ; err != nil {
return fmt . Errorf ( "addLabels: %v" , err )
}
}
2017-02-28 02:35:55 +01:00
for _ , l := range toRemove {
if err = issue . removeLabel ( sess , doer , l ) ; err != nil {
return fmt . Errorf ( "removeLabel: %v" , err )
2017-01-30 13:46:45 +01:00
}
2015-08-14 18:42:43 +02:00
}
return sess . Commit ( )
2015-08-10 08:42:50 +02:00
}
2015-08-12 12:44:09 +02:00
// ReadBy sets issue to be read by given user.
2016-12-30 17:44:54 +01:00
func ( issue * Issue ) ReadBy ( userID int64 ) error {
if err := UpdateIssueUserByRead ( userID , issue . ID ) ; err != nil {
return err
}
2017-09-19 10:08:30 +02:00
return setNotificationStatusReadIfUnread ( x , userID , issue . ID )
2014-07-23 21:15:47 +02:00
}
2016-08-14 12:32:24 +02:00
func updateIssueCols ( e Engine , issue * Issue , cols ... string ) error {
2017-10-05 06:43:04 +02:00
if _ , err := e . ID ( issue . ID ) . Cols ( cols ... ) . Update ( issue ) ; err != nil {
2017-01-25 03:43:02 +01:00
return err
}
return nil
2016-08-14 12:32:24 +02:00
}
2018-12-13 16:55:43 +01:00
func ( issue * Issue ) changeStatus ( e * xorm . Session , doer * User , isClosed bool ) ( err error ) {
2019-03-05 03:52:52 +01:00
// Reload the issue
currentIssue , err := getIssueByID ( e , issue . ID )
if err != nil {
return err
}
2016-03-05 18:58:51 +01:00
// Nothing should be performed if current status is same as target status
2019-03-05 03:52:52 +01:00
if currentIssue . IsClosed == isClosed {
2015-08-13 10:07:11 +02:00
return nil
}
2018-07-17 23:23:58 +02:00
// Check for open dependencies
2018-10-27 16:45:24 +02:00
if isClosed && issue . Repo . isDependenciesEnabled ( e ) {
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
2018-10-27 16:45:24 +02:00
noDeps , err := issueNoDependenciesLeft ( e , issue )
2018-07-17 23:23:58 +02:00
if err != nil {
return err
}
if ! noDeps {
return ErrDependenciesLeft { issue . ID }
}
}
2016-11-22 12:24:39 +01:00
issue . IsClosed = isClosed
2018-02-19 03:39:26 +01:00
if isClosed {
2019-08-15 16:46:21 +02:00
issue . ClosedUnix = timeutil . TimeStampNow ( )
2018-02-19 03:39:26 +01:00
} else {
issue . ClosedUnix = 0
}
2015-08-13 10:07:11 +02:00
2018-02-19 03:39:26 +01:00
if err = updateIssueCols ( e , issue , "is_closed" , "closed_unix" ) ; err != nil {
2015-08-13 10:07:11 +02:00
return err
}
2016-03-05 18:58:51 +01:00
// Update issue count of labels
2016-11-22 12:24:39 +01:00
if err = issue . getLabels ( e ) ; err != nil {
2015-08-13 10:07:11 +02:00
return err
}
2016-11-22 12:24:39 +01:00
for idx := range issue . Labels {
if err = updateLabel ( e , issue . Labels [ idx ] ) ; err != nil {
2015-08-13 10:07:11 +02:00
return err
}
}
2016-03-05 18:58:51 +01:00
// Update issue count of milestone
2019-10-06 23:26:19 +02:00
if err := updateMilestoneClosedNum ( e , issue . MilestoneID ) ; err != nil {
2015-08-13 10:07:11 +02:00
return err
}
2016-03-05 18:58:51 +01:00
// New action comment
2018-12-13 16:55:43 +01:00
if _ , err = createStatusComment ( e , doer , issue ) ; err != nil {
2015-08-13 10:07:11 +02:00
return err
}
return nil
}
2016-03-05 18:58:51 +01:00
// ChangeStatus changes issue status to open or closed.
2018-12-13 16:55:43 +01:00
func ( issue * Issue ) ChangeStatus ( doer * User , isClosed bool ) ( err error ) {
2015-08-13 10:07:11 +02:00
sess := x . NewSession ( )
2017-06-21 02:57:05 +02:00
defer sess . Close ( )
2015-08-13 10:07:11 +02:00
if err = sess . Begin ( ) ; err != nil {
return err
}
2018-12-13 16:55:43 +01:00
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 10:07:11 +02:00
return err
}
2016-08-14 12:32:24 +02:00
if err = sess . Commit ( ) ; err != nil {
return fmt . Errorf ( "Commit: %v" , err )
}
2018-10-27 16:45:24 +02:00
sess . Close ( )
2016-08-14 12:32:24 +02:00
2018-11-28 12:26:14 +01:00
mode , _ := AccessLevel ( issue . Poster , issue . Repo )
2016-08-14 12:32:24 +02:00
if issue . IsPull {
2018-12-13 16:55:43 +01:00
if err = issue . loadPullRequest ( sess ) ; err != nil {
return err
}
2016-08-14 12:32:24 +02:00
// 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 16:55:43 +01:00
Repository : issue . Repo . APIFormat ( mode ) ,
2016-08-14 12:32:24 +02:00
Sender : doer . APIFormat ( ) ,
}
if isClosed {
2016-11-07 16:37:32 +01:00
apiPullRequest . Action = api . HookIssueClosed
2016-08-14 12:32:24 +02:00
} else {
2016-11-29 09:25:47 +01:00
apiPullRequest . Action = api . HookIssueReOpened
2016-08-14 12:32:24 +02:00
}
2018-12-13 16:55:43 +01:00
err = PrepareWebhooks ( issue . Repo , HookEventPullRequest , apiPullRequest )
2018-05-21 04:28:29 +02:00
} else {
apiIssue := & api . IssuePayload {
Index : issue . Index ,
Issue : issue . APIFormat ( ) ,
2018-12-13 16:55:43 +01:00
Repository : issue . Repo . APIFormat ( mode ) ,
2018-05-21 04:28:29 +02:00
Sender : doer . APIFormat ( ) ,
}
if isClosed {
apiIssue . Action = api . HookIssueClosed
} else {
apiIssue . Action = api . HookIssueReOpened
}
2018-12-13 16:55:43 +01:00
err = PrepareWebhooks ( issue . Repo , HookEventIssues , apiIssue )
2016-08-14 12:32:24 +02:00
}
if err != nil {
2019-04-02 09:48:31 +02:00
log . Error ( "PrepareWebhooks [is_pull: %v, is_closed: %v]: %v" , issue . IsPull , isClosed , err )
2016-08-14 12:32:24 +02:00
} else {
2018-12-13 16:55:43 +01:00
go HookQueue . Add ( issue . Repo . ID )
2016-08-14 12:32:24 +02:00
}
return nil
}
2016-11-24 09:41:11 +01:00
// ChangeTitle changes the title of this issue, as the given user.
2019-10-11 08:44:43 +02:00
func ( issue * Issue ) ChangeTitle ( doer * User , oldTitle string ) ( err error ) {
2017-02-05 15:36:00 +01:00
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 16:55:43 +01:00
if err = issue . loadRepo ( sess ) ; err != nil {
return fmt . Errorf ( "loadRepo: %v" , err )
}
2019-10-11 08:44:43 +02:00
if _ , err = createChangeTitleComment ( sess , doer , issue . Repo , issue , oldTitle , issue . Title ) ; err != nil {
2017-02-05 15:36:00 +01:00
return fmt . Errorf ( "createChangeTitleComment: %v" , err )
}
2019-09-20 07:45:38 +02:00
if err = issue . neuterCrossReferences ( sess ) ; err != nil {
return err
}
if err = issue . addCrossReferences ( sess , doer ) ; err != nil {
return err
}
2019-10-11 08:44:43 +02:00
return sess . Commit ( )
2015-08-13 10:07:11 +02:00
}
2017-02-11 05:00:29 +01:00
// 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 09:41:11 +01:00
// ChangeContent changes issue content, as the given user.
2016-08-14 12:32:24 +02:00
func ( issue * Issue ) ChangeContent ( doer * User , content string ) ( err error ) {
oldContent := issue . Content
issue . Content = content
2018-01-03 09:34:13 +01:00
2019-09-20 07:45:38 +02:00
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 12:32:24 +02:00
return fmt . Errorf ( "UpdateIssueCols: %v" , err )
}
2019-09-20 07:45:38 +02:00
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 12:32:24 +02:00
2018-11-28 12:26:14 +01:00
mode , _ := AccessLevel ( issue . Poster , issue . Repo )
2016-08-14 12:32:24 +02:00
if issue . IsPull {
issue . PullRequest . Issue = issue
2016-11-07 16:37:32 +01:00
err = PrepareWebhooks ( issue . Repo , HookEventPullRequest , & api . PullRequestPayload {
Action : api . HookIssueEdited ,
2016-08-14 12:32:24 +02:00
Index : issue . Index ,
Changes : & api . ChangesPayload {
Body : & api . ChangesFromPayload {
From : oldContent ,
} ,
} ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2018-05-21 04:28:29 +02:00
Repository : issue . Repo . APIFormat ( mode ) ,
2016-08-14 12:32:24 +02:00
Sender : doer . APIFormat ( ) ,
} )
2018-05-21 04:28:29 +02:00
} 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 12:32:24 +02:00
}
if err != nil {
2019-04-02 09:48:31 +02:00
log . Error ( "PrepareWebhooks [is_pull: %v]: %v" , issue . IsPull , err )
2016-08-14 12:32:24 +02:00
} else {
go HookQueue . Add ( issue . RepoID )
}
return nil
}
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 16:46:21 +02: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"
}
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-16 03:40:32 +02:00
type NewIssueOptions struct {
Repo * Repository
Issue * Issue
2017-03-01 02:08:45 +01:00
LabelIDs [ ] int64
2018-05-09 18:29:04 +02:00
AssigneeIDs [ ] int64
2016-08-16 03:40:32 +02:00
Attachments [ ] string // In UUID format.
IsPull bool
}
2016-03-14 04:20:22 +01:00
2017-02-01 03:36:08 +01:00
func newIssue ( e * xorm . Session , doer * User , opts NewIssueOptions ) ( err error ) {
2016-08-16 03:40:32 +02:00
opts . Issue . Title = strings . TrimSpace ( opts . Issue . Title )
2019-05-18 04:37:49 +02:00
2016-08-16 19:19:09 +02:00
if opts . Issue . MilestoneID > 0 {
2016-08-25 01:05:56 +02:00
milestone , err := getMilestoneByRepoID ( e , opts . Issue . RepoID , opts . Issue . MilestoneID )
2016-08-16 19:19:09 +02:00
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 18:29:04 +02:00
// 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 19:19:09 +02:00
}
2018-05-09 18:29:04 +02:00
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 12:26:14 +01:00
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 18:29:04 +02:00
if err != nil {
2018-11-28 12:26:14 +01:00
return fmt . Errorf ( "canBeAssigned [user_id: %d, repo_id: %d]: %v" , assigneeID , opts . Repo . ID , err )
2018-05-09 18:29:04 +02:00
}
if ! valid {
return ErrUserDoesNotHaveAccessToRepo { UserID : assigneeID , RepoName : opts . Repo . Name }
}
2016-03-14 04:20:22 +01:00
}
}
2016-08-16 19:19:09 +02:00
// Milestone and assignee validation should happen before insert actual object.
2019-10-03 00:28:30 +02:00
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 04:17:23 +02:00
}
inserted , err := getIssueByID ( e , opts . Issue . ID )
if err != nil {
2014-05-07 18:09:30 +02:00
return err
2015-09-02 22:18:09 +02:00
}
2019-08-27 04:17:23 +02:00
// Patch Index with the value calculated by the database
opts . Issue . Index = inserted . Index
2017-02-01 03:36:08 +01:00
if opts . Issue . MilestoneID > 0 {
2019-10-06 23:26:19 +02:00
if _ , err = e . Exec ( "UPDATE `milestone` SET num_issues=num_issues+1 WHERE id=?" , opts . Issue . MilestoneID ) ; err != nil {
2017-02-01 03:36:08 +01:00
return err
}
2019-10-25 12:09:19 +02:00
if _ , err = createMilestoneComment ( e , doer , opts . Repo , opts . Issue , 0 , opts . Issue . MilestoneID ) ; err != nil {
return err
}
2017-02-01 03:36:08 +01:00
}
2018-05-09 18:29:04 +02:00
// Insert the assignees
for _ , assigneeID := range opts . AssigneeIDs {
2018-08-16 13:52:51 +02:00
err = opts . Issue . changeAssignee ( e , doer , assigneeID , true )
2018-05-09 18:29:04 +02:00
if err != nil {
2017-02-03 16:09:10 +01:00
return err
}
}
2016-08-16 03:40:32 +02:00
if opts . IsPull {
_ , err = e . Exec ( "UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?" , opts . Issue . RepoID )
2015-09-02 22:18:09 +02:00
} else {
2016-08-16 03:40:32 +02:00
_ , err = e . Exec ( "UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?" , opts . Issue . RepoID )
2015-09-02 22:18:09 +02:00
}
if err != nil {
2014-05-07 18:09:30 +02:00
return err
2014-03-27 17:48:29 +01:00
}
2014-07-22 13:50:34 +02:00
2017-03-01 02:08:45 +01: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-06 02:45:23 +01:00
// So we have to get all needed labels first.
2017-03-01 02:08:45 +01:00
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 02:45:23 +01:00
}
2015-08-14 18:42:43 +02:00
2017-01-30 13:46:45 +01:00
if err = opts . Issue . loadPoster ( e ) ; err != nil {
return err
}
2016-03-06 02:45:23 +01:00
for _ , label := range labels {
2016-08-16 03:40:32 +02:00
// Silently drop invalid labels.
if label . RepoID != opts . Repo . ID {
2016-03-14 04:20:22 +01:00
continue
}
2017-01-30 13:46:45 +01:00
if err = opts . Issue . addLabel ( e , label , opts . Issue . Poster ) ; err != nil {
2016-08-16 03:40:32 +02:00
return fmt . Errorf ( "addLabel [id: %d]: %v" , label . ID , err )
2016-03-06 02:45:23 +01:00
}
2015-08-10 10:52:08 +02:00
}
}
2016-08-16 03:40:32 +02:00
if err = newIssueUsers ( e , opts . Repo , opts . Issue ) ; err != nil {
2015-08-10 15:47:23 +02:00
return err
}
2016-08-16 03:40:32 +02:00
if len ( opts . Attachments ) > 0 {
attachments , err := getAttachmentsByUUIDs ( e , opts . Attachments )
2015-09-02 01:07:02 +02:00
if err != nil {
2016-08-16 03:40:32 +02:00
return fmt . Errorf ( "getAttachmentsByUUIDs [uuids: %v]: %v" , opts . Attachments , err )
2015-09-02 01:07:02 +02:00
}
2016-08-16 03:40:32 +02:00
for i := 0 ; i < len ( attachments ) ; i ++ {
attachments [ i ] . IssueID = opts . Issue . ID
2017-10-05 06:43:04 +02:00
if _ , err = e . ID ( attachments [ i ] . ID ) . Update ( attachments [ i ] ) ; err != nil {
2016-08-16 03:40:32 +02:00
return fmt . Errorf ( "update attachment [id: %d]: %v" , attachments [ i ] . ID , err )
}
2015-08-11 17:24:40 +02:00
}
}
2019-09-20 07:45:38 +02:00
if err = opts . Issue . loadAttributes ( e ) ; err != nil {
return err
}
return opts . Issue . addCrossReferences ( e , doer )
2015-09-02 01:07:02 +02:00
}
// NewIssue creates new issue with labels for repository.
2018-05-09 18:29:04 +02:00
func NewIssue ( repo * Repository , issue * Issue , labelIDs [ ] int64 , assigneeIDs [ ] int64 , uuids [ ] string ) ( err error ) {
2019-10-03 00:28:30 +02:00
// 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 01:07:02 +02:00
sess := x . NewSession ( )
2017-06-21 02:57:05 +02:00
defer sess . Close ( )
2015-09-02 01:07:02 +02:00
if err = sess . Begin ( ) ; err != nil {
return err
}
2017-02-01 03:36:08 +01:00
if err = newIssue ( sess , issue . Poster , NewIssueOptions {
2016-08-16 03:40:32 +02:00
Repo : repo ,
Issue : issue ,
2017-03-01 02:08:45 +01:00
LabelIDs : labelIDs ,
2016-08-16 03:40:32 +02:00
Attachments : uuids ,
2018-05-09 18:29:04 +02:00
AssigneeIDs : assigneeIDs ,
2016-08-16 03:40:32 +02:00
} ) ; err != nil {
2019-10-03 00:28:30 +02:00
if IsErrUserDoesNotHaveAccessToRepo ( err ) || IsErrNewIssueInsert ( err ) {
2018-05-09 18:29:04 +02:00
return err
}
2015-09-02 01:07:02 +02:00
return fmt . Errorf ( "newIssue: %v" , err )
}
2016-03-14 04:20:22 +01:00
if err = sess . Commit ( ) ; err != nil {
return fmt . Errorf ( "Commit: %v" , err )
}
return nil
2014-03-20 21:04:56 +01:00
}
2018-12-13 16:55:43 +01:00
// GetIssueByIndex returns raw issue without loading attributes by index in a repository.
func GetIssueByIndex ( repoID , index int64 ) ( * Issue , error ) {
2015-08-12 11:04:23 +02:00
issue := & Issue {
RepoID : repoID ,
Index : index ,
}
2014-06-21 06:51:41 +02:00
has , err := x . Get ( issue )
2014-03-22 21:00:46 +01:00
if err != nil {
return nil , err
} else if ! has {
2015-08-12 11:04:23 +02:00
return nil , ErrIssueNotExist { 0 , repoID , index }
2014-03-22 21:00:46 +01:00
}
2016-08-26 22:40:53 +02:00
return issue , nil
}
2018-12-13 16:55:43 +01:00
// GetIssueWithAttrsByIndex returns issue by index in a repository.
func GetIssueWithAttrsByIndex ( repoID , index int64 ) ( * Issue , error ) {
issue , err := GetIssueByIndex ( repoID , index )
2016-08-26 22:40:53 +02:00
if err != nil {
return nil , err
}
2016-07-21 08:26:30 +02:00
return issue , issue . LoadAttributes ( )
2014-03-22 21:00:46 +01:00
}
2016-08-14 12:32:24 +02:00
func getIssueByID ( e Engine , id int64 ) ( * Issue , error ) {
2015-08-12 11:04:23 +02:00
issue := new ( Issue )
2017-10-05 06:43:04 +02:00
has , err := e . ID ( id ) . Get ( issue )
2014-05-07 22:51:14 +02:00
if err != nil {
return nil , err
} else if ! has {
2015-08-12 11:04:23 +02:00
return nil , ErrIssueNotExist { id , 0 , 0 }
2014-05-07 22:51:14 +02:00
}
2018-12-13 16:55:43 +01:00
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-07 22:51:14 +02:00
}
2016-08-14 12:32:24 +02:00
// GetIssueByID returns an issue by given ID.
func GetIssueByID ( id int64 ) ( * Issue , error ) {
return getIssueByID ( x , id )
}
2017-03-15 02:10:35 +01:00
func getIssuesByIDs ( e Engine , issueIDs [ ] int64 ) ( [ ] * Issue , error ) {
issues := make ( [ ] * Issue , 0 , 10 )
return issues , e . In ( "id" , issueIDs ) . Find ( & issues )
}
2019-02-19 15:39:39 +01:00
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 01:54:05 +01:00
// GetIssueIDsByRepoID returns all issue ids by repo id
func GetIssueIDsByRepoID ( repoID int64 ) ( [ ] int64 , error ) {
return getIssueIDsByRepoID ( x , repoID )
}
2017-03-15 02:10:35 +01:00
// GetIssuesByIDs return issues with the given IDs.
func GetIssuesByIDs ( issueIDs [ ] int64 ) ( [ ] * Issue , error ) {
return getIssuesByIDs ( x , issueIDs )
}
2016-11-24 09:41:11 +01:00
// IssuesOptions represents options of an issue.
2015-09-02 22:18:09 +02:00
type IssuesOptions struct {
2019-10-08 19:55:16 +02:00
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-02 22:18:09 +02:00
}
2017-01-01 19:15:09 +01:00
// 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 15:39:41 +01:00
case "nearduedate" :
sess . Asc ( "issue.deadline_unix" )
case "farduedate" :
sess . Desc ( "issue.deadline_unix" )
2017-01-01 19:15:09 +01:00
default :
sess . Desc ( "issue.created_unix" )
}
}
2019-06-12 21:41:28 +02:00
func ( opts * IssuesOptions ) setupSession ( sess * xorm . Session ) {
2017-08-03 07:09:16 +02:00
if opts . Page >= 0 && opts . PageSize > 0 {
2017-01-25 03:43:02 +01:00
var start int
if opts . Page == 0 {
start = 0
} else {
2017-08-03 07:09:16 +02:00
start = ( opts . Page - 1 ) * opts . PageSize
2017-01-25 03:43:02 +01:00
}
2017-08-03 07:09:16 +02:00
sess . Limit ( opts . PageSize , start )
2016-03-14 04:20:22 +01:00
}
2017-01-25 03:43:02 +01:00
if len ( opts . IssueIDs ) > 0 {
sess . In ( "issue.id" , opts . IssueIDs )
}
2014-03-22 21:00:46 +01:00
2019-10-08 19:55:16 +02:00
if len ( opts . RepoIDs ) > 0 {
2015-09-01 12:31:47 +02:00
// In case repository IDs are provided but actually no repository has issue.
2016-11-12 13:06:33 +01:00
sess . In ( "issue.repo_id" , opts . RepoIDs )
2014-03-22 21:00:46 +01:00
}
2017-01-25 03:43:02 +01:00
switch opts . IsClosed {
case util . OptionalBoolTrue :
2017-01-25 09:28:03 +01:00
sess . And ( "issue.is_closed=?" , true )
2017-01-25 03:43:02 +01:00
case util . OptionalBoolFalse :
2017-01-25 09:28:03 +01:00
sess . And ( "issue.is_closed=?" , false )
2017-01-25 03:43:02 +01:00
}
2014-03-22 21:00:46 +01:00
2015-09-02 22:18:09 +02:00
if opts . AssigneeID > 0 {
2018-05-09 18:29:04 +02:00
sess . Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , opts . AssigneeID )
2016-12-24 11:33:21 +01:00
}
if opts . PosterID > 0 {
2016-07-17 03:18:35 +02:00
sess . And ( "issue.poster_id=?" , opts . PosterID )
2014-03-22 18:50:50 +01:00
}
2016-12-24 11:33:21 +01:00
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-02 22:18:09 +02:00
if opts . MilestoneID > 0 {
2016-07-17 03:18:35 +02:00
sess . And ( "issue.milestone_id=?" , opts . MilestoneID )
2014-03-22 18:50:50 +01:00
}
2017-01-25 03:43:02 +01:00
switch opts . IsPull {
case util . OptionalBoolTrue :
2017-01-28 17:01:07 +01:00
sess . And ( "issue.is_pull=?" , true )
2017-01-25 03:43:02 +01:00
case util . OptionalBoolFalse :
2017-01-28 17:01:07 +01:00
sess . And ( "issue.is_pull=?" , false )
2017-01-25 03:43:02 +01:00
}
2015-09-02 22:18:09 +02:00
2019-01-23 05:10:38 +01:00
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 06:22:03 +02:00
}
2015-08-10 15:47:23 +02:00
}
2017-08-03 07:09:16 +02:00
}
// 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-12 21:41:28 +02:00
opts . setupSession ( sess )
2017-08-03 07:09:16 +02: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 {
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-12 21:41:28 +02:00
opts . setupSession ( sess )
2017-08-03 07:09:16 +02:00
sortIssuesSession ( sess , opts . SortType )
2015-08-10 15:47:23 +02:00
2016-07-23 18:23:54 +02:00
issues := make ( [ ] * Issue , 0 , setting . UI . IssuePagingNum )
2016-08-26 22:40:53 +02:00
if err := sess . Find ( & issues ) ; err != nil {
return nil , fmt . Errorf ( "Find: %v" , err )
}
2019-06-23 17:22:43 +02:00
sess . Close ( )
2016-08-26 22:40:53 +02:00
2017-02-22 15:03:59 +01:00
if err := IssueList ( issues ) . LoadAttributes ( ) ; err != nil {
return nil , fmt . Errorf ( "LoadAttributes: %v" , err )
2016-08-26 22:40:53 +02:00
}
return issues , nil
2014-03-22 18:50:50 +01:00
}
2017-03-16 02:34:24 +01:00
// GetParticipantsByIssueID returns all users who are participated in comments of an issue.
func GetParticipantsByIssueID ( issueID int64 ) ( [ ] * User , error ) {
2017-08-30 06:31:33 +02:00
return getParticipantsByIssueID ( x , issueID )
}
func getParticipantsByIssueID ( e Engine , issueID int64 ) ( [ ] * User , error ) {
2017-03-16 02:34:24 +01:00
userIDs := make ( [ ] int64 , 0 , 5 )
2017-08-30 06:31:33 +02:00
if err := e . Table ( "comment" ) . Cols ( "poster_id" ) .
2017-09-16 02:18:25 +02:00
Where ( "`comment`.issue_id = ?" , issueID ) .
2019-09-07 16:53:35 +02:00
And ( "`comment`.type in (?,?,?)" , CommentTypeComment , CommentTypeCode , CommentTypeReview ) .
2017-09-16 02:18:25 +02:00
And ( "`user`.is_active = ?" , true ) .
And ( "`user`.prohibit_login = ?" , false ) .
2018-07-20 04:10:17 +02:00
Join ( "INNER" , "`user`" , "`user`.id = `comment`.poster_id" ) .
2017-03-16 02:34:24 +01:00
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 06:31:33 +02:00
return users , e . In ( "id" , userIDs ) . Find ( & users )
2017-03-16 02:34:24 +01:00
}
2019-10-10 18:45:11 +02:00
// UpdateIssueMentions updates issue-user relations for mentioned users.
func UpdateIssueMentions ( ctx DBContext , issueID int64 , mentions [ ] * User ) error {
2016-07-15 18:36:39 +02:00
if len ( mentions ) == 0 {
return nil
2015-12-21 13:24:11 +01:00
}
2019-10-10 18:45:11 +02:00
ids := make ( [ ] int64 , len ( mentions ) )
for i , u := range mentions {
ids [ i ] = u . ID
2015-12-21 13:24:11 +01:00
}
2019-09-24 07:02:49 +02:00
if err := UpdateIssueUsersByMentions ( ctx , issueID , ids ) ; err != nil {
2016-07-15 18:36:39 +02:00
return fmt . Errorf ( "UpdateIssueUsersByMentions: %v" , err )
2015-12-21 13:24:11 +01:00
}
return nil
}
2014-05-07 18:09:30 +02:00
// IssueStats represents issue statistic information.
type IssueStats struct {
OpenCount , ClosedCount int64
2017-02-14 15:15:18 +01:00
YourRepositoriesCount int64
2014-05-07 18:09:30 +02:00
AssignCount int64
CreateCount int64
MentionCount int64
}
// Filter modes.
const (
2016-11-07 17:24:59 +01:00
FilterModeAll = iota
FilterModeAssign
FilterModeCreate
FilterModeMention
2014-05-07 18:09:30 +02:00
)
2015-08-10 15:47:23 +02:00
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 09:41:11 +01:00
// IssueStatsOptions contains parameters accepted by GetIssueStats.
2015-09-02 22:18:09 +02:00
type IssueStatsOptions struct {
RepoID int64
2016-04-26 06:07:49 +02:00
Labels string
2015-09-02 22:18:09 +02:00
MilestoneID int64
AssigneeID int64
2016-12-24 11:33:21 +01:00
MentionedID int64
PosterID int64
2018-11-29 02:46:30 +01:00
IsPull util . OptionalBool
2017-01-25 03:43:02 +01:00
IssueIDs [ ] int64
2015-09-02 22:18:09 +02:00
}
2014-05-07 22:51:14 +02:00
// GetIssueStats returns issue statistic information by given conditions.
2017-01-25 03:43:02 +01:00
func GetIssueStats ( opts * IssueStatsOptions ) ( * IssueStats , error ) {
2014-05-07 18:09:30 +02:00
stats := & IssueStats { }
2015-07-24 20:52:25 +02:00
2016-04-26 06:07:49 +02:00
countSession := func ( opts * IssueStatsOptions ) * xorm . Session {
2016-11-10 16:16:32 +01:00
sess := x .
2018-11-29 02:46:30 +01:00
Where ( "issue.repo_id = ?" , opts . RepoID )
2015-08-10 15:47:23 +02:00
2017-01-25 03:43:02 +01:00
if len ( opts . IssueIDs ) > 0 {
sess . In ( "issue.id" , opts . IssueIDs )
}
2016-05-06 21:40:41 +02:00
if len ( opts . Labels ) > 0 && opts . Labels != "0" {
2016-12-22 09:58:04 +01:00
labelIDs , err := base . StringsToInt64s ( strings . Split ( opts . Labels , "," ) )
if err != nil {
log . Warn ( "Malformed Labels argument: %s" , opts . Labels )
2019-01-23 05:10:38 +01:00
} 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 06:22:03 +02:00
}
2016-04-26 06:07:49 +02:00
}
if opts . MilestoneID > 0 {
sess . And ( "issue.milestone_id = ?" , opts . MilestoneID )
}
if opts . AssigneeID > 0 {
2018-05-09 18:29:04 +02:00
sess . Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , opts . AssigneeID )
2016-04-26 06:07:49 +02:00
}
2016-12-24 11:33:21 +01:00
if opts . PosterID > 0 {
2017-06-15 05:09:03 +02:00
sess . And ( "issue.poster_id = ?" , opts . PosterID )
2016-12-24 11:33:21 +01:00
}
2015-07-24 20:52:25 +02:00
2016-12-24 11:33:21 +01:00
if opts . MentionedID > 0 {
sess . Join ( "INNER" , "issue_user" , "issue.id = issue_user.issue_id" ) .
2016-12-30 08:26:05 +01:00
And ( "issue_user.uid = ?" , opts . MentionedID ) .
And ( "issue_user.is_mentioned = ?" , true )
2016-12-24 11:33:21 +01:00
}
2016-04-26 06:07:49 +02:00
2018-11-29 02:46:30 +01: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 11:33:21 +01:00
return sess
2015-07-24 20:52:25 +02:00
}
2016-12-24 11:33:21 +01:00
2017-01-25 03:43:02 +01:00
var err error
2017-06-15 05:09:03 +02:00
stats . OpenCount , err = countSession ( opts ) .
And ( "issue.is_closed = ?" , false ) .
Count ( new ( Issue ) )
if err != nil {
return stats , err
2017-01-25 03:43:02 +01:00
}
2017-06-15 05:09:03 +02:00
stats . ClosedCount , err = countSession ( opts ) .
And ( "issue.is_closed = ?" , true ) .
Count ( new ( Issue ) )
2017-02-14 15:15:18 +01:00
return stats , err
2014-05-07 18:09:30 +02:00
}
2017-12-26 00:25:16 +01:00
// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats.
type UserIssueStatsOptions struct {
2019-10-08 19:55:16 +02:00
UserID int64
RepoID int64
UserRepoIDs [ ] int64
FilterMode int
IsPull bool
IsClosed bool
2017-12-26 00:25:16 +01:00
}
2014-05-07 22:51:14 +02:00
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
2017-12-26 00:25:16 +01:00
func GetUserIssueStats ( opts UserIssueStatsOptions ) ( * IssueStats , error ) {
var err error
2014-05-07 18:09:30 +02:00
stats := & IssueStats { }
2015-08-25 16:58:34 +02:00
2017-12-26 00:25:16 +01:00
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-02 22:18:09 +02:00
}
2017-12-26 00:25:16 +01:00
switch opts . FilterMode {
2017-02-14 15:15:18 +01:00
case FilterModeAll :
2017-12-26 00:25:16 +01:00
stats . OpenCount , err = x . Where ( cond ) . And ( "is_closed = ?" , false ) .
2019-10-08 19:55:16 +02:00
And ( builder . In ( "issue.repo_id" , opts . UserRepoIDs ) ) .
2017-02-14 15:15:18 +01:00
Count ( new ( Issue ) )
2017-12-26 00:25:16 +01:00
if err != nil {
return nil , err
}
stats . ClosedCount , err = x . Where ( cond ) . And ( "is_closed = ?" , true ) .
2019-10-08 19:55:16 +02:00
And ( builder . In ( "issue.repo_id" , opts . UserRepoIDs ) ) .
2017-02-14 15:15:18 +01:00
Count ( new ( Issue ) )
2017-12-26 00:25:16 +01:00
if err != nil {
return nil , err
}
2016-11-07 17:24:59 +01:00
case FilterModeAssign :
2019-10-03 02:03:18 +02:00
stats . OpenCount , err = x . Where ( cond ) . And ( "issue.is_closed = ?" , false ) .
2018-05-09 18:29:04 +02:00
Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , opts . UserID ) .
2017-02-14 15:15:18 +01:00
Count ( new ( Issue ) )
2017-12-26 00:25:16 +01:00
if err != nil {
return nil , err
}
2019-10-03 02:03:18 +02:00
stats . ClosedCount , err = x . Where ( cond ) . And ( "issue.is_closed = ?" , true ) .
2018-05-09 18:29:04 +02:00
Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , opts . UserID ) .
2017-02-14 15:15:18 +01:00
Count ( new ( Issue ) )
2017-12-26 00:25:16 +01:00
if err != nil {
return nil , err
}
2016-11-07 17:24:59 +01:00
case FilterModeCreate :
2017-12-26 00:25:16 +01:00
stats . OpenCount , err = x . Where ( cond ) . And ( "is_closed = ?" , false ) .
And ( "poster_id = ?" , opts . UserID ) .
2017-02-14 15:15:18 +01:00
Count ( new ( Issue ) )
2017-12-26 00:25:16 +01:00
if err != nil {
return nil , err
}
stats . ClosedCount , err = x . Where ( cond ) . And ( "is_closed = ?" , true ) .
And ( "poster_id = ?" , opts . UserID ) .
2017-02-14 15:15:18 +01:00
Count ( new ( Issue ) )
2017-12-26 00:25:16 +01:00
if err != nil {
return nil , err
}
2019-09-18 09:24:44 +02:00
case FilterModeMention :
2019-10-03 02:03:18 +02:00
stats . OpenCount , err = x . Where ( cond ) . And ( "issue.is_closed = ?" , false ) .
2019-09-18 09:24:44 +02:00
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 02:03:18 +02:00
stats . ClosedCount , err = x . Where ( cond ) . And ( "issue.is_closed = ?" , true ) .
2019-09-18 09:24:44 +02:00
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 00:25:16 +01:00
}
cond = cond . And ( builder . Eq { "issue.is_closed" : opts . IsClosed } )
stats . AssignCount , err = x . Where ( cond ) .
2018-05-09 18:29:04 +02:00
Join ( "INNER" , "issue_assignees" , "issue.id = issue_assignees.issue_id" ) .
And ( "issue_assignees.assignee_id = ?" , opts . UserID ) .
2017-12-26 00:25:16 +01:00
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 09:24:44 +02:00
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 00:25:16 +01:00
stats . YourRepositoriesCount , err = x . Where ( cond ) .
2019-10-08 19:55:16 +02:00
And ( builder . In ( "issue.repo_id" , opts . UserRepoIDs ) ) .
2017-12-26 00:25:16 +01:00
Count ( new ( Issue ) )
if err != nil {
return nil , err
2015-08-25 16:58:34 +02:00
}
2017-12-26 00:25:16 +01:00
return stats , nil
2014-05-07 18:09:30 +02:00
}
2015-08-25 16:58:34 +02:00
// GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
2015-09-02 22:18:09 +02:00
func GetRepoIssueStats ( repoID , uid int64 , filterMode int , isPull bool ) ( numOpen int64 , numClosed int64 ) {
2016-04-26 06:07:49 +02:00
countSession := func ( isClosed , isPull bool , repoID int64 ) * xorm . Session {
2016-11-10 16:16:32 +01:00
sess := x .
2017-02-09 10:59:57 +01:00
Where ( "is_closed = ?" , isClosed ) .
2016-04-26 06:22:03 +02:00
And ( "is_pull = ?" , isPull ) .
And ( "repo_id = ?" , repoID )
2015-09-02 22:18:09 +02:00
2016-04-26 06:07:49 +02:00
return sess
2015-09-02 22:18:09 +02:00
}
2016-04-26 06:07:49 +02:00
openCountSession := countSession ( false , isPull , repoID )
closedCountSession := countSession ( true , isPull , repoID )
2015-08-25 16:58:34 +02:00
switch filterMode {
2016-11-07 17:24:59 +01:00
case FilterModeAssign :
2018-05-09 18:29:04 +02:00
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 17:24:59 +01:00
case FilterModeCreate :
2016-04-26 06:07:49 +02:00
openCountSession . And ( "poster_id = ?" , uid )
closedCountSession . And ( "poster_id = ?" , uid )
2015-08-25 16:58:34 +02:00
}
2017-02-14 15:15:18 +01: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 16:58:34 +02:00
}
2019-02-21 06:01:28 +01:00
// 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 12:57:57 +02:00
func updateIssue ( e Engine , issue * Issue ) error {
2017-10-05 06:43:04 +02:00
_ , err := e . ID ( issue . ID ) . AllCols ( ) . Update ( issue )
2017-01-25 03:43:02 +01:00
if err != nil {
return err
}
return nil
2015-08-10 12:57:57 +02:00
}
2015-10-24 09:36:47 +02:00
// UpdateIssue updates all fields of given issue.
func UpdateIssue ( issue * Issue ) error {
2019-09-20 07:45:38 +02:00
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 09:36:47 +02: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.
2019-08-15 16:46:21 +02:00
func UpdateIssueDeadline ( issue * Issue , deadlineUnix timeutil . TimeStamp , doer * 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
}
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-17 23:23:58 +02:00
// 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-18 23:51:33 +02:00
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 18:45:11 +02:00
// 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
}
2019-10-14 08:10:42 +02:00
// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID
2019-10-22 10:12:10 +02:00
func UpdateIssuesMigrationsByType ( gitServiceType structs . GitServiceType , originalAuthorID string , posterID int64 ) error {
2019-10-14 08:10:42 +02:00
_ , err := x . Table ( "issue" ) .
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
}