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.

issue.go 12KB

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