2014-03-20 16:04:56 -04: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 13:50:50 -04:00
import (
2014-03-22 16:00:46 -04:00
"errors"
2015-08-10 14:42:50 +08:00
"fmt"
2017-03-03 22:35:42 +08:00
"path"
2017-01-30 20:46:45 +08:00
"sort"
2014-03-22 13:50:50 -04:00
"strings"
"time"
2014-05-07 16:51:14 -04:00
2016-11-11 13:11:45 +01:00
api "code.gitea.io/sdk/gitea"
2014-07-26 00:24:27 -04:00
"github.com/Unknwon/com"
2016-11-10 16:16:32 +01:00
"github.com/go-xorm/xorm"
2014-05-13 14:49:20 -04: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"
2017-01-24 21:43:02 -05:00
"code.gitea.io/gitea/modules/util"
2014-03-22 13:50:50 -04:00
)
2014-03-22 16:00:46 -04:00
var (
2016-11-22 12:24:39 +01:00
errMissingIssueNumber = errors . New ( "No issue number specified" )
2014-03-22 16:00:46 -04:00
)
2014-03-22 13:50:50 -04:00
// Issue represents an issue or pull request of repository.
2014-03-20 16:04:56 -04:00
type Issue struct {
2016-08-14 03:32:24 -07:00
ID int64 ` xorm:"pk autoincr" `
RepoID int64 ` xorm:"INDEX UNIQUE(repo_index)" `
2014-03-29 10:24:42 -04:00
Repo * Repository ` xorm:"-" `
2016-08-16 10:19:09 -07:00
Index int64 ` xorm:"UNIQUE(repo_index)" ` // Index in one repository.
2017-01-06 13:14:33 -02:00
PosterID int64 ` xorm:"INDEX" `
Poster * User ` xorm:"-" `
Title string ` xorm:"name" `
Content string ` xorm:"TEXT" `
RenderedContent string ` xorm:"-" `
Labels [ ] * Label ` xorm:"-" `
MilestoneID int64 ` xorm:"INDEX" `
Milestone * Milestone ` xorm:"-" `
2016-08-16 10:19:09 -07:00
Priority int
2017-01-06 13:14:33 -02:00
AssigneeID int64 ` xorm:"INDEX" `
Assignee * User ` xorm:"-" `
IsClosed bool ` xorm:"INDEX" `
2016-08-16 10:19:09 -07:00
IsRead bool ` xorm:"-" `
2017-01-06 13:14:33 -02:00
IsPull bool ` xorm:"INDEX" ` // Indicates whether is a pull request or not.
2016-08-16 10:19:09 -07:00
PullRequest * PullRequest ` xorm:"-" `
2014-03-29 10:24:42 -04:00
NumComments int
2016-03-09 19:53:30 -05:00
Deadline time . Time ` xorm:"-" `
2017-01-06 13:14:33 -02:00
DeadlineUnix int64 ` xorm:"INDEX" `
2016-03-09 19:53:30 -05:00
Created time . Time ` xorm:"-" `
2017-01-06 13:14:33 -02:00
CreatedUnix int64 ` xorm:"INDEX" `
2016-03-09 19:53:30 -05:00
Updated time . Time ` xorm:"-" `
2017-01-06 13:14:33 -02:00
UpdatedUnix int64 ` xorm:"INDEX" `
2015-08-12 17:04:23 +08:00
Attachments [ ] * Attachment ` xorm:"-" `
2015-08-13 16:07:11 +08:00
Comments [ ] * Comment ` xorm:"-" `
2015-08-12 17:04:23 +08:00
}
2016-11-24 09:41:11 +01:00
// BeforeInsert is invoked from XORM before inserting an object of this type.
2016-08-24 16:05:56 -07:00
func ( issue * Issue ) BeforeInsert ( ) {
issue . CreatedUnix = time . Now ( ) . Unix ( )
issue . UpdatedUnix = issue . CreatedUnix
2016-03-09 19:53:30 -05:00
}
2016-11-24 09:41:11 +01:00
// BeforeUpdate is invoked from XORM before updating this object.
2016-08-24 16:05:56 -07:00
func ( issue * Issue ) BeforeUpdate ( ) {
issue . UpdatedUnix = time . Now ( ) . Unix ( )
issue . DeadlineUnix = issue . Deadline . Unix ( )
2016-03-09 19:53:30 -05:00
}
2016-11-24 09:41:11 +01:00
// AfterSet is invoked from XORM after setting the value of a field of
// this object.
2016-08-24 16:05:56 -07:00
func ( issue * Issue ) AfterSet ( colName string , _ xorm . Cell ) {
2015-08-05 20:23:08 +08:00
switch colName {
2016-08-26 13:40:53 -07:00
case "deadline_unix" :
issue . Deadline = time . Unix ( issue . DeadlineUnix , 0 ) . Local ( )
case "created_unix" :
issue . Created = time . Unix ( issue . CreatedUnix , 0 ) . Local ( )
case "updated_unix" :
issue . Updated = time . Unix ( issue . UpdatedUnix , 0 ) . Local ( )
}
}
2015-08-13 16:07:11 +08:00
2016-12-17 19:49:17 +08:00
func ( issue * Issue ) loadRepo ( e Engine ) ( err error ) {
2016-08-26 13:40:53 -07:00
if issue . Repo == nil {
issue . Repo , err = getRepositoryByID ( e , issue . RepoID )
2016-03-13 23:20:22 -04:00
if err != nil {
2016-08-26 13:40:53 -07:00
return fmt . Errorf ( "getRepositoryByID [%d]: %v" , issue . RepoID , err )
2016-03-13 23:20:22 -04:00
}
2016-08-26 13:40:53 -07:00
}
2016-12-17 19:49:17 +08:00
return nil
}
2017-01-28 14:01:07 -02:00
// GetPullRequest returns the issue pull request
func ( issue * Issue ) GetPullRequest ( ) ( pr * PullRequest , err error ) {
if ! issue . IsPull {
return nil , fmt . Errorf ( "Issue is not a pull request" )
}
pr , err = getPullRequestByIssueID ( x , issue . ID )
return
}
2017-01-30 20:46:45 +08: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 19:49:17 +08:00
}
2017-01-30 20:46:45 +08:00
return nil
}
2016-03-13 23:20:22 -04:00
2017-01-30 20:46:45 +08:00
func ( issue * Issue ) loadPoster ( e Engine ) ( err error ) {
2016-08-26 13:40:53 -07:00
if issue . Poster == nil {
issue . Poster , err = getUserByID ( e , issue . PosterID )
2016-03-13 23:20:22 -04:00
if err != nil {
2016-09-20 17:54:47 +08:00
issue . PosterID = - 1
issue . Poster = NewGhostUser ( )
2016-11-05 02:47:54 +08:00
if ! IsErrUserNotExist ( err ) {
2016-08-26 13:40:53 -07:00
return fmt . Errorf ( "getUserByID.(poster) [%d]: %v" , issue . PosterID , err )
2016-03-13 23:20:22 -04:00
}
2016-11-09 13:07:01 +08:00
err = nil
2016-03-13 23:20:22 -04:00
return
}
2016-08-26 13:40:53 -07:00
}
2017-01-30 20:46:45 +08:00
return
}
2016-03-13 23:20:22 -04:00
2017-01-30 20:46:45 +08: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 13:40:53 -07:00
}
2015-08-10 21:47:23 +08:00
2016-08-26 13:40:53 -07:00
if issue . Milestone == nil && issue . MilestoneID > 0 {
issue . Milestone , err = getMilestoneByRepoID ( e , issue . RepoID , issue . MilestoneID )
2015-08-10 21:47:23 +08:00
if err != nil {
2016-08-26 13:40:53 -07:00
return fmt . Errorf ( "getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v" , issue . RepoID , issue . MilestoneID , err )
2015-08-10 21:47:23 +08:00
}
2015-08-05 20:23:08 +08:00
}
2016-08-26 13:40:53 -07:00
if issue . Assignee == nil && issue . AssigneeID > 0 {
issue . Assignee , err = getUserByID ( e , issue . AssigneeID )
2016-08-14 03:32:24 -07:00
if err != nil {
2016-08-26 13:40:53 -07:00
return fmt . Errorf ( "getUserByID.(assignee) [%d]: %v" , issue . AssigneeID , err )
2016-08-14 03:32:24 -07:00
}
}
if issue . IsPull && issue . PullRequest == nil {
// It is possible pull request is not yet created.
issue . PullRequest , err = getPullRequestByIssueID ( e , issue . ID )
if err != nil && ! IsErrPullRequestNotExist ( err ) {
return fmt . Errorf ( "getPullRequestByIssueID [%d]: %v" , issue . ID , err )
}
}
2016-08-26 13:40:53 -07:00
if issue . Attachments == nil {
issue . Attachments , err = getAttachmentsByIssueID ( e , issue . ID )
if err != nil {
return fmt . Errorf ( "getAttachmentsByIssueID [%d]: %v" , issue . ID , err )
}
}
if issue . Comments == nil {
issue . Comments , err = getCommentsByIssueID ( e , issue . ID )
if err != nil {
return fmt . Errorf ( "getCommentsByIssueID [%d]: %v" , issue . ID , err )
}
}
2016-08-14 03:32:24 -07:00
return nil
}
2016-11-24 09:41:11 +01:00
// LoadAttributes loads the attribute of this issue.
2016-08-14 03:32:24 -07:00
func ( issue * Issue ) LoadAttributes ( ) error {
return issue . loadAttributes ( x )
2015-08-20 00:12:43 +08:00
}
2017-02-03 02:22:39 -05:00
// GetIsRead load the `IsRead` field of the issue
func ( issue * Issue ) GetIsRead ( userID int64 ) error {
issueUser := & IssueUser { IssueID : issue . ID , UID : userID }
if has , err := x . Get ( issueUser ) ; err != nil {
return err
} else if ! has {
2017-02-08 22:47:24 -05:00
issue . IsRead = false
return nil
2017-02-03 02:22:39 -05:00
}
issue . IsRead = issueUser . IsRead
return nil
}
2017-03-03 22:35:42 +08:00
// APIURL returns the absolute APIURL to this issue.
func ( issue * Issue ) APIURL ( ) string {
return issue . Repo . APIURL ( ) + "/" + path . Join ( "issues" , fmt . Sprint ( issue . ID ) )
}
2016-11-24 09:41:11 +01:00
// HTMLURL returns the absolute URL to this issue.
2016-08-16 10:19:09 -07:00
func ( issue * Issue ) HTMLURL ( ) string {
var path string
if issue . IsPull {
path = "pulls"
} else {
path = "issues"
}
return fmt . Sprintf ( "%s/%s/%d" , issue . Repo . HTMLURL ( ) , path , issue . Index )
}
2016-12-02 12:10:39 +01:00
// DiffURL returns the absolute URL to this diff
func ( issue * Issue ) DiffURL ( ) string {
if issue . IsPull {
return fmt . Sprintf ( "%s/pulls/%d.diff" , issue . Repo . HTMLURL ( ) , issue . Index )
}
return ""
}
// PatchURL returns the absolute URL to this patch
func ( issue * Issue ) PatchURL ( ) string {
if issue . IsPull {
return fmt . Sprintf ( "%s/pulls/%d.patch" , issue . Repo . HTMLURL ( ) , issue . Index )
}
return ""
}
2016-03-13 23:20:22 -04:00
// State returns string representation of issue status.
2016-11-22 12:24:39 +01:00
func ( issue * Issue ) State ( ) api . StateType {
if issue . IsClosed {
2016-11-29 09:25:47 +01:00
return api . StateClosed
2016-03-13 23:20:22 -04:00
}
2016-11-29 09:25:47 +01:00
return api . StateOpen
2016-08-14 03:32:24 -07:00
}
2016-11-22 12:24:39 +01:00
// APIFormat assumes some fields assigned with values:
2016-08-14 03:32:24 -07:00
// Required - Poster, Labels,
// Optional - Milestone, Assignee, PullRequest
func ( issue * Issue ) APIFormat ( ) * api . Issue {
apiLabels := make ( [ ] * api . Label , len ( issue . Labels ) )
for i := range issue . Labels {
apiLabels [ i ] = issue . Labels [ i ] . APIFormat ( )
}
apiIssue := & api . Issue {
ID : issue . ID ,
2017-03-03 22:35:42 +08:00
URL : issue . APIURL ( ) ,
2016-08-14 03:32:24 -07:00
Index : issue . Index ,
2016-08-16 10:19:09 -07:00
Poster : issue . Poster . APIFormat ( ) ,
2016-08-14 03:32:24 -07:00
Title : issue . Title ,
Body : issue . Content ,
Labels : apiLabels ,
2016-08-16 10:19:09 -07:00
State : issue . State ( ) ,
2016-08-14 03:32:24 -07:00
Comments : issue . NumComments ,
Created : issue . Created ,
Updated : issue . Updated ,
}
if issue . Milestone != nil {
apiIssue . Milestone = issue . Milestone . APIFormat ( )
}
if issue . Assignee != nil {
apiIssue . Assignee = issue . Assignee . APIFormat ( )
}
if issue . IsPull {
apiIssue . PullRequest = & api . PullRequestMeta {
HasMerged : issue . PullRequest . HasMerged ,
}
if issue . PullRequest . HasMerged {
apiIssue . PullRequest . Merged = & issue . PullRequest . Merged
}
}
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-13 23:20:22 -04:00
}
2015-08-13 16:07:11 +08:00
// IsPoster returns true if given user by ID is the poster.
2016-11-22 12:24:39 +01:00
func ( issue * Issue ) IsPoster ( uid int64 ) bool {
return issue . PosterID == uid
2015-08-13 16:07:11 +08: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 14:42:50 +08: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 14:42:50 +08:00
}
2016-08-14 03:32:24 -07:00
func ( issue * Issue ) sendLabelUpdatedWebhook ( doer * User ) {
var err error
if issue . IsPull {
2016-08-24 21:01:30 +02:00
err = issue . PullRequest . LoadIssue ( )
if err != nil {
log . Error ( 4 , "LoadIssue: %v" , err )
return
}
2016-11-07 16:37:32 +01:00
err = PrepareWebhooks ( issue . Repo , HookEventPullRequest , & api . PullRequestPayload {
Action : api . HookIssueLabelUpdated ,
2016-08-14 03:32:24 -07:00
Index : issue . Index ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2016-12-05 18:48:51 -05:00
Repository : issue . Repo . APIFormat ( AccessModeNone ) ,
2016-08-14 03:32:24 -07:00
Sender : doer . APIFormat ( ) ,
} )
}
if err != nil {
log . Error ( 4 , "PrepareWebhooks [is_pull: %v]: %v" , issue . IsPull , err )
} else {
go HookQueue . Add ( issue . RepoID )
}
}
2017-01-30 20:46:45 +08:00
func ( issue * Issue ) addLabel ( e * xorm . Session , label * Label , doer * User ) error {
return newIssueLabel ( e , issue , label , doer )
2015-08-10 14:42:50 +08:00
}
2016-08-03 11:51:22 -07:00
// AddLabel adds a new label to the issue.
2016-08-14 03:32:24 -07:00
func ( issue * Issue ) AddLabel ( doer * User , label * Label ) error {
2017-01-30 20:46:45 +08:00
if err := NewIssueLabel ( issue , label , doer ) ; err != nil {
2015-08-15 00:42:43 +08:00
return err
}
2016-08-14 03:32:24 -07:00
issue . sendLabelUpdatedWebhook ( doer )
return nil
2015-08-10 14:42:50 +08:00
}
2017-01-30 20:46:45 +08:00
func ( issue * Issue ) addLabels ( e * xorm . Session , labels [ ] * Label , doer * User ) error {
return newIssueLabels ( e , issue , labels , doer )
2016-08-03 11:51:22 -07:00
}
// AddLabels adds a list of new labels to the issue.
2016-08-14 03:32:24 -07:00
func ( issue * Issue ) AddLabels ( doer * User , labels [ ] * Label ) error {
2017-01-30 20:46:45 +08:00
if err := NewIssueLabels ( issue , labels , doer ) ; err != nil {
2016-08-14 03:32:24 -07:00
return err
}
issue . sendLabelUpdatedWebhook ( doer )
return nil
2016-08-03 11:51:22 -07:00
}
func ( issue * Issue ) getLabels ( e Engine ) ( err error ) {
if len ( issue . Labels ) > 0 {
2014-05-24 02:31:58 -04:00
return nil
}
2016-08-03 11:51:22 -07:00
issue . Labels , err = getLabelsByIssueID ( e , issue . ID )
2015-08-10 14:42:50 +08:00
if err != nil {
return fmt . Errorf ( "getLabelsByIssueID: %v" , err )
2014-05-24 02:31:58 -04:00
}
return nil
}
2017-01-30 20:46:45 +08:00
func ( issue * Issue ) removeLabel ( e * xorm . Session , doer * User , label * Label ) error {
2017-01-31 20:31:35 -05:00
return deleteIssueLabel ( e , issue , label , doer )
2015-08-10 14:42:50 +08:00
}
// RemoveLabel removes a label from issue by given ID.
2016-08-14 03:32:24 -07:00
func ( issue * Issue ) RemoveLabel ( doer * User , label * Label ) error {
2016-12-17 19:49:17 +08:00
if err := issue . loadRepo ( x ) ; err != nil {
return err
}
2017-03-14 20:51:46 -04:00
if has , err := HasAccess ( doer . ID , issue . Repo , AccessModeWrite ) ; err != nil {
2016-12-17 19:49:17 +08:00
return err
} else if ! has {
return ErrLabelNotExist { }
}
2017-01-31 20:31:35 -05:00
if err := DeleteIssueLabel ( issue , label , doer ) ; err != nil {
2016-08-14 03:32:24 -07:00
return err
}
issue . sendLabelUpdatedWebhook ( doer )
return nil
2016-08-03 11:51:22 -07:00
}
2017-01-30 20:46:45 +08:00
func ( issue * Issue ) clearLabels ( e * xorm . Session , doer * User ) ( err error ) {
2016-08-03 11:51:22 -07:00
if err = issue . getLabels ( e ) ; err != nil {
return fmt . Errorf ( "getLabels: %v" , err )
}
for i := range issue . Labels {
2017-01-30 20:46:45 +08:00
if err = issue . removeLabel ( e , doer , issue . Labels [ i ] ) ; err != nil {
2016-08-03 11:51:22 -07: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 03:32:24 -07:00
func ( issue * Issue ) ClearLabels ( doer * User ) ( err error ) {
2015-08-15 00:42:43 +08:00
sess := x . NewSession ( )
defer sessionRelease ( sess )
if err = sess . Begin ( ) ; err != nil {
return err
}
2016-12-17 19:49:17 +08:00
if err := issue . loadRepo ( sess ) ; err != nil {
return err
}
2017-03-14 20:51:46 -04:00
if has , err := hasAccess ( sess , doer . ID , issue . Repo , AccessModeWrite ) ; err != nil {
2016-12-17 19:49:17 +08:00
return err
} else if ! has {
return ErrLabelNotExist { }
}
2017-01-30 20:46:45 +08:00
if err = issue . clearLabels ( sess , doer ) ; err != nil {
2015-08-15 00:42:43 +08:00
return err
}
2016-08-14 03:32:24 -07:00
if err = sess . Commit ( ) ; err != nil {
return fmt . Errorf ( "Commit: %v" , err )
}
if issue . IsPull {
2016-08-24 21:01:30 +02:00
err = issue . PullRequest . LoadIssue ( )
if err != nil {
log . Error ( 4 , "LoadIssue: %v" , err )
return
}
2016-11-07 16:37:32 +01:00
err = PrepareWebhooks ( issue . Repo , HookEventPullRequest , & api . PullRequestPayload {
Action : api . HookIssueLabelCleared ,
2016-08-14 03:32:24 -07:00
Index : issue . Index ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2016-12-05 18:48:51 -05:00
Repository : issue . Repo . APIFormat ( AccessModeNone ) ,
2016-08-14 03:32:24 -07:00
Sender : doer . APIFormat ( ) ,
} )
}
if err != nil {
log . Error ( 4 , "PrepareWebhooks [is_pull: %v]: %v" , issue . IsPull , err )
} else {
go HookQueue . Add ( issue . RepoID )
}
return nil
2015-08-15 00:42:43 +08:00
}
2017-01-30 20:46:45 +08:00
type labelSorter [ ] * Label
func ( ts labelSorter ) Len ( ) int {
return len ( [ ] * Label ( ts ) )
}
func ( ts labelSorter ) Less ( i , j int ) bool {
return [ ] * Label ( ts ) [ i ] . ID < [ ] * Label ( ts ) [ j ] . ID
}
func ( ts labelSorter ) Swap ( i , j int ) {
[ ] * Label ( ts ) [ i ] , [ ] * Label ( ts ) [ j ] = [ ] * Label ( ts ) [ j ] , [ ] * Label ( ts ) [ i ]
}
2016-08-03 11:51:22 -07: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 20:46:45 +08:00
func ( issue * Issue ) ReplaceLabels ( labels [ ] * Label , doer * User ) ( err error ) {
2015-08-15 00:42:43 +08:00
sess := x . NewSession ( )
defer sessionRelease ( sess )
if err = sess . Begin ( ) ; err != nil {
return err
}
2017-01-30 20:46:45 +08: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-27 20:35:55 -05:00
addIndex , removeIndex := 0 , 0
for addIndex < len ( labels ) && removeIndex < len ( issue . Labels ) {
addLabel := labels [ addIndex ]
removeLabel := issue . Labels [ removeIndex ]
if addLabel . ID == removeLabel . ID {
addIndex ++
removeIndex ++
} else if addLabel . ID < removeLabel . ID {
toAdd = append ( toAdd , addLabel )
addIndex ++
} else {
toRemove = append ( toRemove , removeLabel )
removeIndex ++
2017-01-30 20:46:45 +08:00
}
}
2017-02-27 20:35:55 -05:00
toAdd = append ( toAdd , labels [ addIndex : ] ... )
toRemove = append ( toRemove , issue . Labels [ removeIndex : ] ... )
2017-01-30 20:46:45 +08:00
if len ( toAdd ) > 0 {
if err = issue . addLabels ( sess , toAdd , doer ) ; err != nil {
return fmt . Errorf ( "addLabels: %v" , err )
}
}
2017-02-27 20:35:55 -05:00
for _ , l := range toRemove {
if err = issue . removeLabel ( sess , doer , l ) ; err != nil {
return fmt . Errorf ( "removeLabel: %v" , err )
2017-01-30 20:46:45 +08:00
}
2015-08-15 00:42:43 +08:00
}
return sess . Commit ( )
2015-08-10 14:42:50 +08:00
}
2016-11-24 09:41:11 +01:00
// GetAssignee sets the Assignee attribute of this issue.
2016-11-22 12:24:39 +01:00
func ( issue * Issue ) GetAssignee ( ) ( err error ) {
if issue . AssigneeID == 0 || issue . Assignee != nil {
2014-05-08 12:24:11 -04:00
return nil
}
2015-08-05 20:26:00 +08:00
2016-11-22 12:24:39 +01:00
issue . Assignee , err = GetUserByID ( issue . AssigneeID )
2015-08-05 11:14:17 +08:00
if IsErrUserNotExist ( err ) {
2014-05-24 02:31:58 -04:00
return nil
}
2014-05-08 12:24:11 -04:00
return err
}
2015-08-12 18:44:09 +08:00
// ReadBy sets issue to be read by given user.
2016-12-30 14:44:54 -02:00
func ( issue * Issue ) ReadBy ( userID int64 ) error {
if err := UpdateIssueUserByRead ( userID , issue . ID ) ; err != nil {
return err
}
2017-01-12 02:27:09 -02:00
if err := setNotificationStatusReadIfUnread ( x , userID , issue . ID ) ; err != nil {
2016-12-30 14:44:54 -02:00
return err
}
return nil
2014-07-23 21:15:47 +02:00
}
2016-08-14 03:32:24 -07:00
func updateIssueCols ( e Engine , issue * Issue , cols ... string ) error {
2017-01-24 21:43:02 -05:00
if _ , err := e . Id ( issue . ID ) . Cols ( cols ... ) . Update ( issue ) ; err != nil {
return err
}
UpdateIssueIndexer ( issue )
return nil
2016-08-14 03:32:24 -07:00
}
// UpdateIssueCols only updates values of specific columns for given issue.
func UpdateIssueCols ( issue * Issue , cols ... string ) error {
return updateIssueCols ( x , issue , cols ... )
}
2016-11-22 12:24:39 +01:00
func ( issue * Issue ) changeStatus ( e * xorm . Session , doer * User , repo * Repository , isClosed bool ) ( err error ) {
2016-03-05 12:58:51 -05:00
// Nothing should be performed if current status is same as target status
2016-11-22 12:24:39 +01:00
if issue . IsClosed == isClosed {
2015-08-13 16:07:11 +08:00
return nil
}
2016-11-22 12:24:39 +01:00
issue . IsClosed = isClosed
2015-08-13 16:07:11 +08:00
2016-11-22 12:24:39 +01:00
if err = updateIssueCols ( e , issue , "is_closed" ) ; err != nil {
2015-08-13 16:07:11 +08:00
return err
}
2016-03-05 12:58:51 -05:00
// Update issue count of labels
2016-11-22 12:24:39 +01:00
if err = issue . getLabels ( e ) ; err != nil {
2015-08-13 16:07:11 +08:00
return err
}
2016-11-22 12:24:39 +01:00
for idx := range issue . Labels {
if issue . IsClosed {
issue . Labels [ idx ] . NumClosedIssues ++
2015-08-13 16:07:11 +08:00
} else {
2016-11-22 12:24:39 +01:00
issue . Labels [ idx ] . NumClosedIssues --
2015-08-13 16:07:11 +08:00
}
2016-11-22 12:24:39 +01:00
if err = updateLabel ( e , issue . Labels [ idx ] ) ; err != nil {
2015-08-13 16:07:11 +08:00
return err
}
}
2016-03-05 12:58:51 -05:00
// Update issue count of milestone
2016-11-22 12:24:39 +01:00
if err = changeMilestoneIssueStats ( e , issue ) ; err != nil {
2015-08-13 16:07:11 +08:00
return err
}
2016-03-05 12:58:51 -05:00
// New action comment
2016-11-22 12:24:39 +01:00
if _ , err = createStatusComment ( e , doer , repo , issue ) ; err != nil {
2015-08-13 16:07:11 +08:00
return err
}
return nil
}
2016-03-05 12:58:51 -05:00
// ChangeStatus changes issue status to open or closed.
2016-08-14 03:32:24 -07:00
func ( issue * Issue ) ChangeStatus ( doer * User , repo * Repository , isClosed bool ) ( err error ) {
2015-08-13 16:07:11 +08:00
sess := x . NewSession ( )
defer sessionRelease ( sess )
if err = sess . Begin ( ) ; err != nil {
return err
}
2016-08-14 03:32:24 -07:00
if err = issue . changeStatus ( sess , doer , repo , isClosed ) ; err != nil {
2015-08-13 16:07:11 +08:00
return err
}
2016-08-14 03:32:24 -07:00
if err = sess . Commit ( ) ; err != nil {
return fmt . Errorf ( "Commit: %v" , err )
}
if issue . IsPull {
// Merge pull request calls issue.changeStatus so we need to handle separately.
issue . PullRequest . Issue = issue
apiPullRequest := & api . PullRequestPayload {
Index : issue . Index ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2016-12-05 18:48:51 -05:00
Repository : repo . APIFormat ( AccessModeNone ) ,
2016-08-14 03:32:24 -07:00
Sender : doer . APIFormat ( ) ,
}
if isClosed {
2016-11-07 16:37:32 +01:00
apiPullRequest . Action = api . HookIssueClosed
2016-08-14 03:32:24 -07:00
} else {
2016-11-29 09:25:47 +01:00
apiPullRequest . Action = api . HookIssueReOpened
2016-08-14 03:32:24 -07:00
}
2016-11-07 16:37:32 +01:00
err = PrepareWebhooks ( repo , HookEventPullRequest , apiPullRequest )
2016-08-14 03:32:24 -07:00
}
if err != nil {
log . Error ( 4 , "PrepareWebhooks [is_pull: %v, is_closed: %v]: %v" , issue . IsPull , isClosed , err )
} else {
go HookQueue . Add ( repo . ID )
}
return nil
}
2016-11-24 09:41:11 +01:00
// ChangeTitle changes the title of this issue, as the given user.
2016-08-14 03:32:24 -07:00
func ( issue * Issue ) ChangeTitle ( doer * User , title string ) ( err error ) {
oldTitle := issue . Title
issue . Title = title
2017-02-05 22:36:00 +08: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 )
}
if _ , err = createChangeTitleComment ( sess , doer , issue . Repo , issue , oldTitle , title ) ; err != nil {
return fmt . Errorf ( "createChangeTitleComment: %v" , err )
}
if err = sess . Commit ( ) ; err != nil {
return err
2016-08-14 03:32:24 -07: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 03:32:24 -07:00
Index : issue . Index ,
Changes : & api . ChangesPayload {
Title : & api . ChangesFromPayload {
From : oldTitle ,
} ,
} ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2016-12-05 18:48:51 -05:00
Repository : issue . Repo . APIFormat ( AccessModeNone ) ,
2016-08-14 03:32:24 -07:00
Sender : doer . APIFormat ( ) ,
} )
}
if err != nil {
log . Error ( 4 , "PrepareWebhooks [is_pull: %v]: %v" , issue . IsPull , err )
} else {
go HookQueue . Add ( issue . RepoID )
}
return nil
2015-08-13 16:07:11 +08:00
}
2017-02-11 12:00:29 +08: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 03:32:24 -07:00
func ( issue * Issue ) ChangeContent ( doer * User , content string ) ( err error ) {
oldContent := issue . Content
issue . Content = content
if err = UpdateIssueCols ( issue , "content" ) ; err != nil {
return fmt . Errorf ( "UpdateIssueCols: %v" , err )
}
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 03:32:24 -07:00
Index : issue . Index ,
Changes : & api . ChangesPayload {
Body : & api . ChangesFromPayload {
From : oldContent ,
} ,
} ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2016-12-05 18:48:51 -05:00
Repository : issue . Repo . APIFormat ( AccessModeNone ) ,
2016-08-14 03:32:24 -07:00
Sender : doer . APIFormat ( ) ,
} )
}
if err != nil {
log . Error ( 4 , "PrepareWebhooks [is_pull: %v]: %v" , issue . IsPull , err )
} else {
go HookQueue . Add ( issue . RepoID )
}
return nil
}
2017-03-14 20:52:01 -04:00
// ChangeAssignee changes the Assignee field of this issue.
2016-08-14 03:32:24 -07:00
func ( issue * Issue ) ChangeAssignee ( doer * User , assigneeID int64 ) ( err error ) {
2017-02-03 23:09:10 +08:00
var oldAssigneeID = issue . AssigneeID
2016-08-14 03:32:24 -07:00
issue . AssigneeID = assigneeID
if err = UpdateIssueUserByAssignee ( issue ) ; err != nil {
return fmt . Errorf ( "UpdateIssueUserByAssignee: %v" , err )
}
2017-02-03 23:09:10 +08:00
sess := x . NewSession ( )
defer sess . Close ( )
if err = issue . loadRepo ( sess ) ; err != nil {
return fmt . Errorf ( "loadRepo: %v" , err )
}
if _ , err = createAssigneeComment ( sess , doer , issue . Repo , issue , oldAssigneeID , assigneeID ) ; err != nil {
return fmt . Errorf ( "createAssigneeComment: %v" , err )
}
2016-08-14 03:32:24 -07:00
issue . Assignee , err = GetUserByID ( issue . AssigneeID )
if err != nil && ! IsErrUserNotExist ( err ) {
log . Error ( 4 , "GetUserByID [assignee_id: %v]: %v" , issue . AssigneeID , err )
2015-10-25 03:10:22 -04:00
return nil
}
2016-08-14 03:32:24 -07:00
// Error not nil here means user does not exist, which is remove assignee.
isRemoveAssignee := err != nil
if issue . IsPull {
issue . PullRequest . Issue = issue
apiPullRequest := & api . PullRequestPayload {
Index : issue . Index ,
PullRequest : issue . PullRequest . APIFormat ( ) ,
2016-12-05 18:48:51 -05:00
Repository : issue . Repo . APIFormat ( AccessModeNone ) ,
2016-08-14 03:32:24 -07:00
Sender : doer . APIFormat ( ) ,
}
if isRemoveAssignee {
2016-11-07 16:37:32 +01:00
apiPullRequest . Action = api . HookIssueUnassigned
2016-08-14 03:32:24 -07:00
} else {
2016-11-07 16:37:32 +01:00
apiPullRequest . Action = api . HookIssueAssigned
2016-08-14 03:32:24 -07:00
}
2017-02-11 12:11:07 -05:00
if err := PrepareWebhooks ( issue . Repo , HookEventPullRequest , apiPullRequest ) ; err != nil {
log . Error ( 4 , "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v" , issue . IsPull , isRemoveAssignee , err )
return nil
}
2016-08-14 03:32:24 -07:00
}
2017-02-11 12:11:07 -05:00
go HookQueue . Add ( issue . RepoID )
2016-08-14 03:32:24 -07:00
return nil
2015-10-25 03:10:22 -04:00
}
2016-11-24 09:41:11 +01:00
// NewIssueOptions represents the options of a new issue.
2016-08-15 18:40:32 -07:00
type NewIssueOptions struct {
Repo * Repository
Issue * Issue
2017-02-28 20:08:45 -05:00
LabelIDs [ ] int64
2016-08-15 18:40:32 -07:00
Attachments [ ] string // In UUID format.
IsPull bool
}
2016-03-13 23:20:22 -04:00
2017-02-01 10:36:08 +08:00
func newIssue ( e * xorm . Session , doer * User , opts NewIssueOptions ) ( err error ) {
2016-08-15 18:40:32 -07:00
opts . Issue . Title = strings . TrimSpace ( opts . Issue . Title )
opts . Issue . Index = opts . Repo . NextIssueIndex ( )
2016-08-16 10:19:09 -07:00
if opts . Issue . MilestoneID > 0 {
2016-08-24 16:05:56 -07:00
milestone , err := getMilestoneByRepoID ( e , opts . Issue . RepoID , opts . Issue . MilestoneID )
2016-08-16 10:19:09 -07: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
}
}
2017-03-14 20:51:46 -04:00
if assigneeID := opts . Issue . AssigneeID ; assigneeID > 0 {
valid , err := hasAccess ( e , assigneeID , opts . Repo , AccessModeWrite )
if err != nil {
return fmt . Errorf ( "hasAccess [user_id: %d, repo_id: %d]: %v" , assigneeID , opts . Repo . ID , err )
2016-08-16 10:19:09 -07:00
}
2017-03-14 20:51:46 -04:00
if ! valid {
opts . Issue . AssigneeID = 0
opts . Issue . Assignee = nil
2016-03-13 23:20:22 -04:00
}
}
2016-08-16 10:19:09 -07:00
// Milestone and assignee validation should happen before insert actual object.
2016-08-15 18:40:32 -07:00
if _ , err = e . Insert ( opts . Issue ) ; err != nil {
2014-05-07 12:09:30 -04:00
return err
2015-09-02 16:18:09 -04:00
}
2017-02-01 10:36:08 +08:00
if opts . Issue . MilestoneID > 0 {
if err = changeMilestoneAssign ( e , doer , opts . Issue , - 1 ) ; err != nil {
return err
}
}
2017-02-03 23:09:10 +08:00
if opts . Issue . AssigneeID > 0 {
if err = opts . Issue . loadRepo ( e ) ; err != nil {
return err
}
if _ , err = createAssigneeComment ( e , doer , opts . Issue . Repo , opts . Issue , - 1 , opts . Issue . AssigneeID ) ; err != nil {
return err
}
}
2016-08-15 18:40:32 -07:00
if opts . IsPull {
_ , err = e . Exec ( "UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?" , opts . Issue . RepoID )
2015-09-02 16:18:09 -04:00
} else {
2016-08-15 18:40:32 -07:00
_ , err = e . Exec ( "UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?" , opts . Issue . RepoID )
2015-09-02 16:18:09 -04:00
}
if err != nil {
2014-05-07 12:09:30 -04:00
return err
2014-03-27 12:48:29 -04:00
}
2014-07-22 13:50:34 +02:00
2017-02-28 20:08:45 -05:00
if len ( opts . LabelIDs ) > 0 {
2016-11-21 20:08:21 +01:00
// During the session, SQLite3 driver cannot handle retrieve objects after update something.
2016-03-05 20:45:23 -05:00
// So we have to get all needed labels first.
2017-02-28 20:08:45 -05:00
labels := make ( [ ] * Label , 0 , len ( opts . LabelIDs ) )
if err = e . In ( "id" , opts . LabelIDs ) . Find ( & labels ) ; err != nil {
return fmt . Errorf ( "find all labels [label_ids: %v]: %v" , opts . LabelIDs , err )
2016-03-05 20:45:23 -05:00
}
2015-08-15 00:42:43 +08:00
2017-01-30 20:46:45 +08:00
if err = opts . Issue . loadPoster ( e ) ; err != nil {
return err
}
2016-03-05 20:45:23 -05:00
for _ , label := range labels {
2016-08-15 18:40:32 -07:00
// Silently drop invalid labels.
if label . RepoID != opts . Repo . ID {
2016-03-13 23:20:22 -04:00
continue
}
2017-01-30 20:46:45 +08:00
if err = opts . Issue . addLabel ( e , label , opts . Issue . Poster ) ; err != nil {
2016-08-15 18:40:32 -07:00
return fmt . Errorf ( "addLabel [id: %d]: %v" , label . ID , err )
2016-03-05 20:45:23 -05:00
}
2015-08-10 16:52:08 +08:00
}
}
2016-08-15 18:40:32 -07:00
if err = newIssueUsers ( e , opts . Repo , opts . Issue ) ; err != nil {
2015-08-10 21:47:23 +08:00
return err
}
2017-01-24 21:43:02 -05:00
UpdateIssueIndexer ( opts . Issue )
2016-08-15 18:40:32 -07:00
if len ( opts . Attachments ) > 0 {
attachments , err := getAttachmentsByUUIDs ( e , opts . Attachments )
2015-09-01 19:07:02 -04:00
if err != nil {
2016-08-15 18:40:32 -07:00
return fmt . Errorf ( "getAttachmentsByUUIDs [uuids: %v]: %v" , opts . Attachments , err )
2015-09-01 19:07:02 -04:00
}
2016-08-15 18:40:32 -07:00
for i := 0 ; i < len ( attachments ) ; i ++ {
attachments [ i ] . IssueID = opts . Issue . ID
if _ , err = e . Id ( attachments [ i ] . ID ) . Update ( attachments [ i ] ) ; err != nil {
return fmt . Errorf ( "update attachment [id: %d]: %v" , attachments [ i ] . ID , err )
}
2015-08-11 23:24:40 +08:00
}
}
2016-08-15 18:40:32 -07:00
return opts . Issue . loadAttributes ( e )
2015-09-01 19:07:02 -04:00
}
// NewIssue creates new issue with labels for repository.
func NewIssue ( repo * Repository , issue * Issue , labelIDs [ ] int64 , uuids [ ] string ) ( err error ) {
sess := x . NewSession ( )
defer sessionRelease ( sess )
if err = sess . Begin ( ) ; err != nil {
return err
}
2017-02-01 10:36:08 +08:00
if err = newIssue ( sess , issue . Poster , NewIssueOptions {
2016-08-15 18:40:32 -07:00
Repo : repo ,
Issue : issue ,
2017-02-28 20:08:45 -05:00
LabelIDs : labelIDs ,
2016-08-15 18:40:32 -07:00
Attachments : uuids ,
} ) ; err != nil {
2015-09-01 19:07:02 -04:00
return fmt . Errorf ( "newIssue: %v" , err )
}
2016-03-13 23:20:22 -04:00
if err = sess . Commit ( ) ; err != nil {
return fmt . Errorf ( "Commit: %v" , err )
}
2016-08-15 18:40:32 -07:00
if err = NotifyWatchers ( & Action {
2016-07-24 01:08:22 +08:00
ActUserID : issue . Poster . ID ,
2015-08-10 23:31:59 +08:00
ActUserName : issue . Poster . Name ,
2016-11-07 16:37:32 +01:00
OpType : ActionCreateIssue ,
2016-08-14 03:32:24 -07:00
Content : fmt . Sprintf ( "%d|%s" , issue . Index , issue . Title ) ,
2015-08-10 23:31:59 +08:00
RepoID : repo . ID ,
RepoUserName : repo . Owner . Name ,
RepoName : repo . Name ,
IsPrivate : repo . IsPrivate ,
2016-08-15 18:40:32 -07:00
} ) ; err != nil {
2016-07-16 00:36:39 +08:00
log . Error ( 4 , "NotifyWatchers: %v" , err )
2016-08-15 18:40:32 -07:00
}
if err = issue . MailParticipants ( ) ; err != nil {
2016-07-16 00:36:39 +08:00
log . Error ( 4 , "MailParticipants: %v" , err )
2015-08-10 23:31:59 +08:00
}
2016-03-13 23:20:22 -04:00
return nil
2014-03-20 16:04:56 -04:00
}
2014-07-23 13:48:06 +02:00
// GetIssueByRef returns an Issue specified by a GFM reference.
// See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
2015-09-03 04:34:08 -04:00
func GetIssueByRef ( ref string ) ( * Issue , error ) {
2014-07-23 13:48:06 +02:00
n := strings . IndexByte ( ref , byte ( '#' ) )
if n == - 1 {
2016-11-22 12:24:39 +01:00
return nil , errMissingIssueNumber
2014-07-23 13:48:06 +02:00
}
2015-09-03 04:34:08 -04:00
index , err := com . StrTo ( ref [ n + 1 : ] ) . Int64 ( )
if err != nil {
return nil , err
2014-07-23 13:48:06 +02:00
}
2015-09-03 04:34:08 -04:00
repo , err := GetRepositoryByRef ( ref [ : n ] )
if err != nil {
return nil , err
2014-07-23 13:48:06 +02:00
}
2015-09-03 04:34:08 -04:00
issue , err := GetIssueByIndex ( repo . ID , index )
if err != nil {
return nil , err
}
2016-07-21 14:26:30 +08:00
return issue , issue . LoadAttributes ( )
2014-07-23 13:48:06 +02:00
}
2016-11-22 12:24:39 +01:00
// GetRawIssueByIndex returns raw issue without loading attributes by index in a repository.
2016-08-26 13:40:53 -07:00
func GetRawIssueByIndex ( repoID , index int64 ) ( * Issue , error ) {
2015-08-12 17:04:23 +08:00
issue := & Issue {
RepoID : repoID ,
Index : index ,
}
2014-06-21 00:51:41 -04:00
has , err := x . Get ( issue )
2014-03-22 16:00:46 -04:00
if err != nil {
return nil , err
} else if ! has {
2015-08-12 17:04:23 +08:00
return nil , ErrIssueNotExist { 0 , repoID , index }
2014-03-22 16:00:46 -04:00
}
2016-08-26 13:40:53 -07:00
return issue , nil
}
// GetIssueByIndex returns issue by index in a repository.
func GetIssueByIndex ( repoID , index int64 ) ( * Issue , error ) {
issue , err := GetRawIssueByIndex ( repoID , index )
if err != nil {
return nil , err
}
2016-07-21 14:26:30 +08:00
return issue , issue . LoadAttributes ( )
2014-03-22 16:00:46 -04:00
}
2016-08-14 03:32:24 -07:00
func getIssueByID ( e Engine , id int64 ) ( * Issue , error ) {
2015-08-12 17:04:23 +08:00
issue := new ( Issue )
2016-08-14 03:32:24 -07:00
has , err := e . Id ( id ) . Get ( issue )
2014-05-07 16:51:14 -04:00
if err != nil {
return nil , err
} else if ! has {
2015-08-12 17:04:23 +08:00
return nil , ErrIssueNotExist { id , 0 , 0 }
2014-05-07 16:51:14 -04:00
}
2016-07-21 14:26:30 +08:00
return issue , issue . LoadAttributes ( )
2014-05-07 16:51:14 -04:00
}
2016-08-14 03:32:24 -07:00
// GetIssueByID returns an issue by given ID.
func GetIssueByID ( id int64 ) ( * Issue , error ) {
return getIssueByID ( x , id )
}
2017-03-14 21:10:35 -04:00
func getIssuesByIDs ( e Engine , issueIDs [ ] int64 ) ( [ ] * Issue , error ) {
issues := make ( [ ] * Issue , 0 , 10 )
return issues , e . In ( "id" , issueIDs ) . Find ( & issues )
}
// 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 16:18:09 -04:00
type IssuesOptions struct {
RepoID int64
2016-12-24 05:33:21 -05:00
AssigneeID int64
2015-09-02 16:18:09 -04:00
PosterID int64
2016-12-24 05:33:21 -05:00
MentionedID int64
2015-09-02 16:18:09 -04:00
MilestoneID int64
RepoIDs [ ] int64
Page int
2017-01-24 21:43:02 -05:00
IsClosed util . OptionalBool
IsPull util . OptionalBool
2015-09-02 16:18:09 -04:00
Labels string
SortType string
2017-01-24 21:43:02 -05:00
IssueIDs [ ] int64
2015-09-02 16:18:09 -04:00
}
2017-01-01 13:15:09 -05:00
// sortIssuesSession sort an issues-related session based on the provided
// sortType string
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" )
default :
sess . Desc ( "issue.created_unix" )
}
}
2015-08-04 22:24:04 +08:00
// Issues returns a list of issues by given conditions.
2015-09-02 16:18:09 -04:00
func Issues ( opts * IssuesOptions ) ( [ ] * Issue , error ) {
2017-01-24 21:43:02 -05:00
var sess * xorm . Session
if opts . Page >= 0 {
var start int
if opts . Page == 0 {
start = 0
} else {
start = ( opts . Page - 1 ) * setting . UI . IssuePagingNum
}
sess = x . Limit ( setting . UI . IssuePagingNum , start )
} else {
sess = x . NewSession ( )
defer sess . Close ( )
2016-03-13 23:20:22 -04:00
}
2017-01-24 21:43:02 -05:00
if len ( opts . IssueIDs ) > 0 {
sess . In ( "issue.id" , opts . IssueIDs )
}
2014-03-22 16:00:46 -04:00
2015-09-02 16:18:09 -04:00
if opts . RepoID > 0 {
2016-11-12 20:06:33 +08:00
sess . And ( "issue.repo_id=?" , opts . RepoID )
} else if len ( opts . RepoIDs ) > 0 {
2015-09-01 06:31:47 -04:00
// In case repository IDs are provided but actually no repository has issue.
2016-11-12 20:06:33 +08:00
sess . In ( "issue.repo_id" , opts . RepoIDs )
2014-03-22 16:00:46 -04:00
}
2017-01-24 21:43:02 -05:00
switch opts . IsClosed {
case util . OptionalBoolTrue :
2017-01-25 16:28:03 +08:00
sess . And ( "issue.is_closed=?" , true )
2017-01-24 21:43:02 -05:00
case util . OptionalBoolFalse :
2017-01-25 16:28:03 +08:00
sess . And ( "issue.is_closed=?" , false )
2017-01-24 21:43:02 -05:00
}
2014-03-22 16:00:46 -04:00
2015-09-02 16:18:09 -04:00
if opts . AssigneeID > 0 {
2016-07-16 18:18:35 -07:00
sess . And ( "issue.assignee_id=?" , opts . AssigneeID )
2016-12-24 05:33:21 -05:00
}
if opts . PosterID > 0 {
2016-07-16 18:18:35 -07:00
sess . And ( "issue.poster_id=?" , opts . PosterID )
2014-03-22 13:50:50 -04:00
}
2016-12-24 05:33:21 -05: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 16:18:09 -04:00
if opts . MilestoneID > 0 {
2016-07-16 18:18:35 -07:00
sess . And ( "issue.milestone_id=?" , opts . MilestoneID )
2014-03-22 13:50:50 -04:00
}
2017-01-24 21:43:02 -05:00
switch opts . IsPull {
case util . OptionalBoolTrue :
2017-01-28 14:01:07 -02:00
sess . And ( "issue.is_pull=?" , true )
2017-01-24 21:43:02 -05:00
case util . OptionalBoolFalse :
2017-01-28 14:01:07 -02:00
sess . And ( "issue.is_pull=?" , false )
2017-01-24 21:43:02 -05:00
}
2015-09-02 16:18:09 -04:00
2017-01-01 13:15:09 -05:00
sortIssuesSession ( sess , opts . SortType )
2014-03-22 13:50:50 -04:00
2016-05-06 15:40:41 -04:00
if len ( opts . Labels ) > 0 && opts . Labels != "0" {
2016-12-22 03:58:04 -05:00
labelIDs , err := base . StringsToInt64s ( strings . Split ( opts . Labels , "," ) )
if err != nil {
return nil , err
}
2016-05-06 15:40:41 -04:00
if len ( labelIDs ) > 0 {
2016-11-10 16:16:32 +01:00
sess .
Join ( "INNER" , "issue_label" , "issue.id = issue_label.issue_id" ) .
In ( "issue_label.label_id" , labelIDs )
2016-04-26 00:22:03 -04:00
}
2015-08-10 21:47:23 +08:00
}
2016-07-24 00:23:54 +08:00
issues := make ( [ ] * Issue , 0 , setting . UI . IssuePagingNum )
2016-08-26 13:40:53 -07:00
if err := sess . Find ( & issues ) ; err != nil {
return nil , fmt . Errorf ( "Find: %v" , err )
}
2017-02-22 22:03:59 +08:00
if err := IssueList ( issues ) . LoadAttributes ( ) ; err != nil {
return nil , fmt . Errorf ( "LoadAttributes: %v" , err )
2016-08-26 13:40:53 -07:00
}
return issues , nil
2014-03-22 13:50:50 -04:00
}
2016-07-16 00:36:39 +08:00
// UpdateIssueMentions extracts mentioned people from content and
// updates issue-user relations for them.
2016-12-22 17:00:39 +08:00
func UpdateIssueMentions ( e Engine , issueID int64 , mentions [ ] string ) error {
2016-07-16 00:36:39 +08:00
if len ( mentions ) == 0 {
return nil
2015-12-21 04:24:11 -08:00
}
2016-07-16 00:36:39 +08:00
for i := range mentions {
mentions [ i ] = strings . ToLower ( mentions [ i ] )
}
users := make ( [ ] * User , 0 , len ( mentions ) )
2016-12-22 17:00:39 +08:00
if err := e . In ( "lower_name" , mentions ) . Asc ( "lower_name" ) . Find ( & users ) ; err != nil {
2016-07-16 00:36:39 +08:00
return fmt . Errorf ( "find mentioned users: %v" , err )
2015-12-21 04:24:11 -08:00
}
2016-07-16 00:36:39 +08:00
ids := make ( [ ] int64 , 0 , len ( mentions ) )
2015-12-21 04:24:11 -08:00
for _ , user := range users {
2016-07-24 01:08:22 +08:00
ids = append ( ids , user . ID )
2016-07-16 00:36:39 +08:00
if ! user . IsOrganization ( ) || user . NumMembers == 0 {
2015-12-21 04:24:11 -08:00
continue
}
2016-07-16 00:36:39 +08:00
memberIDs := make ( [ ] int64 , 0 , user . NumMembers )
2016-07-24 01:08:22 +08:00
orgUsers , err := GetOrgUsersByOrgID ( user . ID )
2015-12-21 04:24:11 -08:00
if err != nil {
2016-07-24 01:08:22 +08:00
return fmt . Errorf ( "GetOrgUsersByOrgID [%d]: %v" , user . ID , err )
2015-12-21 04:24:11 -08:00
}
for _ , orgUser := range orgUsers {
2016-07-16 00:36:39 +08:00
memberIDs = append ( memberIDs , orgUser . ID )
2015-12-21 04:24:11 -08:00
}
2016-07-16 00:36:39 +08:00
ids = append ( ids , memberIDs ... )
2015-12-21 04:24:11 -08:00
}
2016-12-22 17:00:39 +08:00
if err := UpdateIssueUsersByMentions ( e , issueID , ids ) ; err != nil {
2016-07-16 00:36:39 +08:00
return fmt . Errorf ( "UpdateIssueUsersByMentions: %v" , err )
2015-12-21 04:24:11 -08:00
}
return nil
}
2014-05-07 12:09:30 -04:00
// IssueStats represents issue statistic information.
type IssueStats struct {
OpenCount , ClosedCount int64
2017-02-14 22:15:18 +08:00
YourRepositoriesCount int64
2014-05-07 12:09:30 -04:00
AssignCount int64
CreateCount int64
MentionCount int64
}
// Filter modes.
const (
2016-11-07 17:24:59 +01:00
FilterModeAll = iota
FilterModeAssign
FilterModeCreate
FilterModeMention
2014-05-07 12:09:30 -04:00
)
2015-08-10 21:47:23 +08: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 16:18:09 -04:00
type IssueStatsOptions struct {
2017-02-14 22:15:18 +08:00
FilterMode int
2015-09-02 16:18:09 -04:00
RepoID int64
2016-04-26 06:07:49 +02:00
Labels string
2015-09-02 16:18:09 -04:00
MilestoneID int64
AssigneeID int64
2016-12-24 05:33:21 -05:00
MentionedID int64
PosterID int64
2015-09-02 16:18:09 -04:00
IsPull bool
2017-01-24 21:43:02 -05:00
IssueIDs [ ] int64
2015-09-02 16:18:09 -04:00
}
2014-05-07 16:51:14 -04:00
// GetIssueStats returns issue statistic information by given conditions.
2017-01-24 21:43:02 -05:00
func GetIssueStats ( opts * IssueStatsOptions ) ( * IssueStats , error ) {
2014-05-07 12:09:30 -04:00
stats := & IssueStats { }
2015-07-25 02:52:25 +08: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 .
Where ( "issue.repo_id = ?" , opts . RepoID ) .
And ( "is_pull = ?" , opts . IsPull )
2015-08-10 21:47:23 +08:00
2017-01-24 21:43:02 -05:00
if len ( opts . IssueIDs ) > 0 {
sess . In ( "issue.id" , opts . IssueIDs )
}
2016-05-06 15:40:41 -04:00
if len ( opts . Labels ) > 0 && opts . Labels != "0" {
2016-12-22 03:58:04 -05:00
labelIDs , err := base . StringsToInt64s ( strings . Split ( opts . Labels , "," ) )
if err != nil {
log . Warn ( "Malformed Labels argument: %s" , opts . Labels )
} else if len ( labelIDs ) > 0 {
sess . Join ( "INNER" , "issue_label" , "issue.id = issue_id" ) .
2016-11-10 16:16:32 +01:00
In ( "label_id" , labelIDs )
2016-04-26 00:22:03 -04:00
}
2016-04-26 06:07:49 +02:00
}
if opts . MilestoneID > 0 {
sess . And ( "issue.milestone_id = ?" , opts . MilestoneID )
}
if opts . AssigneeID > 0 {
sess . And ( "assignee_id = ?" , opts . AssigneeID )
}
2016-12-24 05:33:21 -05:00
if opts . PosterID > 0 {
sess . And ( "poster_id = ?" , opts . PosterID )
}
2015-07-25 02:52:25 +08:00
2016-12-24 05:33:21 -05:00
if opts . MentionedID > 0 {
sess . Join ( "INNER" , "issue_user" , "issue.id = issue_user.issue_id" ) .
2016-12-30 15:26:05 +08:00
And ( "issue_user.uid = ?" , opts . MentionedID ) .
And ( "issue_user.is_mentioned = ?" , true )
2016-12-24 05:33:21 -05:00
}
2016-04-26 06:07:49 +02:00
2016-12-24 05:33:21 -05:00
return sess
2015-07-25 02:52:25 +08:00
}
2016-12-24 05:33:21 -05:00
2017-01-24 21:43:02 -05:00
var err error
2017-02-14 22:15:18 +08:00
switch opts . FilterMode {
case FilterModeAll , FilterModeAssign :
stats . OpenCount , err = countSession ( opts ) .
And ( "is_closed = ?" , false ) .
Count ( new ( Issue ) )
stats . ClosedCount , err = countSession ( opts ) .
And ( "is_closed = ?" , true ) .
Count ( new ( Issue ) )
case FilterModeCreate :
stats . OpenCount , err = countSession ( opts ) .
And ( "poster_id = ?" , opts . PosterID ) .
And ( "is_closed = ?" , false ) .
Count ( new ( Issue ) )
stats . ClosedCount , err = countSession ( opts ) .
And ( "poster_id = ?" , opts . PosterID ) .
And ( "is_closed = ?" , true ) .
Count ( new ( Issue ) )
case FilterModeMention :
stats . OpenCount , err = countSession ( opts ) .
Join ( "INNER" , "issue_user" , "issue.id = issue_user.issue_id" ) .
And ( "issue_user.uid = ?" , opts . PosterID ) .
And ( "issue_user.is_mentioned = ?" , true ) .
And ( "issue.is_closed = ?" , false ) .
Count ( new ( Issue ) )
stats . ClosedCount , err = countSession ( opts ) .
Join ( "INNER" , "issue_user" , "issue.id = issue_user.issue_id" ) .
And ( "issue_user.uid = ?" , opts . PosterID ) .
And ( "issue_user.is_mentioned = ?" , true ) .
And ( "issue.is_closed = ?" , true ) .
Count ( new ( Issue ) )
2017-01-24 21:43:02 -05:00
}
2017-02-14 22:15:18 +08:00
return stats , err
2014-05-07 12:09:30 -04:00
}
2014-05-07 16:51:14 -04:00
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
2015-09-02 16:18:09 -04:00
func GetUserIssueStats ( repoID , uid int64 , repoIDs [ ] int64 , filterMode int , isPull bool ) * IssueStats {
2014-05-07 12:09:30 -04:00
stats := & IssueStats { }
2015-08-25 22:58:34 +08:00
2016-04-26 06:07:49 +02:00
countSession := func ( isClosed , isPull bool , repoID int64 , repoIDs [ ] int64 ) * xorm . Session {
2016-11-10 16:16:32 +01:00
sess := x .
Where ( "issue.is_closed = ?" , isClosed ) .
And ( "issue.is_pull = ?" , isPull )
2015-09-02 16:18:09 -04:00
2016-11-12 20:06:33 +08:00
if repoID > 0 {
2016-04-26 00:22:03 -04:00
sess . And ( "repo_id = ?" , repoID )
2016-11-12 20:06:33 +08:00
} else if len ( repoIDs ) > 0 {
2016-04-26 00:22:03 -04:00
sess . In ( "repo_id" , repoIDs )
2016-04-26 06:07:49 +02:00
}
return sess
2015-09-02 16:18:09 -04:00
}
2017-02-14 22:15:18 +08:00
stats . AssignCount , _ = countSession ( false , isPull , repoID , nil ) .
2016-04-26 06:07:49 +02:00
And ( "assignee_id = ?" , uid ) .
2017-02-14 22:15:18 +08:00
Count ( new ( Issue ) )
2016-04-26 06:07:49 +02:00
2017-02-14 22:15:18 +08:00
stats . CreateCount , _ = countSession ( false , isPull , repoID , nil ) .
2016-08-09 23:19:52 -07:00
And ( "poster_id = ?" , uid ) .
2017-02-14 22:15:18 +08:00
Count ( new ( Issue ) )
2016-04-26 06:07:49 +02:00
2017-02-14 22:15:18 +08:00
stats . YourRepositoriesCount , _ = countSession ( false , isPull , repoID , repoIDs ) .
Count ( new ( Issue ) )
2015-09-02 16:18:09 -04:00
2015-08-25 22:58:34 +08:00
switch filterMode {
2017-02-14 22:15:18 +08:00
case FilterModeAll :
stats . OpenCount , _ = countSession ( false , isPull , repoID , repoIDs ) .
Count ( new ( Issue ) )
stats . ClosedCount , _ = countSession ( true , isPull , repoID , repoIDs ) .
Count ( new ( Issue ) )
2016-11-07 17:24:59 +01:00
case FilterModeAssign :
2017-02-14 22:15:18 +08:00
stats . OpenCount , _ = countSession ( false , isPull , repoID , nil ) .
And ( "assignee_id = ?" , uid ) .
Count ( new ( Issue ) )
stats . ClosedCount , _ = countSession ( true , isPull , repoID , nil ) .
And ( "assignee_id = ?" , uid ) .
Count ( new ( Issue ) )
2016-11-07 17:24:59 +01:00
case FilterModeCreate :
2017-02-14 22:15:18 +08:00
stats . OpenCount , _ = countSession ( false , isPull , repoID , nil ) .
And ( "poster_id = ?" , uid ) .
Count ( new ( Issue ) )
stats . ClosedCount , _ = countSession ( true , isPull , repoID , nil ) .
And ( "poster_id = ?" , uid ) .
Count ( new ( Issue ) )
2015-08-25 22:58:34 +08:00
}
2014-05-07 12:09:30 -04:00
return stats
}
2015-08-25 22:58:34 +08:00
// GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
2015-09-02 16:18:09 -04: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 17:59:57 +08:00
Where ( "is_closed = ?" , isClosed ) .
2016-04-26 00:22:03 -04:00
And ( "is_pull = ?" , isPull ) .
And ( "repo_id = ?" , repoID )
2015-09-02 16:18:09 -04:00
2016-04-26 06:07:49 +02:00
return sess
2015-09-02 16:18:09 -04:00
}
2016-04-26 06:07:49 +02:00
openCountSession := countSession ( false , isPull , repoID )
closedCountSession := countSession ( true , isPull , repoID )
2015-08-25 22:58:34 +08:00
switch filterMode {
2016-11-07 17:24:59 +01:00
case FilterModeAssign :
2016-04-26 06:07:49 +02:00
openCountSession . And ( "assignee_id = ?" , uid )
closedCountSession . And ( "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 22:58:34 +08:00
}
2017-02-14 22:15:18 +08:00
openResult , _ := openCountSession . Count ( new ( Issue ) )
closedResult , _ := closedCountSession . Count ( new ( Issue ) )
2016-04-26 06:07:49 +02:00
return openResult , closedResult
2015-08-25 22:58:34 +08:00
}
2015-08-10 18:57:57 +08:00
func updateIssue ( e Engine , issue * Issue ) error {
_ , err := e . Id ( issue . ID ) . AllCols ( ) . Update ( issue )
2017-01-24 21:43:02 -05:00
if err != nil {
return err
}
UpdateIssueIndexer ( issue )
return nil
2015-08-10 18:57:57 +08:00
}
2015-10-24 03:36:47 -04:00
// UpdateIssue updates all fields of given issue.
func UpdateIssue ( issue * Issue ) error {
return updateIssue ( x , issue )
}