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 22 kB

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