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

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