diff options
Diffstat (limited to 'models/issue.go')
-rw-r--r-- | models/issue.go | 666 |
1 files changed, 444 insertions, 222 deletions
diff --git a/models/issue.go b/models/issue.go index d423f056..37fc125c 100644 --- a/models/issue.go +++ b/models/issue.go @@ -7,8 +7,12 @@ package models import ( "bytes" "errors" + "fmt" "html/template" + "io" + "mime/multipart" "os" + "path" "strconv" "strings" "time" @@ -16,15 +20,15 @@ import ( "github.com/Unknwon/com" "github.com/go-xorm/xorm" + "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/setting" + gouuid "github.com/gogits/gogs/modules/uuid" ) var ( ErrIssueNotExist = errors.New("Issue does not exist") - ErrLabelNotExist = errors.New("Label 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") ) @@ -38,7 +42,6 @@ type Issue struct { Repo *Repository `xorm:"-"` PosterID int64 Poster *User `xorm:"-"` - LabelIds string `xorm:"TEXT"` Labels []*Label `xorm:"-"` MilestoneID int64 Milestone *Milestone `xorm:"-"` @@ -60,15 +63,28 @@ func (i *Issue) AfterSet(colName string, _ xorm.Cell) { var err error switch colName { case "milestone_id": + if i.MilestoneID == 0 { + return + } + i.Milestone, err = GetMilestoneByID(i.MilestoneID) if err != nil { log.Error(3, "GetMilestoneById: %v", err) } + case "assignee_id": + if i.AssigneeID == 0 { + return + } + + i.Assignee, err = GetUserByID(i.AssigneeID) + if err != nil { + log.Error(3, "GetUserByID: %v", err) + } } } func (i *Issue) GetPoster() (err error) { - i.Poster, err = GetUserById(i.PosterID) + i.Poster, err = GetUserByID(i.PosterID) if IsErrUserNotExist(err) { i.Poster = &User{Name: "FakeUser"} return nil @@ -76,35 +92,56 @@ func (i *Issue) GetPoster() (err error) { return err } -func (i *Issue) GetLabels() error { - if len(i.LabelIds) < 3 { +func (i *Issue) hasLabel(e Engine, labelID int64) bool { + return hasIssueLabel(e, i.ID, labelID) +} + +// HasLabel returns true if issue has been labeled by given ID. +func (i *Issue) HasLabel(labelID int64) bool { + return i.hasLabel(x, labelID) +} + +func (i *Issue) addLabel(e Engine, labelID int64) error { + return newIssueLabel(e, i.ID, labelID) +} + +// AddLabel adds new label to issue by given ID. +func (i *Issue) AddLabel(labelID int64) error { + return i.addLabel(x, labelID) +} + +func (i *Issue) getLabels(e Engine) (err error) { + if len(i.Labels) > 0 { 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).MustInt64() - if id > 0 { - l, err := GetLabelById(id) - if err != nil { - if err == ErrLabelNotExist { - continue - } - return err - } - i.Labels = append(i.Labels, l) - } + i.Labels, err = getLabelsByIssueID(e, i.ID) + if err != nil { + return fmt.Errorf("getLabelsByIssueID: %v", err) } return nil } +// GetLabels retrieves all labels of issue and assign to corresponding field. +func (i *Issue) GetLabels() error { + return i.getLabels(x) +} + +func (i *Issue) removeLabel(e Engine, labelID int64) error { + return deleteIssueLabel(e, i.ID, labelID) +} + +// RemoveLabel removes a label from issue by given ID. +func (i *Issue) RemoveLabel(labelID int64) error { + return i.removeLabel(x, labelID) +} + func (i *Issue) GetAssignee() (err error) { - if i.AssigneeID == 0 { + if i.AssigneeID == 0 || i.Assignee != nil { return nil } - i.Assignee, err = GetUserById(i.AssigneeID) + i.Assignee, err = GetUserByID(i.AssigneeID) if IsErrUserNotExist(err) { return nil } @@ -124,8 +161,21 @@ func (i *Issue) AfterDelete() { } } -// CreateIssue creates new issue for repository. -func NewIssue(issue *Issue) (err error) { +// CreateIssue creates new issue with labels for repository. +func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { + // Check attachments. + attachments := make([]*Attachment, 0, len(uuids)) + for _, uuid := range uuids { + attach, err := GetAttachmentByUUID(uuid) + if err != nil { + if IsErrAttachmentNotExist(err) { + continue + } + return fmt.Errorf("GetAttachmentByUUID[%s]: %v", uuid, err) + } + attachments = append(attachments, attach) + } + sess := x.NewSession() defer sessionRelease(sess) if err = sess.Begin(); err != nil { @@ -134,20 +184,51 @@ func NewIssue(issue *Issue) (err error) { if _, err = sess.Insert(issue); err != nil { return err - } else if _, err = sess.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", issue.RepoID); err != nil { + } else if _, err = sess.Exec("UPDATE `repository` SET num_issues=num_issues+1 WHERE id=?", issue.RepoID); err != nil { return err } - if err = sess.Commit(); err != nil { - return err + for _, id := range labelIDs { + if err = issue.addLabel(sess, id); err != nil { + return fmt.Errorf("addLabel: %v", err) + } } if issue.MilestoneID > 0 { - // FIXES(280): Update milestone counter. - return ChangeMilestoneAssign(0, issue.MilestoneID, issue) + if err = changeMilestoneAssign(sess, 0, issue); err != nil { + return err + } } - return + if err = newIssueUsers(sess, repo, issue); err != nil { + return err + } + + for i := range attachments { + attachments[i].IssueID = issue.ID + // No assign value could be 0, so ignore AllCols(). + if _, err = sess.Id(attachments[i].ID).Update(attachments[i]); err != nil { + return fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err) + } + } + + // Notify watchers. + act := &Action{ + ActUserID: issue.Poster.Id, + ActUserName: issue.Poster.Name, + ActEmail: issue.Poster.Email, + OpType: CREATE_ISSUE, + Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name), + RepoID: repo.ID, + RepoUserName: repo.Owner.Name, + RepoName: repo.Name, + IsPrivate: repo.IsPrivate, + } + if err = notifyWatchers(sess, act); err != nil { + return err + } + + return sess.Commit() } // GetIssueByRef returns an Issue specified by a GFM reference. @@ -170,7 +251,7 @@ func GetIssueByRef(ref string) (issue *Issue, err error) { return } - return GetIssueByIndex(repo.Id, issueNumber) + return GetIssueByIndex(repo.ID, issueNumber) } // GetIssueByIndex returns issue by given index in repository. @@ -198,7 +279,7 @@ func GetIssueById(id int64) (*Issue, error) { } // Issues returns a list of issues by given conditions. -func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isClosed, isMention bool, labelIds, sortType string) ([]*Issue, error) { +func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isClosed, isMention bool, labels, sortType string) ([]*Issue, error) { sess := x.Limit(setting.IssuePagingNum, (page-1)*setting.IssuePagingNum) if repoID > 0 { @@ -217,14 +298,6 @@ func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isCl sess.And("issue.milestone_id=?", milestoneID) } - if len(labelIds) > 0 { - for _, label := range strings.Split(labelIds, ",") { - if com.StrTo(label).MustInt() > 0 { - sess.And("label_ids like ?", "%$"+label+"|%") - } - } - } - switch sortType { case "oldest": sess.Asc("created") @@ -242,10 +315,26 @@ func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isCl sess.Desc("created") } + labelIDs := base.StringsToInt64s(strings.Split(labels, ",")) + if len(labelIDs) > 0 { + validJoin := false + queryStr := "issue.id=issue_label.issue_id" + for _, id := range labelIDs { + if id == 0 { + continue + } + validJoin = true + queryStr += " AND issue_label.label_id=" + com.ToStr(id) + } + if validJoin { + sess.Join("INNER", "issue_label", queryStr) + } + } + if isMention { - queryStr := "issue.id = issue_user.issue_id AND issue_user.is_mentioned=1" + queryStr := "issue.id=issue_user.issue_id AND issue_user.is_mentioned=1" if uid > 0 { - queryStr += " AND issue_user.uid = " + com.ToStr(uid) + queryStr += " AND issue_user.uid=" + com.ToStr(uid) } sess.Join("INNER", "issue_user", queryStr) } @@ -261,12 +350,6 @@ const ( IS_CLOSE ) -// GetIssuesByLabel returns a list of issues by given label and repository. -func GetIssuesByLabel(repoID, labelID int64) ([]*Issue, error) { - issues := make([]*Issue, 0, 10) - return issues, x.Where("repo_id=?", repoID).And("label_ids like '%$" + com.ToStr(labelID) + "|%'").Find(&issues) -} - // 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)) @@ -282,11 +365,11 @@ func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 { // 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 + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"uid INDEX"` // User ID. + IssueID int64 + RepoID int64 `xorm:"INDEX"` + MilestoneID int64 IsRead bool IsAssigned bool IsMentioned bool @@ -294,70 +377,72 @@ type IssueUser struct { IsClosed bool } -// FIXME: organization -// NewIssueUserPairs adds new issue-user pairs for new issue of repository. -func NewIssueUserPairs(repo *Repository, issueID, orgID, posterID, assigneeID int64) error { - users, err := repo.GetCollaborators() +func newIssueUsers(e *xorm.Session, repo *Repository, issue *Issue) error { + users, err := repo.GetAssignees() if err != nil { return err } iu := &IssueUser{ - IssueId: issueID, - RepoId: repo.Id, + IssueID: issue.ID, + RepoID: repo.ID, } + // Poster can be anyone. isNeedAddPoster := true for _, u := range users { - iu.Id = 0 - iu.Uid = u.Id - iu.IsPoster = iu.Uid == posterID + iu.ID = 0 + iu.UID = u.Id + iu.IsPoster = iu.UID == issue.PosterID if isNeedAddPoster && iu.IsPoster { isNeedAddPoster = false } - iu.IsAssigned = iu.Uid == assigneeID - if _, err = x.Insert(iu); err != nil { + iu.IsAssigned = iu.UID == issue.AssigneeID + if _, err = e.Insert(iu); err != nil { return err } } if isNeedAddPoster { - iu.Id = 0 - iu.Uid = posterID + iu.ID = 0 + iu.UID = issue.PosterID iu.IsPoster = true - iu.IsAssigned = iu.Uid == assigneeID - if _, err = x.Insert(iu); err != nil { + if _, err = e.Insert(iu); err != nil { return err } } + return nil +} - // Add owner's as well. - if repo.OwnerId != posterID { - iu.Id = 0 - iu.Uid = repo.OwnerId - iu.IsAssigned = iu.Uid == assigneeID - if _, err = x.Insert(iu); err != nil { - return err - } +// NewIssueUsers adds new issue-user relations for new issue of repository. +func NewIssueUsers(repo *Repository, issue *Issue) (err error) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err } - return nil + if err = newIssueUsers(sess, repo, issue); err != nil { + return err + } + + return sess.Commit() } // PairsContains returns true when pairs list contains given issue. func PairsContains(ius []*IssueUser, issueId, uid int64) int { for i := range ius { - if ius[i].IssueId == issueId && - ius[i].Uid == uid { + if ius[i].IssueID == issueId && + ius[i].UID == uid { return i } } return -1 } -// GetIssueUserPairs returns issue-user pairs by given repository and user. -func GetIssueUserPairs(rid, uid int64, isClosed bool) ([]*IssueUser, error) { +// GetIssueUsers returns issue-user pairs by given repository and user. +func GetIssueUsers(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}) + err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoID: rid, UID: uid}) return ius, err } @@ -420,50 +505,58 @@ const ( FM_MENTION ) +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 +} + // GetIssueStats returns issue statistic information by given conditions. func GetIssueStats(repoID, uid, labelID, milestoneID int64, isShowClosed bool, filterMode int) *IssueStats { stats := &IssueStats{} - issue := new(Issue) + // issue := new(Issue) - queryStr := "issue.repo_id=? AND issue.is_closed=?" + queryStr := "SELECT COUNT(*) FROM `issue` " if labelID > 0 { - queryStr += " AND issue.label_ids like '%$" + com.ToStr(labelID) + "|%'" + queryStr += "INNER JOIN `issue_label` ON `issue`.id=`issue_label`.issue_id AND `issue_label`.label_id=" + com.ToStr(labelID) } + + baseCond := " WHERE issue.repo_id=? AND issue.is_closed=?" if milestoneID > 0 { - queryStr += " AND milestone_id=" + com.ToStr(milestoneID) + baseCond += " AND issue.milestone_id=" + com.ToStr(milestoneID) } switch filterMode { case FM_ALL: - stats.OpenCount, _ = x.Where(queryStr, repoID, false).Count(issue) - stats.ClosedCount, _ = x.Where(queryStr, repoID, true).Count(issue) - return stats + resutls, _ := x.Query(queryStr+baseCond, repoID, false) + stats.OpenCount = parseCountResult(resutls) + resutls, _ = x.Query(queryStr+baseCond, repoID, true) + stats.ClosedCount = parseCountResult(resutls) case FM_ASSIGN: - queryStr += " AND assignee_id=?" - stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid).Count(issue) - stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid).Count(issue) - return stats + baseCond += " AND assignee_id=?" + resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid) + stats.OpenCount = parseCountResult(resutls) + resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid) + stats.ClosedCount = parseCountResult(resutls) case FM_CREATE: - queryStr += " AND poster_id=?" - stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid).Count(issue) - stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid).Count(issue) - return stats + baseCond += " AND poster_id=?" + resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid) + stats.OpenCount = parseCountResult(resutls) + resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid) + stats.ClosedCount = parseCountResult(resutls) case FM_MENTION: - queryStr += " AND uid=? AND is_mentioned=?" - if labelID > 0 { - stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid, true). - Join("INNER", "issue", "issue.id = issue_id").Count(new(IssueUser)) - stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid, true). - Join("INNER", "issue", "issue.id = issue_id").Count(new(IssueUser)) - return stats - } - - queryStr = strings.Replace(queryStr, "issue.", "", 2) - stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid, true).Count(new(IssueUser)) - stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid, true).Count(new(IssueUser)) - return stats + queryStr += " INNER JOIN `issue_user` ON `issue`.id=`issue_user`.issue_id" + baseCond += " AND `issue_user`.uid=? AND `issue_user`.is_mentioned=?" + resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid, true) + stats.OpenCount = parseCountResult(resutls) + resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid, true) + stats.ClosedCount = parseCountResult(resutls) } return stats } @@ -477,15 +570,14 @@ func GetUserIssueStats(uid int64, filterMode int) *IssueStats { return stats } +func updateIssue(e Engine, issue *Issue) error { + _, err := e.Id(issue.ID).AllCols().Update(issue) + return err +} + // 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 + return updateIssue(x, issue) } // UpdateIssueUserByStatus updates issue-user pairs by issue status. @@ -495,22 +587,34 @@ func UpdateIssueUserPairsByStatus(iid int64, isClosed bool) error { 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 { +func updateIssueUserByAssignee(e *xorm.Session, issueID, assigneeID int64) (err error) { + if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE issue_id=?", false, issueID); err != nil { return err } // Assignee ID equals to 0 means clear assignee. - if aid == 0 { + if assigneeID == 0 { return nil } - rawSql = "UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?" - _, err := x.Exec(rawSql, true, aid, iid) + _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE uid=? AND issue_id=?", true, assigneeID, issueID) return err } +// UpdateIssueUserByAssignee updates issue-user relation for assignee. +func UpdateIssueUserByAssignee(issueID, assigneeID int64) (err error) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if err = updateIssueUserByAssignee(sess, issueID, assigneeID); err != nil { + return err + } + + return sess.Commit() +} + // 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 = ?" @@ -518,10 +622,10 @@ func UpdateIssueUserPairByRead(uid, iid int64) error { return err } -// UpdateIssueUserPairsByMentions updates issue-user pairs by mentioning. -func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error { +// UpdateIssueUsersByMentions updates issue-user pairs by mentioning. +func UpdateIssueUsersByMentions(uids []int64, iid int64) error { for _, uid := range uids { - iu := &IssueUser{Uid: uid, IssueId: iid} + iu := &IssueUser{UID: uid, IssueID: iid} has, err := x.Get(iu) if err != nil { return err @@ -529,7 +633,7 @@ func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error { iu.IsMentioned = true if has { - _, err = x.Id(iu.Id).AllCols().Update(iu) + _, err = x.Id(iu.ID).AllCols().Update(iu) } else { _, err = x.Insert(iu) } @@ -550,7 +654,7 @@ func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error { // Label represents a label of repository for issues. type Label struct { ID int64 `xorm:"pk autoincr"` - RepoId int64 `xorm:"INDEX"` + RepoID int64 `xorm:"INDEX"` Name string Color string `xorm:"VARCHAR(7)"` NumIssues int @@ -570,10 +674,9 @@ func NewLabel(l *Label) error { return err } -// GetLabelById returns a label by given ID. -func GetLabelById(id int64) (*Label, error) { +func getLabelByID(e Engine, id int64) (*Label, error) { if id <= 0 { - return nil, ErrLabelNotExist + return nil, ErrLabelNotExist{id} } l := &Label{ID: id} @@ -581,16 +684,43 @@ func GetLabelById(id int64) (*Label, error) { if err != nil { return nil, err } else if !has { - return nil, ErrLabelNotExist + return nil, ErrLabelNotExist{l.ID} } return l, nil } -// GetLabels returns a list of labels of given repository ID. -func GetLabels(repoId int64) ([]*Label, error) { +// GetLabelByID returns a label by given ID. +func GetLabelByID(id int64) (*Label, error) { + return getLabelByID(x, id) +} + +// GetLabelsByRepoID returns all labels that belong to given repository by ID. +func GetLabelsByRepoID(repoID int64) ([]*Label, error) { labels := make([]*Label, 0, 10) - err := x.Where("repo_id=?", repoId).Find(&labels) - return labels, err + return labels, x.Where("repo_id=?", repoID).Find(&labels) +} + +func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) { + issueLabels, err := getIssueLabels(e, issueID) + if err != nil { + return nil, fmt.Errorf("getIssueLabels: %v", err) + } + + var label *Label + labels := make([]*Label, 0, len(issueLabels)) + for idx := range issueLabels { + label, err = getLabelByID(e, issueLabels[idx].LabelID) + if err != nil && !IsErrLabelNotExist(err) { + return nil, fmt.Errorf("getLabelByID: %v", err) + } + labels = append(labels, label) + } + return labels, nil +} + +// GetLabelsByIssueID returns all labels that belong to given issue by ID. +func GetLabelsByIssueID(issueID int64) ([]*Label, error) { + return getLabelsByIssueID(x, issueID) } // UpdateLabel updates label information. @@ -601,38 +731,92 @@ func UpdateLabel(l *Label) error { // DeleteLabel delete a label of given repository. func DeleteLabel(repoID, labelID int64) error { - l, err := GetLabelById(labelID) + l, err := GetLabelByID(labelID) if err != nil { - if err == ErrLabelNotExist { + if IsErrLabelNotExist(err) { return nil } return err } - issues, err := GetIssuesByLabel(repoID, labelID) - if err != nil { - return err - } - sess := x.NewSession() defer sessionRelease(sess) if err = sess.Begin(); err != nil { return err } - for _, issue := range issues { - issue.LabelIds = strings.Replace(issue.LabelIds, "$"+com.ToStr(labelID)+"|", "", -1) - if _, err = sess.Id(issue.ID).AllCols().Update(issue); err != nil { - return err - } - } - - if _, err = sess.Delete(l); err != nil { + if _, err = x.Where("label_id=?", labelID).Delete(new(IssueLabel)); err != nil { + return err + } else if _, err = sess.Delete(l); err != nil { return err } return sess.Commit() } +// .___ .____ ___. .__ +// | | ______ ________ __ ____ | | _____ \_ |__ ____ | | +// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| | +// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__ +// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/ +// \/ \/ \/ \/ \/ \/ \/ + +// IssueLabel represetns an issue-lable relation. +type IssueLabel struct { + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"UNIQUE(s)"` + LabelID int64 `xorm:"UNIQUE(s)"` +} + +func hasIssueLabel(e Engine, issueID, labelID int64) bool { + has, _ := e.Where("issue_id=? AND label_id=?", issueID, labelID).Get(new(IssueLabel)) + return has +} + +// HasIssueLabel returns true if issue has been labeled. +func HasIssueLabel(issueID, labelID int64) bool { + return hasIssueLabel(x, issueID, labelID) +} + +func newIssueLabel(e Engine, issueID, labelID int64) error { + if issueID == 0 || labelID == 0 { + return nil + } + + _, err := e.Insert(&IssueLabel{ + IssueID: issueID, + LabelID: labelID, + }) + return err +} + +// NewIssueLabel creates a new issue-label relation. +func NewIssueLabel(issueID, labelID int64) error { + return newIssueLabel(x, issueID, labelID) +} + +func getIssueLabels(e Engine, issueID int64) ([]*IssueLabel, error) { + issueLabels := make([]*IssueLabel, 0, 10) + return issueLabels, e.Where("issue_id=?", issueID).Asc("label_id").Find(&issueLabels) +} + +// GetIssueLabels returns all issue-label relations of given issue by ID. +func GetIssueLabels(issueID int64) ([]*IssueLabel, error) { + return getIssueLabels(x, issueID) +} + +func deleteIssueLabel(e Engine, issueID, labelID int64) error { + _, err := e.Delete(&IssueLabel{ + IssueID: issueID, + LabelID: labelID, + }) + return err +} + +// DeleteIssueLabel deletes issue-label relation. +func DeleteIssueLabel(issueID, labelID int64) error { + return deleteIssueLabel(x, issueID, labelID) +} + // _____ .__.__ __ // / \ |__| | ____ _______/ |_ ____ ____ ____ // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ @@ -658,6 +842,14 @@ type Milestone struct { ClosedDate time.Time } +func (m *Milestone) BeforeUpdate() { + if m.NumIssues > 0 { + m.Completeness = m.NumClosedIssues * 100 / m.NumIssues + } else { + m.Completeness = 0 + } +} + func (m *Milestone) AfterSet(colName string, _ xorm.Cell) { if colName == "deadline" { if m.Deadline.Year() == 9999 { @@ -694,14 +886,30 @@ func NewMilestone(m *Milestone) (err error) { return sess.Commit() } +func getMilestoneByID(e Engine, 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{id, 0} + } + return m, nil +} + // GetMilestoneByID returns the milestone of given ID. func GetMilestoneByID(id int64) (*Milestone, error) { - m := &Milestone{ID: id} + return getMilestoneByID(x, id) +} + +// GetRepoMilestoneByID returns the milestone of given ID and repository. +func GetRepoMilestoneByID(repoID, milestoneID int64) (*Milestone, error) { + m := &Milestone{ID: milestoneID, RepoID: repoID} has, err := x.Get(m) if err != nil { return nil, err } else if !has { - return nil, ErrMilestoneNotExist{id} + return nil, ErrMilestoneNotExist{milestoneID, repoID} } return m, nil } @@ -760,7 +968,7 @@ func MilestoneStats(repoID int64) (open int64, closed int64) { // ChangeMilestoneStatus changes the milestone open/closed status. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { - repo, err := GetRepositoryById(m.RepoID) + repo, err := GetRepositoryByID(m.RepoID) if err != nil { return err } @@ -776,9 +984,9 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { return err } - repo.NumMilestones = int(countRepoMilestones(sess, repo.Id)) - repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.Id)) - if _, err = sess.Id(repo.Id).AllCols().Update(repo); err != nil { + repo.NumMilestones = int(countRepoMilestones(sess, repo.ID)) + repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID)) + if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil { return err } return sess.Commit() @@ -804,21 +1012,12 @@ func ChangeMilestoneIssueStats(issue *Issue) error { 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 - } - +func changeMilestoneAssign(e *xorm.Session, oldMid int64, issue *Issue) error { if oldMid > 0 { - m, err := GetMilestoneByID(oldMid) + m, err := getMilestoneByID(e, oldMid) if err != nil { return err } @@ -827,26 +1026,16 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { 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() + if err = updateMilestone(e, m); err != nil { return err - } - - rawSql := "UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?" - if _, err = sess.Exec(rawSql, issue.ID); err != nil { - sess.Rollback() + } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=0 WHERE issue_id=?", issue.ID); err != nil { return err } } - if mid > 0 { - m, err := GetMilestoneByID(mid) + if issue.MilestoneID > 0 { + m, err := GetMilestoneByID(issue.MilestoneID) if err != nil { return err } @@ -860,19 +1049,28 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { 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() + if err = updateMilestone(e, m); err != nil { 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() + } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=? WHERE issue_id=?", m.ID, issue.ID); err != nil { return err } } + return nil +} + +// ChangeMilestoneAssign changes assignment of milestone for issue. +func ChangeMilestoneAssign(oldMid int64, issue *Issue) (err error) { + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if err = changeMilestoneAssign(sess, oldMid, issue); err != nil { + return err + } + return sess.Commit() } @@ -886,7 +1084,7 @@ func DeleteMilestoneByID(mid int64) error { return err } - repo, err := GetRepositoryById(m.RepoID) + repo, err := GetRepositoryByID(m.RepoID) if err != nil { return err } @@ -901,9 +1099,9 @@ func DeleteMilestoneByID(mid int64) error { return err } - repo.NumMilestones = int(countRepoMilestones(sess, repo.Id)) - repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.Id)) - if _, err = sess.Id(repo.Id).AllCols().Update(repo); err != nil { + repo.NumMilestones = int(countRepoMilestones(sess, repo.ID)) + repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID)) + if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil { return err } @@ -1036,49 +1234,73 @@ func (c *Comment) AfterDelete() { } } +// Attachment represent a attachment of issue/comment/release. type Attachment struct { - Id int64 - IssueId int64 - CommentId int64 + ID int64 `xorm:"pk autoincr"` + UUID string `xorm:"uuid UNIQUE"` + IssueID int64 `xorm:"INDEX"` + CommentID int64 + ReleaseID int64 `xorm:"INDEX"` 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() +// AttachmentLocalPath returns where attachment is stored in local file system based on given UUID. +func AttachmentLocalPath(uuid string) string { + return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid) +} + +// LocalPath returns where attachment is stored in local file system. +func (attach *Attachment) LocalPath() string { + return AttachmentLocalPath(attach.UUID) +} + +// NewAttachment creates a new attachment object. +func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) { + attach := &Attachment{ + UUID: gouuid.NewV4().String(), + Name: name, + } + + if err = os.MkdirAll(path.Dir(attach.LocalPath()), os.ModePerm); err != nil { + return nil, fmt.Errorf("MkdirAll: %v", err) + } + + fw, err := os.Create(attach.LocalPath()) + if err != nil { + return nil, fmt.Errorf("Create: %v", err) + } + defer fw.Close() + if _, err = fw.Write(buf); err != nil { + return nil, fmt.Errorf("Write: %v", err) + } else if _, err = io.Copy(fw, file); err != nil { + return nil, fmt.Errorf("Copy: %v", err) + } + + sess := x.NewSession() + defer sessionRelease(sess) 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() + if _, err := sess.Insert(attach); err != nil { return nil, err } - return a, sess.Commit() + return attach, sess.Commit() } -// Attachment returns the attachment by given ID. -func GetAttachmentById(id int64) (*Attachment, error) { - m := &Attachment{Id: id} - - has, err := x.Get(m) - +// GetAttachmentByUUID returns attachment by given UUID. +func GetAttachmentByUUID(uuid string) (*Attachment, error) { + attach := &Attachment{UUID: uuid} + has, err := x.Get(attach) if err != nil { return nil, err + } else if !has { + return nil, ErrAttachmentNotExist{0, uuid} } - - if !has { - return nil, ErrAttachmentNotExist - } - - return m, nil + return attach, nil } func GetAttachmentsForIssue(issueId int64) ([]*Attachment, error) { @@ -1111,12 +1333,12 @@ func DeleteAttachment(a *Attachment, remove bool) error { func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) { for i, a := range attachments { if remove { - if err := os.Remove(a.Path); err != nil { + if err := os.Remove(a.LocalPath()); err != nil { return i, err } } - if _, err := x.Delete(a.Id); err != nil { + if _, err := x.Delete(a.ID); err != nil { return i, err } } |