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.

1089 lines
27 KiB

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