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 23 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
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
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  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. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/go-xorm/xorm"
  12. "github.com/gogits/gogs/modules/base"
  13. )
  14. var (
  15. ErrIssueNotExist = errors.New("Issue does not exist")
  16. ErrLabelNotExist = errors.New("Label does not exist")
  17. ErrMilestoneNotExist = errors.New("Milestone does not exist")
  18. ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone")
  19. ErrMissingIssueNumber = errors.New("No issue number specified")
  20. )
  21. // Issue represents an issue or pull request of repository.
  22. type Issue struct {
  23. Id int64
  24. RepoId int64 `xorm:"INDEX"`
  25. Index int64 // Index in one repository.
  26. Name string
  27. Repo *Repository `xorm:"-"`
  28. PosterId int64
  29. Poster *User `xorm:"-"`
  30. LabelIds string `xorm:"TEXT"`
  31. Labels []*Label `xorm:"-"`
  32. MilestoneId int64
  33. AssigneeId int64
  34. Assignee *User `xorm:"-"`
  35. IsRead bool `xorm:"-"`
  36. IsPull bool // Indicates whether is a pull request or not.
  37. IsClosed bool
  38. Content string `xorm:"TEXT"`
  39. RenderedContent string `xorm:"-"`
  40. Priority int
  41. NumComments int
  42. Deadline time.Time
  43. Created time.Time `xorm:"CREATED"`
  44. Updated time.Time `xorm:"UPDATED"`
  45. }
  46. func (i *Issue) GetPoster() (err error) {
  47. i.Poster, err = GetUserById(i.PosterId)
  48. if err == ErrUserNotExist {
  49. i.Poster = &User{Name: "FakeUser"}
  50. return nil
  51. }
  52. return err
  53. }
  54. func (i *Issue) GetLabels() error {
  55. if len(i.LabelIds) < 3 {
  56. return nil
  57. }
  58. strIds := strings.Split(strings.TrimSuffix(i.LabelIds[1:], "|"), "|$")
  59. i.Labels = make([]*Label, 0, len(strIds))
  60. for _, strId := range strIds {
  61. id, _ := base.StrTo(strId).Int64()
  62. if id > 0 {
  63. l, err := GetLabelById(id)
  64. if err != nil {
  65. if err == ErrLabelNotExist {
  66. continue
  67. }
  68. return err
  69. }
  70. i.Labels = append(i.Labels, l)
  71. }
  72. }
  73. return nil
  74. }
  75. func (i *Issue) GetAssignee() (err error) {
  76. if i.AssigneeId == 0 {
  77. return nil
  78. }
  79. i.Assignee, err = GetUserById(i.AssigneeId)
  80. if err == ErrUserNotExist {
  81. return nil
  82. }
  83. return err
  84. }
  85. // CreateIssue creates new issue for repository.
  86. func NewIssue(issue *Issue) (err error) {
  87. sess := x.NewSession()
  88. defer sess.Close()
  89. if err = sess.Begin(); err != nil {
  90. return err
  91. }
  92. if _, err = sess.Insert(issue); err != nil {
  93. sess.Rollback()
  94. return err
  95. }
  96. rawSql := "UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?"
  97. if _, err = sess.Exec(rawSql, issue.RepoId); err != nil {
  98. sess.Rollback()
  99. return err
  100. }
  101. if err = sess.Commit(); err != nil {
  102. return err
  103. }
  104. if issue.MilestoneId > 0 {
  105. // FIXES(280): Update milestone counter.
  106. return ChangeMilestoneAssign(0, issue.MilestoneId, issue)
  107. }
  108. return
  109. }
  110. // GetIssueByRef returns an Issue specified by a GFM reference.
  111. // See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
  112. func GetIssueByRef(ref string) (issue *Issue, err error) {
  113. var issueNumber int64
  114. var repo *Repository
  115. n := strings.IndexByte(ref, byte('#'))
  116. if n == -1 {
  117. return nil, ErrMissingIssueNumber
  118. }
  119. if issueNumber, err = strconv.ParseInt(ref[n+1:], 10, 64); err != nil {
  120. return
  121. }
  122. if repo, err = GetRepositoryByRef(ref[:n]); err != nil {
  123. return
  124. }
  125. return GetIssueByIndex(repo.Id, issueNumber)
  126. }
  127. // GetIssueByIndex returns issue by given index in repository.
  128. func GetIssueByIndex(rid, index int64) (*Issue, error) {
  129. issue := &Issue{RepoId: rid, Index: index}
  130. has, err := x.Get(issue)
  131. if err != nil {
  132. return nil, err
  133. } else if !has {
  134. return nil, ErrIssueNotExist
  135. }
  136. return issue, nil
  137. }
  138. // GetIssueById returns an issue by ID.
  139. func GetIssueById(id int64) (*Issue, error) {
  140. issue := &Issue{Id: id}
  141. has, err := x.Get(issue)
  142. if err != nil {
  143. return nil, err
  144. } else if !has {
  145. return nil, ErrIssueNotExist
  146. }
  147. return issue, nil
  148. }
  149. // GetIssues returns a list of issues by given conditions.
  150. func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labelIds, sortType string) ([]Issue, error) {
  151. sess := x.Limit(20, (page-1)*20)
  152. if rid > 0 {
  153. sess.Where("repo_id=?", rid).And("is_closed=?", isClosed)
  154. } else {
  155. sess.Where("is_closed=?", isClosed)
  156. }
  157. if uid > 0 {
  158. sess.And("assignee_id=?", uid)
  159. } else if pid > 0 {
  160. sess.And("poster_id=?", pid)
  161. }
  162. if mid > 0 {
  163. sess.And("milestone_id=?", mid)
  164. }
  165. if len(labelIds) > 0 {
  166. for _, label := range strings.Split(labelIds, ",") {
  167. sess.And("label_ids like '%$" + label + "|%'")
  168. }
  169. }
  170. switch sortType {
  171. case "oldest":
  172. sess.Asc("created")
  173. case "recentupdate":
  174. sess.Desc("updated")
  175. case "leastupdate":
  176. sess.Asc("updated")
  177. case "mostcomment":
  178. sess.Desc("num_comments")
  179. case "leastcomment":
  180. sess.Asc("num_comments")
  181. case "priority":
  182. sess.Desc("priority")
  183. default:
  184. sess.Desc("created")
  185. }
  186. var issues []Issue
  187. err := sess.Find(&issues)
  188. return issues, err
  189. }
  190. type IssueStatus int
  191. const (
  192. IS_OPEN = iota + 1
  193. IS_CLOSE
  194. )
  195. // GetIssuesByLabel returns a list of issues by given label and repository.
  196. func GetIssuesByLabel(repoId int64, label string) ([]*Issue, error) {
  197. issues := make([]*Issue, 0, 10)
  198. err := x.Where("repo_id=?", repoId).And("label_ids like '%$" + label + "|%'").Find(&issues)
  199. return issues, err
  200. }
  201. // GetIssueCountByPoster returns number of issues of repository by poster.
  202. func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 {
  203. count, _ := x.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue))
  204. return count
  205. }
  206. // .___ ____ ___
  207. // | | ______ ________ __ ____ | | \______ ___________
  208. // | |/ ___// ___/ | \_/ __ \| | / ___// __ \_ __ \
  209. // | |\___ \ \___ \| | /\ ___/| | /\___ \\ ___/| | \/
  210. // |___/____ >____ >____/ \___ >______//____ >\___ >__|
  211. // \/ \/ \/ \/ \/
  212. // IssueUser represents an issue-user relation.
  213. type IssueUser struct {
  214. Id int64
  215. Uid int64 `xorm:"INDEX"` // User ID.
  216. IssueId int64
  217. RepoId int64 `xorm:"INDEX"`
  218. MilestoneId int64
  219. IsRead bool
  220. IsAssigned bool
  221. IsMentioned bool
  222. IsPoster bool
  223. IsClosed bool
  224. }
  225. // NewIssueUserPairs adds new issue-user pairs for new issue of repository.
  226. func NewIssueUserPairs(rid, iid, oid, pid, aid int64, repoName string) (err error) {
  227. iu := &IssueUser{IssueId: iid, RepoId: rid}
  228. us, err := GetCollaborators(repoName)
  229. if err != nil {
  230. return err
  231. }
  232. isNeedAddPoster := true
  233. for _, u := range us {
  234. iu.Uid = u.Id
  235. iu.IsPoster = iu.Uid == pid
  236. if isNeedAddPoster && iu.IsPoster {
  237. isNeedAddPoster = false
  238. }
  239. iu.IsAssigned = iu.Uid == aid
  240. if _, err = x.Insert(iu); err != nil {
  241. return err
  242. }
  243. }
  244. if isNeedAddPoster {
  245. iu.Uid = pid
  246. iu.IsPoster = true
  247. iu.IsAssigned = iu.Uid == aid
  248. if _, err = x.Insert(iu); err != nil {
  249. return err
  250. }
  251. }
  252. return nil
  253. }
  254. // PairsContains returns true when pairs list contains given issue.
  255. func PairsContains(ius []*IssueUser, issueId int64) int {
  256. for i := range ius {
  257. if ius[i].IssueId == issueId {
  258. return i
  259. }
  260. }
  261. return -1
  262. }
  263. // GetIssueUserPairs returns issue-user pairs by given repository and user.
  264. func GetIssueUserPairs(rid, uid int64, isClosed bool) ([]*IssueUser, error) {
  265. ius := make([]*IssueUser, 0, 10)
  266. err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoId: rid, Uid: uid})
  267. return ius, err
  268. }
  269. // GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs.
  270. func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) {
  271. if len(rids) == 0 {
  272. return []*IssueUser{}, nil
  273. }
  274. buf := bytes.NewBufferString("")
  275. for _, rid := range rids {
  276. buf.WriteString("repo_id=")
  277. buf.WriteString(base.ToStr(rid))
  278. buf.WriteString(" OR ")
  279. }
  280. cond := strings.TrimSuffix(buf.String(), " OR ")
  281. ius := make([]*IssueUser, 0, 10)
  282. sess := x.Limit(20, (page-1)*20).Where("is_closed=?", isClosed)
  283. if len(cond) > 0 {
  284. sess.And(cond)
  285. }
  286. err := sess.Find(&ius)
  287. return ius, err
  288. }
  289. // GetIssueUserPairsByMode returns issue-user pairs by given repository and user.
  290. func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int) ([]*IssueUser, error) {
  291. ius := make([]*IssueUser, 0, 10)
  292. sess := x.Limit(20, (page-1)*20).Where("uid=?", uid).And("is_closed=?", isClosed)
  293. if rid > 0 {
  294. sess.And("repo_id=?", rid)
  295. }
  296. switch filterMode {
  297. case FM_ASSIGN:
  298. sess.And("is_assigned=?", true)
  299. case FM_CREATE:
  300. sess.And("is_poster=?", true)
  301. default:
  302. return ius, nil
  303. }
  304. err := sess.Find(&ius)
  305. return ius, err
  306. }
  307. // IssueStats represents issue statistic information.
  308. type IssueStats struct {
  309. OpenCount, ClosedCount int64
  310. AllCount int64
  311. AssignCount int64
  312. CreateCount int64
  313. MentionCount int64
  314. }
  315. // Filter modes.
  316. const (
  317. FM_ASSIGN = iota + 1
  318. FM_CREATE
  319. FM_MENTION
  320. )
  321. // GetIssueStats returns issue statistic information by given conditions.
  322. func GetIssueStats(rid, uid int64, isShowClosed bool, filterMode int) *IssueStats {
  323. stats := &IssueStats{}
  324. issue := new(Issue)
  325. tmpSess := &xorm.Session{}
  326. sess := x.Where("repo_id=?", rid)
  327. *tmpSess = *sess
  328. stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue)
  329. *tmpSess = *sess
  330. stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue)
  331. if isShowClosed {
  332. stats.AllCount = stats.ClosedCount
  333. } else {
  334. stats.AllCount = stats.OpenCount
  335. }
  336. if filterMode != FM_MENTION {
  337. sess = x.Where("repo_id=?", rid)
  338. switch filterMode {
  339. case FM_ASSIGN:
  340. sess.And("assignee_id=?", uid)
  341. case FM_CREATE:
  342. sess.And("poster_id=?", uid)
  343. default:
  344. goto nofilter
  345. }
  346. *tmpSess = *sess
  347. stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue)
  348. *tmpSess = *sess
  349. stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue)
  350. } else {
  351. sess := x.Where("repo_id=?", rid).And("uid=?", uid).And("is_mentioned=?", true)
  352. *tmpSess = *sess
  353. stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(new(IssueUser))
  354. *tmpSess = *sess
  355. stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(new(IssueUser))
  356. }
  357. nofilter:
  358. stats.AssignCount, _ = x.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("assignee_id=?", uid).Count(issue)
  359. stats.CreateCount, _ = x.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("poster_id=?", uid).Count(issue)
  360. stats.MentionCount, _ = x.Where("repo_id=?", rid).And("uid=?", uid).And("is_closed=?", isShowClosed).And("is_mentioned=?", true).Count(new(IssueUser))
  361. return stats
  362. }
  363. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
  364. func GetUserIssueStats(uid int64, filterMode int) *IssueStats {
  365. stats := &IssueStats{}
  366. issue := new(Issue)
  367. stats.AssignCount, _ = x.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue)
  368. stats.CreateCount, _ = x.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue)
  369. return stats
  370. }
  371. // UpdateIssue updates information of issue.
  372. func UpdateIssue(issue *Issue) error {
  373. _, err := x.Id(issue.Id).AllCols().Update(issue)
  374. if err != nil {
  375. return err
  376. }
  377. return err
  378. }
  379. // UpdateIssueUserByStatus updates issue-user pairs by issue status.
  380. func UpdateIssueUserPairsByStatus(iid int64, isClosed bool) error {
  381. rawSql := "UPDATE `issue_user` SET is_closed = ? WHERE issue_id = ?"
  382. _, err := x.Exec(rawSql, isClosed, iid)
  383. return err
  384. }
  385. // UpdateIssueUserPairByAssignee updates issue-user pair for assigning.
  386. func UpdateIssueUserPairByAssignee(aid, iid int64) error {
  387. rawSql := "UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?"
  388. if _, err := x.Exec(rawSql, false, iid); err != nil {
  389. return err
  390. }
  391. // Assignee ID equals to 0 means clear assignee.
  392. if aid == 0 {
  393. return nil
  394. }
  395. rawSql = "UPDATE `issue_user` SET is_assigned = true WHERE uid = ? AND issue_id = ?"
  396. _, err := x.Exec(rawSql, aid, iid)
  397. return err
  398. }
  399. // UpdateIssueUserPairByRead updates issue-user pair for reading.
  400. func UpdateIssueUserPairByRead(uid, iid int64) error {
  401. rawSql := "UPDATE `issue_user` SET is_read = ? WHERE uid = ? AND issue_id = ?"
  402. _, err := x.Exec(rawSql, true, uid, iid)
  403. return err
  404. }
  405. // UpdateIssueUserPairsByMentions updates issue-user pairs by mentioning.
  406. func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error {
  407. for _, uid := range uids {
  408. iu := &IssueUser{Uid: uid, IssueId: iid}
  409. has, err := x.Get(iu)
  410. if err != nil {
  411. return err
  412. }
  413. iu.IsMentioned = true
  414. if has {
  415. _, err = x.Id(iu.Id).AllCols().Update(iu)
  416. } else {
  417. _, err = x.Insert(iu)
  418. }
  419. if err != nil {
  420. return err
  421. }
  422. }
  423. return nil
  424. }
  425. // .____ ___. .__
  426. // | | _____ \_ |__ ____ | |
  427. // | | \__ \ | __ \_/ __ \| |
  428. // | |___ / __ \| \_\ \ ___/| |__
  429. // |_______ (____ /___ /\___ >____/
  430. // \/ \/ \/ \/
  431. // Label represents a label of repository for issues.
  432. type Label struct {
  433. Id int64
  434. RepoId int64 `xorm:"INDEX"`
  435. Name string
  436. Color string `xorm:"VARCHAR(7)"`
  437. NumIssues int
  438. NumClosedIssues int
  439. NumOpenIssues int `xorm:"-"`
  440. IsChecked bool `xorm:"-"`
  441. }
  442. // CalOpenIssues calculates the open issues of label.
  443. func (m *Label) CalOpenIssues() {
  444. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  445. }
  446. // NewLabel creates new label of repository.
  447. func NewLabel(l *Label) error {
  448. _, err := x.Insert(l)
  449. return err
  450. }
  451. // GetLabelById returns a label by given ID.
  452. func GetLabelById(id int64) (*Label, error) {
  453. if id <= 0 {
  454. return nil, ErrLabelNotExist
  455. }
  456. l := &Label{Id: id}
  457. has, err := x.Get(l)
  458. if err != nil {
  459. return nil, err
  460. } else if !has {
  461. return nil, ErrLabelNotExist
  462. }
  463. return l, nil
  464. }
  465. // GetLabels returns a list of labels of given repository ID.
  466. func GetLabels(repoId int64) ([]*Label, error) {
  467. labels := make([]*Label, 0, 10)
  468. err := x.Where("repo_id=?", repoId).Find(&labels)
  469. return labels, err
  470. }
  471. // UpdateLabel updates label information.
  472. func UpdateLabel(l *Label) error {
  473. _, err := x.Id(l.Id).Update(l)
  474. return err
  475. }
  476. // DeleteLabel delete a label of given repository.
  477. func DeleteLabel(repoId int64, strId string) error {
  478. id, _ := base.StrTo(strId).Int64()
  479. l, err := GetLabelById(id)
  480. if err != nil {
  481. if err == ErrLabelNotExist {
  482. return nil
  483. }
  484. return err
  485. }
  486. issues, err := GetIssuesByLabel(repoId, strId)
  487. if err != nil {
  488. return err
  489. }
  490. sess := x.NewSession()
  491. defer sess.Close()
  492. if err = sess.Begin(); err != nil {
  493. return err
  494. }
  495. for _, issue := range issues {
  496. issue.LabelIds = strings.Replace(issue.LabelIds, "$"+strId+"|", "", -1)
  497. if _, err = sess.Id(issue.Id).AllCols().Update(issue); err != nil {
  498. sess.Rollback()
  499. return err
  500. }
  501. }
  502. if _, err = sess.Delete(l); err != nil {
  503. sess.Rollback()
  504. return err
  505. }
  506. return sess.Commit()
  507. }
  508. // _____ .__.__ __
  509. // / \ |__| | ____ _______/ |_ ____ ____ ____
  510. // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
  511. // / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
  512. // \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
  513. // \/ \/ \/ \/ \/
  514. // Milestone represents a milestone of repository.
  515. type Milestone struct {
  516. Id int64
  517. RepoId int64 `xorm:"INDEX"`
  518. Index int64
  519. Name string
  520. Content string
  521. RenderedContent string `xorm:"-"`
  522. IsClosed bool
  523. NumIssues int
  524. NumClosedIssues int
  525. NumOpenIssues int `xorm:"-"`
  526. Completeness int // Percentage(1-100).
  527. Deadline time.Time
  528. DeadlineString string `xorm:"-"`
  529. ClosedDate time.Time
  530. }
  531. // CalOpenIssues calculates the open issues of milestone.
  532. func (m *Milestone) CalOpenIssues() {
  533. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  534. }
  535. // NewMilestone creates new milestone of repository.
  536. func NewMilestone(m *Milestone) (err error) {
  537. sess := x.NewSession()
  538. defer sess.Close()
  539. if err = sess.Begin(); err != nil {
  540. return err
  541. }
  542. if _, err = sess.Insert(m); err != nil {
  543. sess.Rollback()
  544. return err
  545. }
  546. rawSql := "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?"
  547. if _, err = sess.Exec(rawSql, m.RepoId); err != nil {
  548. sess.Rollback()
  549. return err
  550. }
  551. return sess.Commit()
  552. }
  553. // GetMilestoneById returns the milestone by given ID.
  554. func GetMilestoneById(id int64) (*Milestone, error) {
  555. m := &Milestone{Id: id}
  556. has, err := x.Get(m)
  557. if err != nil {
  558. return nil, err
  559. } else if !has {
  560. return nil, ErrMilestoneNotExist
  561. }
  562. return m, nil
  563. }
  564. // GetMilestoneByIndex returns the milestone of given repository and index.
  565. func GetMilestoneByIndex(repoId, idx int64) (*Milestone, error) {
  566. m := &Milestone{RepoId: repoId, Index: idx}
  567. has, err := x.Get(m)
  568. if err != nil {
  569. return nil, err
  570. } else if !has {
  571. return nil, ErrMilestoneNotExist
  572. }
  573. return m, nil
  574. }
  575. // GetMilestones returns a list of milestones of given repository and status.
  576. func GetMilestones(repoId int64, isClosed bool) ([]*Milestone, error) {
  577. miles := make([]*Milestone, 0, 10)
  578. err := x.Where("repo_id=?", repoId).And("is_closed=?", isClosed).Find(&miles)
  579. return miles, err
  580. }
  581. // UpdateMilestone updates information of given milestone.
  582. func UpdateMilestone(m *Milestone) error {
  583. _, err := x.Id(m.Id).Update(m)
  584. return err
  585. }
  586. // ChangeMilestoneStatus changes the milestone open/closed status.
  587. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  588. repo, err := GetRepositoryById(m.RepoId)
  589. if err != nil {
  590. return err
  591. }
  592. sess := x.NewSession()
  593. defer sess.Close()
  594. if err = sess.Begin(); err != nil {
  595. return err
  596. }
  597. m.IsClosed = isClosed
  598. if _, err = sess.Id(m.Id).AllCols().Update(m); err != nil {
  599. sess.Rollback()
  600. return err
  601. }
  602. if isClosed {
  603. repo.NumClosedMilestones++
  604. } else {
  605. repo.NumClosedMilestones--
  606. }
  607. if _, err = sess.Id(repo.Id).Update(repo); err != nil {
  608. sess.Rollback()
  609. return err
  610. }
  611. return sess.Commit()
  612. }
  613. // ChangeMilestoneIssueStats updates the open/closed issues counter and progress for the
  614. // milestone associated witht the given issue.
  615. func ChangeMilestoneIssueStats(issue *Issue) error {
  616. if issue.MilestoneId == 0 {
  617. return nil
  618. }
  619. m, err := GetMilestoneById(issue.MilestoneId)
  620. if err != nil {
  621. return err
  622. }
  623. if issue.IsClosed {
  624. m.NumOpenIssues--
  625. m.NumClosedIssues++
  626. } else {
  627. m.NumOpenIssues++
  628. m.NumClosedIssues--
  629. }
  630. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  631. return UpdateMilestone(m)
  632. }
  633. // ChangeMilestoneAssign changes assignment of milestone for issue.
  634. func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) {
  635. sess := x.NewSession()
  636. defer sess.Close()
  637. if err = sess.Begin(); err != nil {
  638. return err
  639. }
  640. if oldMid > 0 {
  641. m, err := GetMilestoneById(oldMid)
  642. if err != nil {
  643. return err
  644. }
  645. m.NumIssues--
  646. if issue.IsClosed {
  647. m.NumClosedIssues--
  648. }
  649. if m.NumIssues > 0 {
  650. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  651. } else {
  652. m.Completeness = 0
  653. }
  654. if _, err = sess.Id(m.Id).Update(m); err != nil {
  655. sess.Rollback()
  656. return err
  657. }
  658. rawSql := "UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?"
  659. if _, err = sess.Exec(rawSql, issue.Id); err != nil {
  660. sess.Rollback()
  661. return err
  662. }
  663. }
  664. if mid > 0 {
  665. m, err := GetMilestoneById(mid)
  666. if err != nil {
  667. return err
  668. }
  669. m.NumIssues++
  670. if issue.IsClosed {
  671. m.NumClosedIssues++
  672. }
  673. if m.NumIssues == 0 {
  674. return ErrWrongIssueCounter
  675. }
  676. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  677. if _, err = sess.Id(m.Id).Update(m); err != nil {
  678. sess.Rollback()
  679. return err
  680. }
  681. rawSql := "UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?"
  682. if _, err = sess.Exec(rawSql, m.Id, issue.Id); err != nil {
  683. sess.Rollback()
  684. return err
  685. }
  686. }
  687. return sess.Commit()
  688. }
  689. // DeleteMilestone deletes a milestone.
  690. func DeleteMilestone(m *Milestone) (err error) {
  691. sess := x.NewSession()
  692. defer sess.Close()
  693. if err = sess.Begin(); err != nil {
  694. return err
  695. }
  696. if _, err = sess.Delete(m); err != nil {
  697. sess.Rollback()
  698. return err
  699. }
  700. rawSql := "UPDATE `repository` SET num_milestones = num_milestones - 1 WHERE id = ?"
  701. if _, err = sess.Exec(rawSql, m.RepoId); err != nil {
  702. sess.Rollback()
  703. return err
  704. }
  705. rawSql = "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?"
  706. if _, err = sess.Exec(rawSql, m.Id); err != nil {
  707. sess.Rollback()
  708. return err
  709. }
  710. rawSql = "UPDATE `issue_user` SET milestone_id = 0 WHERE milestone_id = ?"
  711. if _, err = sess.Exec(rawSql, m.Id); err != nil {
  712. sess.Rollback()
  713. return err
  714. }
  715. return sess.Commit()
  716. }
  717. // _________ __
  718. // \_ ___ \ ____ _____ _____ ____ _____/ |_
  719. // / \ \/ / _ \ / \ / \_/ __ \ / \ __\
  720. // \ \___( <_> ) Y Y \ Y Y \ ___/| | \ |
  721. // \______ /\____/|__|_| /__|_| /\___ >___| /__|
  722. // \/ \/ \/ \/ \/
  723. // Issue types.
  724. const (
  725. IT_PLAIN = iota // Pure comment.
  726. IT_REOPEN // Issue reopen status change prompt.
  727. IT_CLOSE // Issue close status change prompt.
  728. )
  729. // Comment represents a comment in commit and issue page.
  730. type Comment struct {
  731. Id int64
  732. Type int
  733. PosterId int64
  734. Poster *User `xorm:"-"`
  735. IssueId int64
  736. CommitId int64
  737. Line int64
  738. Content string `xorm:"TEXT"`
  739. Created time.Time `xorm:"CREATED"`
  740. }
  741. // CreateComment creates comment of issue or commit.
  742. func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string) error {
  743. sess := x.NewSession()
  744. defer sess.Close()
  745. if err := sess.Begin(); err != nil {
  746. return err
  747. }
  748. if _, err := sess.Insert(&Comment{PosterId: userId, Type: cmtType, IssueId: issueId,
  749. CommitId: commitId, Line: line, Content: content}); err != nil {
  750. sess.Rollback()
  751. return err
  752. }
  753. // Check comment type.
  754. switch cmtType {
  755. case IT_PLAIN:
  756. rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?"
  757. if _, err := sess.Exec(rawSql, issueId); err != nil {
  758. sess.Rollback()
  759. return err
  760. }
  761. case IT_REOPEN:
  762. rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?"
  763. if _, err := sess.Exec(rawSql, repoId); err != nil {
  764. sess.Rollback()
  765. return err
  766. }
  767. case IT_CLOSE:
  768. rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?"
  769. if _, err := sess.Exec(rawSql, repoId); err != nil {
  770. sess.Rollback()
  771. return err
  772. }
  773. }
  774. return sess.Commit()
  775. }
  776. // GetIssueComments returns list of comment by given issue id.
  777. func GetIssueComments(issueId int64) ([]Comment, error) {
  778. comments := make([]Comment, 0, 10)
  779. err := x.Asc("created").Find(&comments, &Comment{IssueId: issueId})
  780. return comments, err
  781. }