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

issue.go 14 kB

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