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

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
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. }