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 21KB

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