2016-03-05 20:58:51 +03:00
// Copyright 2016 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
import (
"fmt"
"strings"
"time"
"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
2016-08-26 23:40:53 +03:00
2016-11-11 12:39:44 +03:00
api "code.gitea.io/sdk/gitea"
2016-03-05 20:58:51 +03:00
2016-11-10 19:24:48 +03:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markdown"
2016-03-05 20:58:51 +03:00
)
// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
type CommentType int
2016-11-28 16:33:09 +03:00
// Enumerate all the comment types
2016-03-05 20:58:51 +03:00
const (
// Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0)
2016-11-07 19:30:04 +03:00
CommentTypeComment CommentType = iota
CommentTypeReopen
2016-11-07 19:35:34 +03:00
CommentTypeClose
2016-03-05 20:58:51 +03:00
// References.
2016-11-07 19:35:34 +03:00
CommentTypeIssueRef
2016-03-05 20:58:51 +03:00
// Reference from a commit (not part of a pull request)
2016-11-07 19:35:34 +03:00
CommentTypeCommitRef
2016-03-05 20:58:51 +03:00
// Reference from a comment
2016-11-07 23:58:22 +03:00
CommentTypeCommentRef
2016-03-05 20:58:51 +03:00
// Reference from a pull request
2016-11-07 19:35:34 +03:00
CommentTypePullRef
2016-03-05 20:58:51 +03:00
)
2016-11-28 16:33:09 +03:00
// CommentTag defines comment tag type
2016-03-05 20:58:51 +03:00
type CommentTag int
2016-11-28 16:33:09 +03:00
// Enumerate all the comment tag types
2016-03-05 20:58:51 +03:00
const (
2016-11-07 19:30:04 +03:00
CommentTagNone CommentTag = iota
CommentTagPoster
CommentTagWriter
2016-11-07 19:35:34 +03:00
CommentTagOwner
2016-03-05 20:58:51 +03:00
)
// Comment represents a comment in commit and issue page.
type Comment struct {
ID int64 ` xorm:"pk autoincr" `
Type CommentType
2017-01-06 18:14:33 +03:00
PosterID int64 ` xorm:"INDEX" `
2016-03-05 20:58:51 +03:00
Poster * User ` xorm:"-" `
IssueID int64 ` xorm:"INDEX" `
CommitID int64
Line int64
2016-03-10 03:53:30 +03:00
Content string ` xorm:"TEXT" `
RenderedContent string ` xorm:"-" `
Created time . Time ` xorm:"-" `
2017-01-06 18:14:33 +03:00
CreatedUnix int64 ` xorm:"INDEX" `
2016-08-26 23:40:53 +03:00
Updated time . Time ` xorm:"-" `
2017-01-06 18:14:33 +03:00
UpdatedUnix int64 ` xorm:"INDEX" `
2016-03-05 20:58:51 +03:00
// Reference issue in commit message
CommitSHA string ` xorm:"VARCHAR(40)" `
Attachments [ ] * Attachment ` xorm:"-" `
// For view issue page.
ShowTag CommentTag ` xorm:"-" `
}
2016-11-28 16:33:09 +03:00
// BeforeInsert will be invoked by XORM before inserting a record
// representing this object.
2016-03-10 03:53:30 +03:00
func ( c * Comment ) BeforeInsert ( ) {
2016-07-23 15:24:44 +03:00
c . CreatedUnix = time . Now ( ) . Unix ( )
2016-08-26 23:40:53 +03:00
c . UpdatedUnix = c . CreatedUnix
}
2016-11-28 16:33:09 +03:00
// BeforeUpdate is invoked from XORM before updating this object.
2016-08-26 23:40:53 +03:00
func ( c * Comment ) BeforeUpdate ( ) {
c . UpdatedUnix = time . Now ( ) . Unix ( )
2016-03-10 03:53:30 +03:00
}
2016-11-28 16:33:09 +03:00
// AfterSet is invoked from XORM after setting the value of a field of this object.
2016-03-05 20:58:51 +03:00
func ( c * Comment ) AfterSet ( colName string , _ xorm . Cell ) {
var err error
switch colName {
case "id" :
c . Attachments , err = GetAttachmentsByCommentID ( c . ID )
if err != nil {
log . Error ( 3 , "GetAttachmentsByCommentID[%d]: %v" , c . ID , err )
}
case "poster_id" :
c . Poster , err = GetUserByID ( c . PosterID )
if err != nil {
if IsErrUserNotExist ( err ) {
c . PosterID = - 1
2016-08-14 13:32:24 +03:00
c . Poster = NewGhostUser ( )
2016-03-05 20:58:51 +03:00
} else {
log . Error ( 3 , "GetUserByID[%d]: %v" , c . ID , err )
}
}
2016-03-10 03:53:30 +03:00
case "created_unix" :
c . Created = time . Unix ( c . CreatedUnix , 0 ) . Local ( )
2016-08-26 23:40:53 +03:00
case "updated_unix" :
c . Updated = time . Unix ( c . UpdatedUnix , 0 ) . Local ( )
2016-03-05 20:58:51 +03:00
}
}
2016-11-28 16:33:09 +03:00
// AfterDelete is invoked from XORM after the object is deleted.
2016-03-05 20:58:51 +03:00
func ( c * Comment ) AfterDelete ( ) {
_ , err := DeleteAttachmentsByComment ( c . ID , true )
if err != nil {
log . Info ( "Could not delete files for comment %d on issue #%d: %s" , c . ID , c . IssueID , err )
}
}
2016-12-22 11:29:26 +03:00
// HTMLURL formats a URL-string to the issue-comment
func ( c * Comment ) HTMLURL ( ) string {
issue , err := GetIssueByID ( c . IssueID )
if err != nil { // Silently dropping errors :unamused:
log . Error ( 4 , "GetIssueByID(%d): %v" , c . IssueID , err )
return ""
}
return fmt . Sprintf ( "%s#issuecomment-%d" , issue . HTMLURL ( ) , c . ID )
}
// IssueURL formats a URL-string to the issue
func ( c * Comment ) IssueURL ( ) string {
issue , err := GetIssueByID ( c . IssueID )
if err != nil { // Silently dropping errors :unamused:
log . Error ( 4 , "GetIssueByID(%d): %v" , c . IssueID , err )
return ""
}
if issue . IsPull {
return ""
}
return issue . HTMLURL ( )
}
// PRURL formats a URL-string to the pull-request
func ( c * Comment ) PRURL ( ) string {
issue , err := GetIssueByID ( c . IssueID )
if err != nil { // Silently dropping errors :unamused:
log . Error ( 4 , "GetIssueByID(%d): %v" , c . IssueID , err )
return ""
}
if ! issue . IsPull {
return ""
}
return issue . HTMLURL ( )
}
2016-11-28 16:33:09 +03:00
// APIFormat converts a Comment to the api.Comment format
2016-08-26 21:23:21 +03:00
func ( c * Comment ) APIFormat ( ) * api . Comment {
2016-08-26 23:40:53 +03:00
return & api . Comment {
2016-12-22 11:29:26 +03:00
ID : c . ID ,
Poster : c . Poster . APIFormat ( ) ,
HTMLURL : c . HTMLURL ( ) ,
IssueURL : c . IssueURL ( ) ,
PRURL : c . PRURL ( ) ,
Body : c . Content ,
Created : c . Created ,
Updated : c . Updated ,
2016-08-26 21:23:21 +03:00
}
}
2016-03-05 20:58:51 +03:00
// HashTag returns unique hash tag for comment.
func ( c * Comment ) HashTag ( ) string {
return "issuecomment-" + com . ToStr ( c . ID )
}
// EventTag returns unique event hash tag for comment.
func ( c * Comment ) EventTag ( ) string {
return "event-" + com . ToStr ( c . ID )
}
2016-07-15 19:36:39 +03:00
// MailParticipants sends new comment emails to repository watchers
// and mentioned people.
2016-12-22 12:00:39 +03:00
func ( c * Comment ) MailParticipants ( e Engine , opType ActionType , issue * Issue ) ( err error ) {
2016-11-28 16:33:09 +03:00
mentions := markdown . FindAllMentions ( c . Content )
2016-12-22 12:00:39 +03:00
if err = UpdateIssueMentions ( e , c . IssueID , mentions ) ; err != nil {
2016-11-28 16:33:09 +03:00
return fmt . Errorf ( "UpdateIssueMentions [%d]: %v" , c . IssueID , err )
2016-07-15 19:36:39 +03:00
}
switch opType {
2016-11-07 18:37:32 +03:00
case ActionCommentIssue :
2016-11-28 16:33:09 +03:00
issue . Content = c . Content
2016-11-07 18:37:32 +03:00
case ActionCloseIssue :
2016-07-15 19:36:39 +03:00
issue . Content = fmt . Sprintf ( "Closed #%d" , issue . Index )
2016-11-07 18:37:32 +03:00
case ActionReopenIssue :
2016-07-15 19:36:39 +03:00
issue . Content = fmt . Sprintf ( "Reopened #%d" , issue . Index )
}
2016-11-28 16:33:09 +03:00
if err = mailIssueCommentToParticipants ( issue , c . Poster , mentions ) ; err != nil {
2016-07-15 19:36:39 +03:00
log . Error ( 4 , "mailIssueCommentToParticipants: %v" , err )
}
return nil
}
2016-03-05 20:58:51 +03:00
func createComment ( e * xorm . Session , opts * CreateCommentOptions ) ( _ * Comment , err error ) {
comment := & Comment {
Type : opts . Type ,
2016-07-23 20:08:22 +03:00
PosterID : opts . Doer . ID ,
2016-07-15 19:36:39 +03:00
Poster : opts . Doer ,
2016-03-05 20:58:51 +03:00
IssueID : opts . Issue . ID ,
CommitID : opts . CommitID ,
CommitSHA : opts . CommitSHA ,
Line : opts . LineNum ,
Content : opts . Content ,
}
if _ , err = e . Insert ( comment ) ; err != nil {
return nil , err
}
2016-03-06 02:08:42 +03:00
// Compose comment action, could be plain comment, close or reopen issue/pull request.
2016-03-05 20:58:51 +03:00
// This object will be used to notify watchers in the end of function.
act := & Action {
2016-07-23 20:08:22 +03:00
ActUserID : opts . Doer . ID ,
2016-03-05 20:58:51 +03:00
ActUserName : opts . Doer . Name ,
Content : fmt . Sprintf ( "%d|%s" , opts . Issue . Index , strings . Split ( opts . Content , "\n" ) [ 0 ] ) ,
RepoID : opts . Repo . ID ,
RepoUserName : opts . Repo . Owner . Name ,
RepoName : opts . Repo . Name ,
IsPrivate : opts . Repo . IsPrivate ,
}
// Check comment type.
switch opts . Type {
2016-11-07 19:30:04 +03:00
case CommentTypeComment :
2016-11-07 18:37:32 +03:00
act . OpType = ActionCommentIssue
2016-03-05 20:58:51 +03:00
if _ , err = e . Exec ( "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?" , opts . Issue . ID ) ; err != nil {
return nil , err
}
// Check attachments
attachments := make ( [ ] * Attachment , 0 , len ( opts . Attachments ) )
for _ , uuid := range opts . Attachments {
attach , err := getAttachmentByUUID ( e , uuid )
if err != nil {
if IsErrAttachmentNotExist ( err ) {
continue
}
2016-07-15 19:36:39 +03:00
return nil , fmt . Errorf ( "getAttachmentByUUID [%s]: %v" , uuid , err )
2016-03-05 20:58:51 +03:00
}
attachments = append ( attachments , attach )
}
for i := range attachments {
attachments [ i ] . IssueID = opts . Issue . ID
attachments [ i ] . CommentID = comment . ID
// No assign value could be 0, so ignore AllCols().
if _ , err = e . Id ( attachments [ i ] . ID ) . Update ( attachments [ i ] ) ; err != nil {
2016-07-15 19:36:39 +03:00
return nil , fmt . Errorf ( "update attachment [%d]: %v" , attachments [ i ] . ID , err )
2016-03-05 20:58:51 +03:00
}
}
2016-11-07 19:30:04 +03:00
case CommentTypeReopen :
2016-11-07 18:37:32 +03:00
act . OpType = ActionReopenIssue
2016-03-05 20:58:51 +03:00
if opts . Issue . IsPull {
2016-11-07 18:37:32 +03:00
act . OpType = ActionReopenPullRequest
2016-03-05 20:58:51 +03:00
}
if opts . Issue . IsPull {
_ , err = e . Exec ( "UPDATE `repository` SET num_closed_pulls=num_closed_pulls-1 WHERE id=?" , opts . Repo . ID )
} else {
_ , err = e . Exec ( "UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?" , opts . Repo . ID )
}
if err != nil {
return nil , err
}
2016-03-06 02:08:42 +03:00
2016-11-07 19:35:34 +03:00
case CommentTypeClose :
2016-11-07 18:37:32 +03:00
act . OpType = ActionCloseIssue
2016-03-05 20:58:51 +03:00
if opts . Issue . IsPull {
2016-11-07 18:37:32 +03:00
act . OpType = ActionClosePullRequest
2016-03-05 20:58:51 +03:00
}
if opts . Issue . IsPull {
_ , err = e . Exec ( "UPDATE `repository` SET num_closed_pulls=num_closed_pulls+1 WHERE id=?" , opts . Repo . ID )
} else {
_ , err = e . Exec ( "UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?" , opts . Repo . ID )
}
if err != nil {
return nil , err
}
2016-07-15 19:36:39 +03:00
2016-03-05 20:58:51 +03:00
}
2016-07-15 19:36:39 +03:00
// Notify watchers for whatever action comes in, ignore if no action type.
2016-03-11 13:11:58 +03:00
if act . OpType > 0 {
if err = notifyWatchers ( e , act ) ; err != nil {
2016-07-15 19:36:39 +03:00
log . Error ( 4 , "notifyWatchers: %v" , err )
2016-03-11 13:11:58 +03:00
}
2016-12-22 12:00:39 +03:00
if err = comment . MailParticipants ( e , act . OpType , opts . Issue ) ; err != nil {
log . Error ( 4 , "MailParticipants: %v" , err )
}
2016-03-05 20:58:51 +03:00
}
return comment , nil
}
func createStatusComment ( e * xorm . Session , doer * User , repo * Repository , issue * Issue ) ( * Comment , error ) {
2016-11-07 19:35:34 +03:00
cmtType := CommentTypeClose
2016-03-05 20:58:51 +03:00
if ! issue . IsClosed {
2016-11-07 19:30:04 +03:00
cmtType = CommentTypeReopen
2016-03-05 20:58:51 +03:00
}
return createComment ( e , & CreateCommentOptions {
Type : cmtType ,
Doer : doer ,
Repo : repo ,
Issue : issue ,
} )
}
2016-11-28 16:33:09 +03:00
// CreateCommentOptions defines options for creating comment
2016-03-05 20:58:51 +03:00
type CreateCommentOptions struct {
Type CommentType
Doer * User
Repo * Repository
Issue * Issue
CommitID int64
CommitSHA string
LineNum int64
Content string
Attachments [ ] string // UUIDs of attachments
}
// CreateComment creates comment of issue or commit.
func CreateComment ( opts * CreateCommentOptions ) ( comment * Comment , err error ) {
sess := x . NewSession ( )
defer sessionRelease ( sess )
if err = sess . Begin ( ) ; err != nil {
return nil , err
}
comment , err = createComment ( sess , opts )
if err != nil {
return nil , err
}
return comment , sess . Commit ( )
}
// CreateIssueComment creates a plain issue comment.
func CreateIssueComment ( doer * User , repo * Repository , issue * Issue , content string , attachments [ ] string ) ( * Comment , error ) {
return CreateComment ( & CreateCommentOptions {
2016-11-07 19:30:04 +03:00
Type : CommentTypeComment ,
2016-03-05 20:58:51 +03:00
Doer : doer ,
Repo : repo ,
Issue : issue ,
Content : content ,
Attachments : attachments ,
} )
}
// CreateRefComment creates a commit reference comment to issue.
func CreateRefComment ( doer * User , repo * Repository , issue * Issue , content , commitSHA string ) error {
if len ( commitSHA ) == 0 {
return fmt . Errorf ( "cannot create reference with empty commit SHA" )
}
// Check if same reference from same commit has already existed.
has , err := x . Get ( & Comment {
2016-11-07 19:35:34 +03:00
Type : CommentTypeCommitRef ,
2016-03-05 20:58:51 +03:00
IssueID : issue . ID ,
CommitSHA : commitSHA ,
} )
if err != nil {
return fmt . Errorf ( "check reference comment: %v" , err )
} else if has {
return nil
}
_ , err = CreateComment ( & CreateCommentOptions {
2016-11-07 19:35:34 +03:00
Type : CommentTypeCommitRef ,
2016-03-05 20:58:51 +03:00
Doer : doer ,
Repo : repo ,
Issue : issue ,
CommitSHA : commitSHA ,
Content : content ,
} )
return err
}
// GetCommentByID returns the comment by given ID.
func GetCommentByID ( id int64 ) ( * Comment , error ) {
c := new ( Comment )
has , err := x . Id ( id ) . Get ( c )
if err != nil {
return nil , err
} else if ! has {
2016-08-26 23:40:53 +03:00
return nil , ErrCommentNotExist { id , 0 }
2016-03-05 20:58:51 +03:00
}
return c , nil
}
2016-08-26 23:40:53 +03:00
func getCommentsByIssueIDSince ( e Engine , issueID , since int64 ) ( [ ] * Comment , error ) {
2016-03-05 20:58:51 +03:00
comments := make ( [ ] * Comment , 0 , 10 )
2016-11-10 18:16:32 +03:00
sess := e .
Where ( "issue_id = ?" , issueID ) .
Asc ( "created_unix" )
2016-08-26 23:40:53 +03:00
if since > 0 {
2016-08-27 00:07:21 +03:00
sess . And ( "updated_unix >= ?" , since )
2016-08-26 23:40:53 +03:00
}
return comments , sess . Find ( & comments )
}
2016-12-22 11:29:26 +03:00
func getCommentsByRepoIDSince ( e Engine , repoID , since int64 ) ( [ ] * Comment , error ) {
comments := make ( [ ] * Comment , 0 , 10 )
2017-01-14 05:21:30 +03:00
sess := e . Where ( "issue.repo_id = ?" , repoID ) .
Join ( "INNER" , "issue" , "issue.id = comment.issue_id" ) .
Asc ( "comment.created_unix" )
2016-12-22 11:29:26 +03:00
if since > 0 {
2017-01-14 05:21:30 +03:00
sess . And ( "comment.updated_unix >= ?" , since )
2016-12-22 11:29:26 +03:00
}
return comments , sess . Find ( & comments )
}
2016-08-26 23:40:53 +03:00
func getCommentsByIssueID ( e Engine , issueID int64 ) ( [ ] * Comment , error ) {
return getCommentsByIssueIDSince ( e , issueID , - 1 )
}
// GetCommentsByIssueID returns all comments of an issue.
func GetCommentsByIssueID ( issueID int64 ) ( [ ] * Comment , error ) {
return getCommentsByIssueID ( x , issueID )
}
2016-11-28 16:33:09 +03:00
// GetCommentsByIssueIDSince returns a list of comments of an issue since a given time point.
2016-08-26 23:40:53 +03:00
func GetCommentsByIssueIDSince ( issueID , since int64 ) ( [ ] * Comment , error ) {
return getCommentsByIssueIDSince ( x , issueID , since )
2016-03-05 20:58:51 +03:00
}
2016-12-22 11:29:26 +03:00
// GetCommentsByRepoIDSince returns a list of comments for all issues in a repo since a given time point.
func GetCommentsByRepoIDSince ( repoID , since int64 ) ( [ ] * Comment , error ) {
return getCommentsByRepoIDSince ( x , repoID , since )
}
2016-03-05 20:58:51 +03:00
// UpdateComment updates information of comment.
func UpdateComment ( c * Comment ) error {
_ , err := x . Id ( c . ID ) . AllCols ( ) . Update ( c )
return err
}
2016-07-25 21:48:17 +03:00
2017-01-25 05:43:02 +03:00
// DeleteComment deletes the comment
func DeleteComment ( comment * Comment ) error {
2016-07-25 21:48:17 +03:00
sess := x . NewSession ( )
defer sessionRelease ( sess )
2017-01-25 05:43:02 +03:00
if err := sess . Begin ( ) ; err != nil {
2016-07-25 21:48:17 +03:00
return err
}
2017-01-25 05:43:02 +03:00
if _ , err := sess . Id ( comment . ID ) . Delete ( new ( Comment ) ) ; err != nil {
2016-07-25 21:48:17 +03:00
return err
}
2016-11-07 19:30:04 +03:00
if comment . Type == CommentTypeComment {
2017-01-25 05:43:02 +03:00
if _ , err := sess . Exec ( "UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?" , comment . IssueID ) ; err != nil {
2016-07-25 21:48:17 +03:00
return err
}
}
return sess . Commit ( )
}