|  | @@ -8,6 +8,7 @@ import (
 | 
	
		
			
				|  |  |  	"bytes"
 | 
	
		
			
				|  |  |  	"errors"
 | 
	
		
			
				|  |  |  	"html/template"
 | 
	
		
			
				|  |  | +	"os"
 | 
	
		
			
				|  |  |  	"strconv"
 | 
	
		
			
				|  |  |  	"strings"
 | 
	
		
			
				|  |  |  	"time"
 | 
	
	
		
			
				|  | @@ -15,14 +16,17 @@ import (
 | 
	
		
			
				|  |  |  	"github.com/go-xorm/xorm"
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  	"github.com/gogits/gogs/modules/base"
 | 
	
		
			
				|  |  | +	"github.com/gogits/gogs/modules/log"
 | 
	
		
			
				|  |  |  )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  var (
 | 
	
		
			
				|  |  | -	ErrIssueNotExist      = errors.New("Issue does not exist")
 | 
	
		
			
				|  |  | -	ErrLabelNotExist      = errors.New("Label does not exist")
 | 
	
		
			
				|  |  | -	ErrMilestoneNotExist  = errors.New("Milestone does not exist")
 | 
	
		
			
				|  |  | -	ErrWrongIssueCounter  = errors.New("Invalid number of issues for this milestone")
 | 
	
		
			
				|  |  | -	ErrMissingIssueNumber = errors.New("No issue number specified")
 | 
	
		
			
				|  |  | +	ErrIssueNotExist       = errors.New("Issue does not exist")
 | 
	
		
			
				|  |  | +	ErrLabelNotExist       = errors.New("Label does not exist")
 | 
	
		
			
				|  |  | +	ErrMilestoneNotExist   = errors.New("Milestone does not exist")
 | 
	
		
			
				|  |  | +	ErrWrongIssueCounter   = errors.New("Invalid number of issues for this milestone")
 | 
	
		
			
				|  |  | +	ErrAttachmentNotExist  = errors.New("Attachment does not exist")
 | 
	
		
			
				|  |  | +	ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue")
 | 
	
		
			
				|  |  | +	ErrMissingIssueNumber  = errors.New("No issue number specified")
 | 
	
		
			
				|  |  |  )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  // Issue represents an issue or pull request of repository.
 | 
	
	
		
			
				|  | @@ -94,6 +98,19 @@ func (i *Issue) GetAssignee() (err error) {
 | 
	
		
			
				|  |  |  	return err
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +func (i *Issue) Attachments() []*Attachment {
 | 
	
		
			
				|  |  | +	a, _ := GetAttachmentsForIssue(i.Id)
 | 
	
		
			
				|  |  | +	return a
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func (i *Issue) AfterDelete() {
 | 
	
		
			
				|  |  | +	_, err := DeleteAttachmentsByIssue(i.Id, true)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if err != nil {
 | 
	
		
			
				|  |  | +		log.Info("Could not delete files for issue #%d: %s", i.Id, err)
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  // CreateIssue creates new issue for repository.
 | 
	
		
			
				|  |  |  func NewIssue(issue *Issue) (err error) {
 | 
	
		
			
				|  |  |  	sess := x.NewSession()
 | 
	
	
		
			
				|  | @@ -871,17 +888,19 @@ type Comment struct {
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  // CreateComment creates comment of issue or commit.
 | 
	
		
			
				|  |  | -func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string) error {
 | 
	
		
			
				|  |  | +func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string, attachments []int64) (*Comment, error) {
 | 
	
		
			
				|  |  |  	sess := x.NewSession()
 | 
	
		
			
				|  |  |  	defer sess.Close()
 | 
	
		
			
				|  |  |  	if err := sess.Begin(); err != nil {
 | 
	
		
			
				|  |  | -		return err
 | 
	
		
			
				|  |  | +		return nil, err
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -	if _, err := sess.Insert(&Comment{PosterId: userId, Type: cmtType, IssueId: issueId,
 | 
	
		
			
				|  |  | -		CommitId: commitId, Line: line, Content: content}); err != nil {
 | 
	
		
			
				|  |  | +	comment := &Comment{PosterId: userId, Type: cmtType, IssueId: issueId,
 | 
	
		
			
				|  |  | +		CommitId: commitId, Line: line, Content: content}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if _, err := sess.Insert(comment); err != nil {
 | 
	
		
			
				|  |  |  		sess.Rollback()
 | 
	
		
			
				|  |  | -		return err
 | 
	
		
			
				|  |  | +		return nil, err
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  	// Check comment type.
 | 
	
	
		
			
				|  | @@ -890,22 +909,46 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType Commen
 | 
	
		
			
				|  |  |  		rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?"
 | 
	
		
			
				|  |  |  		if _, err := sess.Exec(rawSql, issueId); err != nil {
 | 
	
		
			
				|  |  |  			sess.Rollback()
 | 
	
		
			
				|  |  | -			return err
 | 
	
		
			
				|  |  | +			return nil, err
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		if len(attachments) > 0 {
 | 
	
		
			
				|  |  | +			rawSql = "UPDATE `attachment` SET comment_id = ? WHERE id IN (?)"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			astrs := make([]string, 0, len(attachments))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			for _, a := range attachments {
 | 
	
		
			
				|  |  | +				astrs = append(astrs, strconv.FormatInt(a, 10))
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			if _, err := sess.Exec(rawSql, comment.Id, strings.Join(astrs, ",")); err != nil {
 | 
	
		
			
				|  |  | +				sess.Rollback()
 | 
	
		
			
				|  |  | +				return nil, err
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  	case REOPEN:
 | 
	
		
			
				|  |  |  		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?"
 | 
	
		
			
				|  |  |  		if _, err := sess.Exec(rawSql, repoId); err != nil {
 | 
	
		
			
				|  |  |  			sess.Rollback()
 | 
	
		
			
				|  |  | -			return err
 | 
	
		
			
				|  |  | +			return nil, err
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  	case CLOSE:
 | 
	
		
			
				|  |  |  		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?"
 | 
	
		
			
				|  |  |  		if _, err := sess.Exec(rawSql, repoId); err != nil {
 | 
	
		
			
				|  |  |  			sess.Rollback()
 | 
	
		
			
				|  |  | -			return err
 | 
	
		
			
				|  |  | +			return nil, err
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	return sess.Commit()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return comment, sess.Commit()
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// GetCommentById returns the comment with the given id
 | 
	
		
			
				|  |  | +func GetCommentById(commentId int64) (*Comment, error) {
 | 
	
		
			
				|  |  | +	c := &Comment{Id: commentId}
 | 
	
		
			
				|  |  | +	_, err := x.Get(c)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return c, err
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (c *Comment) ContentHtml() template.HTML {
 | 
	
	
		
			
				|  | @@ -918,3 +961,127 @@ func GetIssueComments(issueId int64) ([]Comment, error) {
 | 
	
		
			
				|  |  |  	err := x.Asc("created").Find(&comments, &Comment{IssueId: issueId})
 | 
	
		
			
				|  |  |  	return comments, err
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// Attachments returns the attachments for this comment.
 | 
	
		
			
				|  |  | +func (c *Comment) Attachments() []*Attachment {
 | 
	
		
			
				|  |  | +	a, _ := GetAttachmentsByComment(c.Id)
 | 
	
		
			
				|  |  | +	return a
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +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)
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +type Attachment struct {
 | 
	
		
			
				|  |  | +	Id        int64
 | 
	
		
			
				|  |  | +	IssueId   int64
 | 
	
		
			
				|  |  | +	CommentId int64
 | 
	
		
			
				|  |  | +	Name      string
 | 
	
		
			
				|  |  | +	Path      string    `xorm:"TEXT"`
 | 
	
		
			
				|  |  | +	Created   time.Time `xorm:"CREATED"`
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// CreateAttachment creates a new attachment inside the database and
 | 
	
		
			
				|  |  | +func CreateAttachment(issueId, commentId int64, name, path string) (*Attachment, error) {
 | 
	
		
			
				|  |  | +	sess := x.NewSession()
 | 
	
		
			
				|  |  | +	defer sess.Close()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if err := sess.Begin(); err != nil {
 | 
	
		
			
				|  |  | +		return nil, err
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	a := &Attachment{IssueId: issueId, CommentId: commentId, Name: name, Path: path}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if _, err := sess.Insert(a); err != nil {
 | 
	
		
			
				|  |  | +		sess.Rollback()
 | 
	
		
			
				|  |  | +		return nil, err
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return a, sess.Commit()
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// Attachment returns the attachment by given ID.
 | 
	
		
			
				|  |  | +func GetAttachmentById(id int64) (*Attachment, error) {
 | 
	
		
			
				|  |  | +	m := &Attachment{Id: id}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	has, err := x.Get(m)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if err != nil {
 | 
	
		
			
				|  |  | +		return nil, err
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if !has {
 | 
	
		
			
				|  |  | +		return nil, ErrAttachmentNotExist
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return m, nil
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func GetAttachmentsForIssue(issueId int64) ([]*Attachment, error) {
 | 
	
		
			
				|  |  | +	attachments := make([]*Attachment, 0, 10)
 | 
	
		
			
				|  |  | +	err := x.Where("issue_id = ?", issueId).And("comment_id = 0").Find(&attachments)
 | 
	
		
			
				|  |  | +	return attachments, err
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// GetAttachmentsByIssue returns a list of attachments for the given issue
 | 
	
		
			
				|  |  | +func GetAttachmentsByIssue(issueId int64) ([]*Attachment, error) {
 | 
	
		
			
				|  |  | +	attachments := make([]*Attachment, 0, 10)
 | 
	
		
			
				|  |  | +	err := x.Where("issue_id = ?", issueId).And("comment_id > 0").Find(&attachments)
 | 
	
		
			
				|  |  | +	return attachments, err
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// GetAttachmentsByComment returns a list of attachments for the given comment
 | 
	
		
			
				|  |  | +func GetAttachmentsByComment(commentId int64) ([]*Attachment, error) {
 | 
	
		
			
				|  |  | +	attachments := make([]*Attachment, 0, 10)
 | 
	
		
			
				|  |  | +	err := x.Where("comment_id = ?", commentId).Find(&attachments)
 | 
	
		
			
				|  |  | +	return attachments, err
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// DeleteAttachment deletes the given attachment and optionally the associated file.
 | 
	
		
			
				|  |  | +func DeleteAttachment(a *Attachment, remove bool) error {
 | 
	
		
			
				|  |  | +	_, err := DeleteAttachments([]*Attachment{a}, remove)
 | 
	
		
			
				|  |  | +	return err
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// DeleteAttachments deletes the given attachments and optionally the associated files.
 | 
	
		
			
				|  |  | +func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
 | 
	
		
			
				|  |  | +	for i, a := range attachments {
 | 
	
		
			
				|  |  | +		if remove {
 | 
	
		
			
				|  |  | +			if err := os.Remove(a.Path); err != nil {
 | 
	
		
			
				|  |  | +				return i, err
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		if _, err := x.Delete(a.Id); err != nil {
 | 
	
		
			
				|  |  | +			return i, err
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return len(attachments), nil
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
 | 
	
		
			
				|  |  | +func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) {
 | 
	
		
			
				|  |  | +	attachments, err := GetAttachmentsByIssue(issueId)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if err != nil {
 | 
	
		
			
				|  |  | +		return 0, err
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return DeleteAttachments(attachments, remove)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// DeleteAttachmentsByComment deletes all attachments associated with the given comment.
 | 
	
		
			
				|  |  | +func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
 | 
	
		
			
				|  |  | +	attachments, err := GetAttachmentsByComment(commentId)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if err != nil {
 | 
	
		
			
				|  |  | +		return 0, err
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return DeleteAttachments(attachments, remove)
 | 
	
		
			
				|  |  | +}
 |