issue.go 25 KB

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