You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

478 lines
12 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "bytes"
  7. "errors"
  8. "strings"
  9. "time"
  10. "github.com/gogits/gogs/modules/base"
  11. )
  12. var (
  13. ErrIssueNotExist = errors.New("Issue does not exist")
  14. )
  15. // Issue represents an issue or pull request of repository.
  16. type Issue struct {
  17. Id int64
  18. Index int64 // Index in one repository.
  19. Name string
  20. RepoId int64 `xorm:"INDEX"`
  21. Repo *Repository `xorm:"-"`
  22. PosterId int64
  23. Poster *User `xorm:"-"`
  24. MilestoneId int64
  25. AssigneeId int64
  26. Assignee *User `xorm:"-"`
  27. IsRead bool `xorm:"-"`
  28. IsPull bool // Indicates whether is a pull request or not.
  29. IsClosed bool
  30. Labels string `xorm:"TEXT"`
  31. Content string `xorm:"TEXT"`
  32. RenderedContent string `xorm:"-"`
  33. Priority int
  34. NumComments int
  35. Deadline time.Time
  36. Created time.Time `xorm:"CREATED"`
  37. Updated time.Time `xorm:"UPDATED"`
  38. }
  39. func (i *Issue) GetPoster() (err error) {
  40. i.Poster, err = GetUserById(i.PosterId)
  41. if err == ErrUserNotExist {
  42. i.Poster = &User{Name: "FakeUser"}
  43. return nil
  44. }
  45. return err
  46. }
  47. func (i *Issue) GetAssignee() (err error) {
  48. if i.AssigneeId == 0 {
  49. return nil
  50. }
  51. i.Assignee, err = GetUserById(i.AssigneeId)
  52. return err
  53. }
  54. // CreateIssue creates new issue for repository.
  55. func NewIssue(issue *Issue) (err error) {
  56. sess := orm.NewSession()
  57. defer sess.Close()
  58. if err = sess.Begin(); err != nil {
  59. return err
  60. }
  61. if _, err = sess.Insert(issue); err != nil {
  62. sess.Rollback()
  63. return err
  64. }
  65. rawSql := "UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?"
  66. if _, err = sess.Exec(rawSql, issue.RepoId); err != nil {
  67. sess.Rollback()
  68. return err
  69. }
  70. return sess.Commit()
  71. }
  72. // GetIssueByIndex returns issue by given index in repository.
  73. func GetIssueByIndex(rid, index int64) (*Issue, error) {
  74. issue := &Issue{RepoId: rid, Index: index}
  75. has, err := orm.Get(issue)
  76. if err != nil {
  77. return nil, err
  78. } else if !has {
  79. return nil, ErrIssueNotExist
  80. }
  81. return issue, nil
  82. }
  83. // GetIssueById returns an issue by ID.
  84. func GetIssueById(id int64) (*Issue, error) {
  85. issue := &Issue{Id: id}
  86. has, err := orm.Get(issue)
  87. if err != nil {
  88. return nil, err
  89. } else if !has {
  90. return nil, ErrIssueNotExist
  91. }
  92. return issue, nil
  93. }
  94. // GetIssues returns a list of issues by given conditions.
  95. func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labels, sortType string) ([]Issue, error) {
  96. sess := orm.Limit(20, (page-1)*20)
  97. if rid > 0 {
  98. sess.Where("repo_id=?", rid).And("is_closed=?", isClosed)
  99. } else {
  100. sess.Where("is_closed=?", isClosed)
  101. }
  102. if uid > 0 {
  103. sess.And("assignee_id=?", uid)
  104. } else if pid > 0 {
  105. sess.And("poster_id=?", pid)
  106. }
  107. if mid > 0 {
  108. sess.And("milestone_id=?", mid)
  109. }
  110. if len(labels) > 0 {
  111. for _, label := range strings.Split(labels, ",") {
  112. sess.And("labels like '%$" + label + "|%'")
  113. }
  114. }
  115. switch sortType {
  116. case "oldest":
  117. sess.Asc("created")
  118. case "recentupdate":
  119. sess.Desc("updated")
  120. case "leastupdate":
  121. sess.Asc("updated")
  122. case "mostcomment":
  123. sess.Desc("num_comments")
  124. case "leastcomment":
  125. sess.Asc("num_comments")
  126. case "priority":
  127. sess.Desc("priority")
  128. default:
  129. sess.Desc("created")
  130. }
  131. var issues []Issue
  132. err := sess.Find(&issues)
  133. return issues, err
  134. }
  135. // GetIssueCountByPoster returns number of issues of repository by poster.
  136. func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 {
  137. count, _ := orm.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue))
  138. return count
  139. }
  140. // IssueUser represents an issue-user relation.
  141. type IssueUser struct {
  142. Id int64
  143. Uid int64 // User ID.
  144. IssueId int64
  145. RepoId int64
  146. IsRead bool
  147. IsAssigned bool
  148. IsMentioned bool
  149. IsPoster bool
  150. IsClosed bool
  151. }
  152. // NewIssueUserPairs adds new issue-user pairs for new issue of repository.
  153. func NewIssueUserPairs(rid, iid, oid, pid, aid int64, repoName string) (err error) {
  154. iu := &IssueUser{IssueId: iid, RepoId: rid}
  155. us, err := GetCollaborators(repoName)
  156. if err != nil {
  157. return err
  158. }
  159. isNeedAddPoster := true
  160. for _, u := range us {
  161. iu.Uid = u.Id
  162. iu.IsPoster = iu.Uid == pid
  163. if isNeedAddPoster && iu.IsPoster {
  164. isNeedAddPoster = false
  165. }
  166. iu.IsAssigned = iu.Uid == aid
  167. if _, err = orm.Insert(iu); err != nil {
  168. return err
  169. }
  170. }
  171. if isNeedAddPoster {
  172. iu.Uid = pid
  173. iu.IsPoster = true
  174. iu.IsAssigned = iu.Uid == aid
  175. if _, err = orm.Insert(iu); err != nil {
  176. return err
  177. }
  178. }
  179. return nil
  180. }
  181. // PairsContains returns true when pairs list contains given issue.
  182. func PairsContains(ius []*IssueUser, issueId int64) int {
  183. for i := range ius {
  184. if ius[i].IssueId == issueId {
  185. return i
  186. }
  187. }
  188. return -1
  189. }
  190. // GetIssueUserPairs returns issue-user pairs by given repository and user.
  191. func GetIssueUserPairs(rid, uid int64, isClosed bool) ([]*IssueUser, error) {
  192. ius := make([]*IssueUser, 0, 10)
  193. err := orm.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoId: rid, Uid: uid})
  194. return ius, err
  195. }
  196. // GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs.
  197. func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) {
  198. buf := bytes.NewBufferString("")
  199. for _, rid := range rids {
  200. buf.WriteString("repo_id=")
  201. buf.WriteString(base.ToStr(rid))
  202. buf.WriteString(" OR ")
  203. }
  204. cond := strings.TrimSuffix(buf.String(), " OR ")
  205. ius := make([]*IssueUser, 0, 10)
  206. sess := orm.Limit(20, (page-1)*20).Where("is_closed=?", isClosed)
  207. if len(cond) > 0 {
  208. sess.And(cond)
  209. }
  210. err := sess.Find(&ius)
  211. return ius, err
  212. }
  213. // GetIssueUserPairsByMode returns issue-user pairs by given repository and user.
  214. func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int) ([]*IssueUser, error) {
  215. ius := make([]*IssueUser, 0, 10)
  216. sess := orm.Limit(20, (page-1)*20).Where("uid=?", uid).And("is_closed=?", isClosed)
  217. if rid > 0 {
  218. sess.And("repo_id=?", rid)
  219. }
  220. switch filterMode {
  221. case FM_ASSIGN:
  222. sess.And("is_assigned=?", true)
  223. case FM_CREATE:
  224. sess.And("is_poster=?", true)
  225. default:
  226. return ius, nil
  227. }
  228. err := sess.Find(&ius)
  229. return ius, err
  230. }
  231. // IssueStats represents issue statistic information.
  232. type IssueStats struct {
  233. OpenCount, ClosedCount int64
  234. AllCount int64
  235. AssignCount int64
  236. CreateCount int64
  237. MentionCount int64
  238. }
  239. // Filter modes.
  240. const (
  241. FM_ASSIGN = iota + 1
  242. FM_CREATE
  243. FM_MENTION
  244. )
  245. // GetIssueStats returns issue statistic information by given conditions.
  246. func GetIssueStats(rid, uid int64, isShowClosed bool, filterMode int) *IssueStats {
  247. stats := &IssueStats{}
  248. issue := new(Issue)
  249. sess := orm.Where("repo_id=?", rid)
  250. tmpSess := sess
  251. stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue)
  252. *tmpSess = *sess
  253. stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue)
  254. if isShowClosed {
  255. stats.AllCount = stats.ClosedCount
  256. } else {
  257. stats.AllCount = stats.OpenCount
  258. }
  259. if filterMode != FM_MENTION {
  260. sess = orm.Where("repo_id=?", rid)
  261. switch filterMode {
  262. case FM_ASSIGN:
  263. sess.And("assignee_id=?", uid)
  264. case FM_CREATE:
  265. sess.And("poster_id=?", uid)
  266. default:
  267. goto nofilter
  268. }
  269. *tmpSess = *sess
  270. stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue)
  271. *tmpSess = *sess
  272. stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue)
  273. } else {
  274. sess := orm.Where("repo_id=?", rid).And("uid=?", uid).And("is_mentioned=?", true)
  275. *tmpSess = *sess
  276. stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(new(IssueUser))
  277. *tmpSess = *sess
  278. stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(new(IssueUser))
  279. }
  280. nofilter:
  281. stats.AssignCount, _ = orm.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("assignee_id=?", uid).Count(issue)
  282. stats.CreateCount, _ = orm.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("poster_id=?", uid).Count(issue)
  283. stats.MentionCount, _ = orm.Where("repo_id=?", rid).And("uid=?", uid).And("is_closed=?", isShowClosed).And("is_mentioned=?", true).Count(new(IssueUser))
  284. return stats
  285. }
  286. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
  287. func GetUserIssueStats(uid int64, filterMode int) *IssueStats {
  288. stats := &IssueStats{}
  289. issue := new(Issue)
  290. stats.AssignCount, _ = orm.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue)
  291. stats.CreateCount, _ = orm.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue)
  292. return stats
  293. }
  294. // UpdateIssue updates information of issue.
  295. func UpdateIssue(issue *Issue) error {
  296. _, err := orm.Id(issue.Id).AllCols().Update(issue)
  297. return err
  298. }
  299. // UpdateIssueUserByStatus updates issue-user pairs by issue status.
  300. func UpdateIssueUserPairsByStatus(iid int64, isClosed bool) error {
  301. rawSql := "UPDATE `issue_user` SET is_closed = ? WHERE issue_id = ?"
  302. _, err := orm.Exec(rawSql, isClosed, iid)
  303. return err
  304. }
  305. // UpdateIssueUserPairByAssignee updates issue-user pair for assigning.
  306. func UpdateIssueUserPairByAssignee(aid, iid int64) error {
  307. rawSql := "UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?"
  308. if _, err := orm.Exec(rawSql, false, iid); err != nil {
  309. return err
  310. }
  311. // Assignee ID equals to 0 means clear assignee.
  312. if aid == 0 {
  313. return nil
  314. }
  315. rawSql = "UPDATE `issue_user` SET is_assigned = true WHERE uid = ? AND issue_id = ?"
  316. _, err := orm.Exec(rawSql, aid, iid)
  317. return err
  318. }
  319. // UpdateIssueUserPairByRead updates issue-user pair for reading.
  320. func UpdateIssueUserPairByRead(uid, iid int64) error {
  321. rawSql := "UPDATE `issue_user` SET is_read = ? WHERE uid = ? AND issue_id = ?"
  322. _, err := orm.Exec(rawSql, true, uid, iid)
  323. return err
  324. }
  325. // UpdateIssueUserPairsByMentions updates issue-user pairs by mentioning.
  326. func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error {
  327. for _, uid := range uids {
  328. iu := &IssueUser{Uid: uid, IssueId: iid}
  329. has, err := orm.Get(iu)
  330. if err != nil {
  331. return err
  332. }
  333. iu.IsMentioned = true
  334. if has {
  335. _, err = orm.Id(iu.Id).AllCols().Update(iu)
  336. } else {
  337. _, err = orm.Insert(iu)
  338. }
  339. if err != nil {
  340. return err
  341. }
  342. }
  343. return nil
  344. }
  345. // Label represents a label of repository for issues.
  346. type Label struct {
  347. Id int64
  348. Rid int64 `xorm:"INDEX"`
  349. Name string
  350. Color string
  351. NumIssues int
  352. NumClosedIssues int
  353. NumOpenIssues int `xorm:"-"`
  354. }
  355. // Milestone represents a milestone of repository.
  356. type Milestone struct {
  357. Id int64
  358. Rid int64 `xorm:"INDEX"`
  359. Name string
  360. Content string
  361. IsClosed bool
  362. NumIssues int
  363. NumClosedIssues int
  364. Completeness int // Percentage(1-100).
  365. Deadline time.Time
  366. ClosedDate time.Time
  367. }
  368. // Issue types.
  369. const (
  370. IT_PLAIN = iota // Pure comment.
  371. IT_REOPEN // Issue reopen status change prompt.
  372. IT_CLOSE // Issue close status change prompt.
  373. )
  374. // Comment represents a comment in commit and issue page.
  375. type Comment struct {
  376. Id int64
  377. Type int
  378. PosterId int64
  379. Poster *User `xorm:"-"`
  380. IssueId int64
  381. CommitId int64
  382. Line int64
  383. Content string
  384. Created time.Time `xorm:"CREATED"`
  385. }
  386. // CreateComment creates comment of issue or commit.
  387. func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string) error {
  388. sess := orm.NewSession()
  389. defer sess.Close()
  390. if err := sess.Begin(); err != nil {
  391. return err
  392. }
  393. if _, err := sess.Insert(&Comment{PosterId: userId, Type: cmtType, IssueId: issueId,
  394. CommitId: commitId, Line: line, Content: content}); err != nil {
  395. sess.Rollback()
  396. return err
  397. }
  398. // Check comment type.
  399. switch cmtType {
  400. case IT_PLAIN:
  401. rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?"
  402. if _, err := sess.Exec(rawSql, issueId); err != nil {
  403. sess.Rollback()
  404. return err
  405. }
  406. case IT_REOPEN:
  407. rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?"
  408. if _, err := sess.Exec(rawSql, repoId); err != nil {
  409. sess.Rollback()
  410. return err
  411. }
  412. case IT_CLOSE:
  413. rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?"
  414. if _, err := sess.Exec(rawSql, repoId); err != nil {
  415. sess.Rollback()
  416. return err
  417. }
  418. }
  419. return sess.Commit()
  420. }
  421. // GetIssueComments returns list of comment by given issue id.
  422. func GetIssueComments(issueId int64) ([]Comment, error) {
  423. comments := make([]Comment, 0, 10)
  424. err := orm.Asc("created").Find(&comments, &Comment{IssueId: issueId})
  425. return comments, err
  426. }