// 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 import ( "bytes" "errors" "html/template" "os" "strconv" "strings" "time" "github.com/Unknwon/com" "github.com/go-xorm/xorm" "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") 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. type Issue struct { Id int64 RepoId int64 `xorm:"INDEX"` Index int64 // Index in one repository. Name string Repo *Repository `xorm:"-"` PosterId int64 Poster *User `xorm:"-"` LabelIds string `xorm:"TEXT"` Labels []*Label `xorm:"-"` MilestoneId int64 AssigneeId int64 Assignee *User `xorm:"-"` IsRead bool `xorm:"-"` IsPull bool // Indicates whether is a pull request or not. IsClosed bool Content string `xorm:"TEXT"` RenderedContent string `xorm:"-"` Priority int NumComments int Deadline time.Time Created time.Time `xorm:"CREATED"` Updated time.Time `xorm:"UPDATED"` } func (i *Issue) GetPoster() (err error) { i.Poster, err = GetUserById(i.PosterId) if err == ErrUserNotExist { i.Poster = &User{Name: "FakeUser"} return nil } return err } func (i *Issue) GetLabels() error { if len(i.LabelIds) < 3 { return nil } strIds := strings.Split(strings.TrimSuffix(i.LabelIds[1:], "|"), "|$") i.Labels = make([]*Label, 0, len(strIds)) for _, strId := range strIds { id, _ := com.StrTo(strId).Int64() if id > 0 { l, err := GetLabelById(id) if err != nil { if err == ErrLabelNotExist { continue } return err } i.Labels = append(i.Labels, l) } } return nil } func (i *Issue) GetAssignee() (err error) { if i.AssigneeId == 0 { return nil } i.Assignee, err = GetUserById(i.AssigneeId) if err == ErrUserNotExist { return nil } 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() defer sess.Close() if err = sess.Begin(); err != nil { return err } if _, err = sess.Insert(issue); err != nil { sess.Rollback() return err } rawSql := "UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?" if _, err = sess.Exec(rawSql, issue.RepoId); err != nil { sess.Rollback() return err } if err = sess.Commit(); err != nil { return err } if issue.MilestoneId > 0 { // FIXES(280): Update milestone counter. return ChangeMilestoneAssign(0, issue.MilestoneId, issue) } return } // 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. func GetIssueByRef(ref string) (issue *Issue, err error) { var issueNumber int64 var repo *Repository n := strings.IndexByte(ref, byte('#')) if n == -1 { return nil, ErrMissingIssueNumber } if issueNumber, err = strconv.ParseInt(ref[n+1:], 10, 64); err != nil { return } if repo, err = GetRepositoryByRef(ref[:n]); err != nil { return } return GetIssueByIndex(repo.Id, issueNumber) } // GetIssueByIndex returns issue by given index in repository. func GetIssueByIndex(rid, index int64) (*Issue, error) { issue := &Issue{RepoId: rid, Index: index} has, err := x.Get(issue) if err != nil { return nil, err } else if !has { return nil, ErrIssueNotExist } return issue, nil } // GetIssueById returns an issue by ID. func GetIssueById(id int64) (*Issue, error) { issue := &Issue{Id: id} has, err := x.Get(issue) if err != nil { return nil, err } else if !has { return nil, ErrIssueNotExist } return issue, nil } // GetIssues returns a list of issues by given conditions. func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labelIds, sortType string) ([]Issue, error) { sess := x.Limit(20, (page-1)*20) if rid > 0 { sess.Where("repo_id=?", rid).And("is_closed=?", isClosed) } else { sess.Where("is_closed=?", isClosed) } if uid > 0 { sess.And("assignee_id=?", uid) } else if pid > 0 { sess.And("poster_id=?", pid) } if mid > 0 { sess.And("milestone_id=?", mid) } if len(labelIds) > 0 { for _, label := range strings.Split(labelIds, ",") { // Prevent SQL inject. if com.StrTo(label).MustInt() > 0 { sess.And("label_ids like '%$" + label + "|%'") } } } switch sortType { case "oldest": sess.Asc("created") case "recentupdate": sess.Desc("updated") case "leastupdate": sess.Asc("updated") case "mostcomment": sess.Desc("num_comments") case "leastcomment": sess.Asc("num_comments") case "priority": sess.Desc("priority") default: sess.Desc("created") } var issues []Issue err := sess.Find(&issues) return issues, err } type IssueStatus int const ( IS_OPEN = iota + 1 IS_CLOSE ) // GetIssuesByLabel returns a list of issues by given label and repository. func GetIssuesByLabel(repoId int64, label string) ([]*Issue, error) { issues := make([]*Issue, 0, 10) err := x.Where("repo_id=?", repoId).And("label_ids like '%$" + label + "|%'").Find(&issues) return issues, err } // GetIssueCountByPoster returns number of issues of repository by poster. func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 { count, _ := x.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue)) return count } // .___ ____ ___ // | | ______ ________ __ ____ | | \______ ___________ // | |/ ___// ___/ | \_/ __ \| | / ___// __ \_ __ \ // | |\___ \ \___ \| | /\ ___/| | /\___ \\ ___/| | \/ // |___/____ >____ >____/ \___ >______//____ >\___ >__| // \/ \/ \/ \/ \/ // IssueUser represents an issue-user relation. type IssueUser struct { Id int64 Uid int64 `xorm:"INDEX"` // User ID. IssueId int64 RepoId int64 `xorm:"INDEX"` MilestoneId int64 IsRead bool IsAssigned bool IsMentioned bool IsPoster bool IsClosed bool } // NewIssueUserPairs adds new issue-user pairs for new issue of repository. func NewIssueUserPairs(rid, iid, oid, pid, aid int64, repoName string) (err error) { iu := &IssueUser{IssueId: iid, RepoId: rid} us, err := GetCollaborators(repoName) if err != nil { return err } isNeedAddPoster := true for _, u := range us { iu.Uid = u.Id iu.IsPoster = iu.Uid == pid if isNeedAddPoster && iu.IsPoster { isNeedAddPoster = false } iu.IsAssigned = iu.Uid == aid if _, err = x.Insert(iu); err != nil { return err } } if isNeedAddPoster { iu.Uid = pid iu.IsPoster = true iu.IsAssigned = iu.Uid == aid if _, err = x.Insert(iu); err != nil { return err } } return nil } // PairsContains returns true when pairs list contains given issue. func PairsContains(ius []*IssueUser, issueId int64) int { for i := range ius { if ius[i].IssueId == issueId { return i } } return -1 } // GetIssueUserPairs returns issue-user pairs by given repository and user. func GetIssueUserPairs(rid, uid int64, isClosed bool) ([]*IssueUser, error) { ius := make([]*IssueUser, 0, 10) err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoId: rid, Uid: uid}) return ius, err } // GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs. func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) { if len(rids) == 0 { return []*IssueUser{}, nil } buf := bytes.NewBufferString("") for _, rid := range rids { buf.WriteString("repo_id=") buf.WriteString(com.ToStr(rid)) buf.WriteString(" OR ") } cond := strings.TrimSuffix(buf.String(), " OR ") ius := make([]*IssueUser, 0, 10) sess := x.Limit(20, (page-1)*20).Where("is_closed=?", isClosed) if len(cond) > 0 { sess.And(cond) } err := sess.Find(&ius) return ius, err } // GetIssueUserPairsByMode returns issue-user pairs by given repository and user. func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int) ([]*IssueUser, error) { ius := make([]*IssueUser, 0, 10) sess := x.Limit(20, (page-1)*20).Where("uid=?", uid).And("is_closed=?", isClosed) if rid > 0 { sess.And("repo_id=?", rid) } switch filterMode { case FM_ASSIGN: sess.And("is_assigned=?", true) case FM_CREATE: sess.And("is_poster=?", true) default: return ius, nil } err := sess.Find(&ius) return ius, err } // IssueStats represents issue statistic information. type IssueStats struct { OpenCount, ClosedCount int64 AllCount int64 AssignCount int64 CreateCount int64 MentionCount int64 } // Filter modes. const ( FM_ASSIGN = iota + 1 FM_CREATE FM_MENTION ) // GetIssueStats returns issue statistic information by given conditions. func GetIssueStats(rid, uid int64, isShowClosed bool, filterMode int) *IssueStats { stats := &IssueStats{} issue := new(Issue) tmpSess := &xorm.Session{} sess := x.Where("repo_id=?", rid) *tmpSess = *sess stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue) *tmpSess = *sess stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue) if isShowClosed { stats.AllCount = stats.ClosedCount } else { stats.AllCount = stats.OpenCount } if filterMode != FM_MENTION { sess = x.Where("repo_id=?", rid) switch filterMode { case FM_ASSIGN: sess.And("assignee_id=?", uid) case FM_CREATE: sess.And("poster_id=?", uid) default: goto nofilter } *tmpSess = *sess stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue) *tmpSess = *sess stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue) } else { sess := x.Where("repo_id=?", rid).And("uid=?", uid).And("is_mentioned=?", true) *tmpSess = *sess stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(new(IssueUser)) *tmpSess = *sess stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(new(IssueUser)) } nofilter: stats.AssignCount, _ = x.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("assignee_id=?", uid).Count(issue) stats.CreateCount, _ = x.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("poster_id=?", uid).Count(issue) stats.MentionCount, _ = x.Where("repo_id=?", rid).And("uid=?", uid).And("is_closed=?", isShowClosed).And("is_mentioned=?", true).Count(new(IssueUser)) return stats } // GetUserIssueStats returns issue statistic information for dashboard by given conditions. func GetUserIssueStats(uid int64, filterMode int) *IssueStats { stats := &IssueStats{} issue := new(Issue) stats.AssignCount, _ = x.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue) stats.CreateCount, _ = x.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue) return stats } // UpdateIssue updates information of issue. func UpdateIssue(issue *Issue) error { _, err := x.Id(issue.Id).AllCols().Update(issue) if err != nil { return err } return err } // UpdateIssueUserByStatus updates issue-user pairs by issue status. func UpdateIssueUserPairsByStatus(iid int64, isClosed bool) error { rawSql := "UPDATE `issue_user` SET is_closed = ? WHERE issue_id = ?" _, err := x.Exec(rawSql, isClosed, iid) return err } // UpdateIssueUserPairByAssignee updates issue-user pair for assigning. func UpdateIssueUserPairByAssignee(aid, iid int64) error { rawSql := "UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?" if _, err := x.Exec(rawSql, false, iid); err != nil { return err } // Assignee ID equals to 0 means clear assignee. if aid == 0 { return nil } rawSql = "UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?" _, err := x.Exec(rawSql, true, aid, iid) return err } // UpdateIssueUserPairByRead updates issue-user pair for reading. func UpdateIssueUserPairByRead(uid, iid int64) error { rawSql := "UPDATE `issue_user` SET is_read = ? WHERE uid = ? AND issue_id = ?" _, err := x.Exec(rawSql, true, uid, iid) return err } // UpdateIssueUserPairsByMentions updates issue-user pairs by mentioning. func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error { for _, uid := range uids { iu := &IssueUser{Uid: uid, IssueId: iid} has, err := x.Get(iu) if err != nil { return err } iu.IsMentioned = true if has { _, err = x.Id(iu.Id).AllCols().Update(iu) } else { _, err = x.Insert(iu) } if err != nil { return err } } return nil } // .____ ___. .__ // | | _____ \_ |__ ____ | | // | | \__ \ | __ \_/ __ \| | // | |___ / __ \| \_\ \ ___/| |__ // |_______ (____ /___ /\___ >____/ // \/ \/ \/ \/ // Label represents a label of repository for issues. type Label struct { Id int64 RepoId int64 `xorm:"INDEX"` Name string Color string `xorm:"VARCHAR(7)"` NumIssues int NumClosedIssues int NumOpenIssues int `xorm:"-"` IsChecked bool `xorm:"-"` } // CalOpenIssues calculates the open issues of label. func (m *Label) CalOpenIssues() { m.NumOpenIssues = m.NumIssues - m.NumClosedIssues } // NewLabel creates new label of repository. func NewLabel(l *Label) error { _, err := x.Insert(l) return err } // GetLabelById returns a label by given ID. func GetLabelById(id int64) (*Label, error) { if id <= 0 { return nil, ErrLabelNotExist } l := &Label{Id: id} has, err := x.Get(l) if err != nil { return nil, err } else if !has { return nil, ErrLabelNotExist } return l, nil } // GetLabels returns a list of labels of given repository ID. func GetLabels(repoId int64) ([]*Label, error) { labels := make([]*Label, 0, 10) err := x.Where("repo_id=?", repoId).Find(&labels) return labels, err } // UpdateLabel updates label information. func UpdateLabel(l *Label) error { _, err := x.Id(l.Id).Update(l) return err } // DeleteLabel delete a label of given repository. func DeleteLabel(repoId int64, strId string) error { id, _ := com.StrTo(strId).Int64() l, err := GetLabelById(id) if err != nil { if err == ErrLabelNotExist { return nil } return err } issues, err := GetIssuesByLabel(repoId, strId) if err != nil { return err } sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err } for _, issue := range issues { issue.LabelIds = strings.Replace(issue.LabelIds, "$"+strId+"|", "", -1) if _, err = sess.Id(issue.Id).AllCols().Update(issue); err != nil { sess.Rollback() return err } } if _, err = sess.Delete(l); err != nil { sess.Rollback() return err } return sess.Commit() } // _____ .__.__ __ // / \ |__| | ____ _______/ |_ ____ ____ ____ // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ // / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/ // \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ > // \/ \/ \/ \/ \/ // Milestone represents a milestone of repository. type Milestone struct { Id int64 RepoId int64 `xorm:"INDEX"` Index int64 Name string Content string `xorm:"TEXT"` RenderedContent string `xorm:"-"` IsClosed bool NumIssues int NumClosedIssues int NumOpenIssues int `xorm:"-"` Completeness int // Percentage(1-100). Deadline time.Time DeadlineString string `xorm:"-"` ClosedDate time.Time } // CalOpenIssues calculates the open issues of milestone. func (m *Milestone) CalOpenIssues() { m.NumOpenIssues = m.NumIssues - m.NumClosedIssues } // NewMilestone creates new milestone of repository. func NewMilestone(m *Milestone) (err error) { sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err } if _, err = sess.Insert(m); err != nil { sess.Rollback() return err } rawSql := "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?" if _, err = sess.Exec(rawSql, m.RepoId); err != nil { sess.Rollback() return err } return sess.Commit() } // GetMilestoneById returns the milestone by given ID. func GetMilestoneById(id int64) (*Milestone, error) { m := &Milestone{Id: id} has, err := x.Get(m) if err != nil { return nil, err } else if !has { return nil, ErrMilestoneNotExist } return m, nil } // GetMilestoneByIndex returns the milestone of given repository and index. func GetMilestoneByIndex(repoId, idx int64) (*Milestone, error) { m := &Milestone{RepoId: repoId, Index: idx} has, err := x.Get(m) if err != nil { return nil, err } else if !has { return nil, ErrMilestoneNotExist } return m, nil } // GetMilestones returns a list of milestones of given repository and status. func GetMilestones(repoId int64, isClosed bool) ([]*Milestone, error) { miles := make([]*Milestone, 0, 10) err := x.Where("repo_id=?", repoId).And("is_closed=?", isClosed).Find(&miles) return miles, err } // UpdateMilestone updates information of given milestone. func UpdateMilestone(m *Milestone) error { _, err := x.Id(m.Id).Update(m) return err } // ChangeMilestoneStatus changes the milestone open/closed status. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { repo, err := GetRepositoryById(m.RepoId) if err != nil { return err } sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err } m.IsClosed = isClosed if _, err = sess.Id(m.Id).AllCols().Update(m); err != nil { sess.Rollback() return err } if isClosed { repo.NumClosedMilestones++ } else { repo.NumClosedMilestones-- } if _, err = sess.Id(repo.Id).Update(repo); err != nil { sess.Rollback() return err } return sess.Commit() } // ChangeMilestoneIssueStats updates the open/closed issues counter and progress for the // milestone associated witht the given issue. func ChangeMilestoneIssueStats(issue *Issue) error { if issue.MilestoneId == 0 { return nil } m, err := GetMilestoneById(issue.MilestoneId) if err != nil { return err } if issue.IsClosed { m.NumOpenIssues-- m.NumClosedIssues++ } else { m.NumOpenIssues++ m.NumClosedIssues-- } m.Completeness = m.NumClosedIssues * 100 / m.NumIssues return UpdateMilestone(m) } // ChangeMilestoneAssign changes assignment of milestone for issue. func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err } if oldMid > 0 { m, err := GetMilestoneById(oldMid) if err != nil { return err } m.NumIssues-- if issue.IsClosed { m.NumClosedIssues-- } if m.NumIssues > 0 { m.Completeness = m.NumClosedIssues * 100 / m.NumIssues } else { m.Completeness = 0 } if _, err = sess.Id(m.Id).Cols("num_issues,num_completeness,num_closed_issues").Update(m); err != nil { sess.Rollback() return err } rawSql := "UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?" if _, err = sess.Exec(rawSql, issue.Id); err != nil { sess.Rollback() return err } } if mid > 0 { m, err := GetMilestoneById(mid) if err != nil { return err } m.NumIssues++ if issue.IsClosed { m.NumClosedIssues++ } if m.NumIssues == 0 { return ErrWrongIssueCounter } m.Completeness = m.NumClosedIssues * 100 / m.NumIssues if _, err = sess.Id(m.Id).Cols("num_issues,num_completeness,num_closed_issues").Update(m); err != nil { sess.Rollback() return err } rawSql := "UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?" if _, err = sess.Exec(rawSql, m.Id, issue.Id); err != nil { sess.Rollback() return err } } return sess.Commit() } // DeleteMilestone deletes a milestone. func DeleteMilestone(m *Milestone) (err error) { sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err } if _, err = sess.Delete(m); err != nil { sess.Rollback() return err } rawSql := "UPDATE `repository` SET num_milestones = num_milestones - 1 WHERE id = ?" if _, err = sess.Exec(rawSql, m.RepoId); err != nil { sess.Rollback() return err } rawSql = "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?" if _, err = sess.Exec(rawSql, m.Id); err != nil { sess.Rollback() return err } rawSql = "UPDATE `issue_user` SET milestone_id = 0 WHERE milestone_id = ?" if _, err = sess.Exec(rawSql, m.Id); err != nil { sess.Rollback() return err } return sess.Commit() } // _________ __ // \_ ___ \ ____ _____ _____ ____ _____/ |_ // / \ \/ / _ \ / \ / \_/ __ \ / \ __\ // \ \___( <_> ) Y Y \ Y Y \ ___/| | \ | // \______ /\____/|__|_| /__|_| /\___ >___| /__| // \/ \/ \/ \/ \/ // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. type CommentType int const ( // Plain comment, can be associated with a commit (CommitId > 0) and a line (Line > 0) COMMENT_TYPE_COMMENT CommentType = iota COMMENT_TYPE_REOPEN COMMENT_TYPE_CLOSE // References. COMMENT_TYPE_ISSUE // Reference from some commit (not part of a pull request) COMMENT_TYPE_COMMIT // Reference from some pull request COMMENT_TYPE_PULL ) // Comment represents a comment in commit and issue page. type Comment struct { Id int64 Type CommentType PosterId int64 Poster *User `xorm:"-"` IssueId int64 CommitId int64 Line int64 Content string `xorm:"TEXT"` Created time.Time `xorm:"CREATED"` } // CreateComment creates comment of issue or commit. 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 nil, err } comment := &Comment{PosterId: userId, Type: cmtType, IssueId: issueId, CommitId: commitId, Line: line, Content: content} if _, err := sess.Insert(comment); err != nil { sess.Rollback() return nil, err } // Check comment type. switch cmtType { case COMMENT_TYPE_COMMENT: rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?" if _, err := sess.Exec(rawSql, issueId); err != nil { sess.Rollback() 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 COMMENT_TYPE_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 nil, err } case COMMENT_TYPE_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 nil, err } } 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 { return template.HTML(c.Content) } // GetIssueComments returns list of comment by given issue id. func GetIssueComments(issueId int64) ([]Comment, error) { comments := make([]Comment, 0, 10) 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) }