You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue_milestone.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. // Copyright 2017 The Gitea 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. "fmt"
  7. "strings"
  8. "code.gitea.io/gitea/modules/setting"
  9. api "code.gitea.io/gitea/modules/structs"
  10. "code.gitea.io/gitea/modules/timeutil"
  11. "xorm.io/builder"
  12. "xorm.io/xorm"
  13. )
  14. // Milestone represents a milestone of repository.
  15. type Milestone struct {
  16. ID int64 `xorm:"pk autoincr"`
  17. RepoID int64 `xorm:"INDEX"`
  18. Repo *Repository `xorm:"-"`
  19. Name string
  20. Content string `xorm:"TEXT"`
  21. RenderedContent string `xorm:"-"`
  22. IsClosed bool
  23. NumIssues int
  24. NumClosedIssues int
  25. NumOpenIssues int `xorm:"-"`
  26. Completeness int // Percentage(1-100).
  27. IsOverdue bool `xorm:"-"`
  28. DeadlineString string `xorm:"-"`
  29. DeadlineUnix timeutil.TimeStamp
  30. ClosedDateUnix timeutil.TimeStamp
  31. TotalTrackedTime int64 `xorm:"-"`
  32. }
  33. // BeforeUpdate is invoked from XORM before updating this object.
  34. func (m *Milestone) BeforeUpdate() {
  35. if m.NumIssues > 0 {
  36. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  37. } else {
  38. m.Completeness = 0
  39. }
  40. }
  41. // AfterLoad is invoked from XORM after setting the value of a field of
  42. // this object.
  43. func (m *Milestone) AfterLoad() {
  44. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  45. if m.DeadlineUnix.Year() == 9999 {
  46. return
  47. }
  48. m.DeadlineString = m.DeadlineUnix.Format("2006-01-02")
  49. if timeutil.TimeStampNow() >= m.DeadlineUnix {
  50. m.IsOverdue = true
  51. }
  52. }
  53. // State returns string representation of milestone status.
  54. func (m *Milestone) State() api.StateType {
  55. if m.IsClosed {
  56. return api.StateClosed
  57. }
  58. return api.StateOpen
  59. }
  60. // APIFormat returns this Milestone in API format.
  61. func (m *Milestone) APIFormat() *api.Milestone {
  62. apiMilestone := &api.Milestone{
  63. ID: m.ID,
  64. State: m.State(),
  65. Title: m.Name,
  66. Description: m.Content,
  67. OpenIssues: m.NumOpenIssues,
  68. ClosedIssues: m.NumClosedIssues,
  69. }
  70. if m.IsClosed {
  71. apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr()
  72. }
  73. if m.DeadlineUnix.Year() < 9999 {
  74. apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr()
  75. }
  76. return apiMilestone
  77. }
  78. // NewMilestone creates new milestone of repository.
  79. func NewMilestone(m *Milestone) (err error) {
  80. sess := x.NewSession()
  81. defer sess.Close()
  82. if err = sess.Begin(); err != nil {
  83. return err
  84. }
  85. m.Name = strings.TrimSpace(m.Name)
  86. if _, err = sess.Insert(m); err != nil {
  87. return err
  88. }
  89. if _, err = sess.Exec("UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil {
  90. return err
  91. }
  92. return sess.Commit()
  93. }
  94. func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
  95. m := new(Milestone)
  96. has, err := e.ID(id).Where("repo_id=?", repoID).Get(m)
  97. if err != nil {
  98. return nil, err
  99. } else if !has {
  100. return nil, ErrMilestoneNotExist{ID: id, RepoID: repoID}
  101. }
  102. return m, nil
  103. }
  104. // GetMilestoneByRepoID returns the milestone in a repository.
  105. func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
  106. return getMilestoneByRepoID(x, repoID, id)
  107. }
  108. // GetMilestoneByRepoIDANDName return a milestone if one exist by name and repo
  109. func GetMilestoneByRepoIDANDName(repoID int64, name string) (*Milestone, error) {
  110. var mile Milestone
  111. has, err := x.Where("repo_id=? AND name=?", repoID, name).Get(&mile)
  112. if err != nil {
  113. return nil, err
  114. }
  115. if !has {
  116. return nil, ErrMilestoneNotExist{Name: name, RepoID: repoID}
  117. }
  118. return &mile, nil
  119. }
  120. // GetMilestoneByID returns the milestone via id .
  121. func GetMilestoneByID(id int64) (*Milestone, error) {
  122. var m Milestone
  123. has, err := x.ID(id).Get(&m)
  124. if err != nil {
  125. return nil, err
  126. } else if !has {
  127. return nil, ErrMilestoneNotExist{ID: id, RepoID: 0}
  128. }
  129. return &m, nil
  130. }
  131. // MilestoneList is a list of milestones offering additional functionality
  132. type MilestoneList []*Milestone
  133. func (milestones MilestoneList) loadTotalTrackedTimes(e Engine) error {
  134. type totalTimesByMilestone struct {
  135. MilestoneID int64
  136. Time int64
  137. }
  138. if len(milestones) == 0 {
  139. return nil
  140. }
  141. var trackedTimes = make(map[int64]int64, len(milestones))
  142. // Get total tracked time by milestone_id
  143. rows, err := e.Table("issue").
  144. Join("INNER", "milestone", "issue.milestone_id = milestone.id").
  145. Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
  146. Where("tracked_time.deleted = ?", false).
  147. Select("milestone_id, sum(time) as time").
  148. In("milestone_id", milestones.getMilestoneIDs()).
  149. GroupBy("milestone_id").
  150. Rows(new(totalTimesByMilestone))
  151. if err != nil {
  152. return err
  153. }
  154. defer rows.Close()
  155. for rows.Next() {
  156. var totalTime totalTimesByMilestone
  157. err = rows.Scan(&totalTime)
  158. if err != nil {
  159. return err
  160. }
  161. trackedTimes[totalTime.MilestoneID] = totalTime.Time
  162. }
  163. for _, milestone := range milestones {
  164. milestone.TotalTrackedTime = trackedTimes[milestone.ID]
  165. }
  166. return nil
  167. }
  168. func (m *Milestone) loadTotalTrackedTime(e Engine) error {
  169. type totalTimesByMilestone struct {
  170. MilestoneID int64
  171. Time int64
  172. }
  173. totalTime := &totalTimesByMilestone{MilestoneID: m.ID}
  174. has, err := e.Table("issue").
  175. Join("INNER", "milestone", "issue.milestone_id = milestone.id").
  176. Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
  177. Where("tracked_time.deleted = ?", false).
  178. Select("milestone_id, sum(time) as time").
  179. Where("milestone_id = ?", m.ID).
  180. GroupBy("milestone_id").
  181. Get(totalTime)
  182. if err != nil {
  183. return err
  184. } else if !has {
  185. return nil
  186. }
  187. m.TotalTrackedTime = totalTime.Time
  188. return nil
  189. }
  190. // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
  191. func (milestones MilestoneList) LoadTotalTrackedTimes() error {
  192. return milestones.loadTotalTrackedTimes(x)
  193. }
  194. // LoadTotalTrackedTime loads the tracked time for the milestone
  195. func (m *Milestone) LoadTotalTrackedTime() error {
  196. return m.loadTotalTrackedTime(x)
  197. }
  198. func (milestones MilestoneList) getMilestoneIDs() []int64 {
  199. var ids = make([]int64, 0, len(milestones))
  200. for _, ms := range milestones {
  201. ids = append(ids, ms.ID)
  202. }
  203. return ids
  204. }
  205. // GetMilestonesByRepoID returns all opened milestones of a repository.
  206. func GetMilestonesByRepoID(repoID int64, state api.StateType, listOptions ListOptions) (MilestoneList, error) {
  207. sess := x.Where("repo_id = ?", repoID)
  208. switch state {
  209. case api.StateClosed:
  210. sess = sess.And("is_closed = ?", true)
  211. case api.StateAll:
  212. break
  213. case api.StateOpen:
  214. fallthrough
  215. default:
  216. sess = sess.And("is_closed = ?", false)
  217. }
  218. if listOptions.Page != 0 {
  219. sess = listOptions.setSessionPagination(sess)
  220. }
  221. miles := make([]*Milestone, 0, listOptions.PageSize)
  222. return miles, sess.Asc("deadline_unix").Asc("id").Find(&miles)
  223. }
  224. // GetMilestones returns a list of milestones of given repository and status.
  225. func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
  226. miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
  227. sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
  228. if page > 0 {
  229. sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
  230. }
  231. switch sortType {
  232. case "furthestduedate":
  233. sess.Desc("deadline_unix")
  234. case "leastcomplete":
  235. sess.Asc("completeness")
  236. case "mostcomplete":
  237. sess.Desc("completeness")
  238. case "leastissues":
  239. sess.Asc("num_issues")
  240. case "mostissues":
  241. sess.Desc("num_issues")
  242. default:
  243. sess.Asc("deadline_unix")
  244. }
  245. return miles, sess.Find(&miles)
  246. }
  247. func updateMilestone(e Engine, m *Milestone) error {
  248. m.Name = strings.TrimSpace(m.Name)
  249. _, err := e.ID(m.ID).AllCols().
  250. SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
  251. builder.Eq{"milestone_id": m.ID},
  252. )).
  253. SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where(
  254. builder.Eq{
  255. "milestone_id": m.ID,
  256. "is_closed": true,
  257. },
  258. )).
  259. Update(m)
  260. return err
  261. }
  262. // UpdateMilestone updates information of given milestone.
  263. func UpdateMilestone(m *Milestone, oldIsClosed bool) error {
  264. sess := x.NewSession()
  265. defer sess.Close()
  266. if err := sess.Begin(); err != nil {
  267. return err
  268. }
  269. if m.IsClosed && !oldIsClosed {
  270. m.ClosedDateUnix = timeutil.TimeStampNow()
  271. }
  272. if err := updateMilestone(sess, m); err != nil {
  273. return err
  274. }
  275. if err := updateMilestoneCompleteness(sess, m.ID); err != nil {
  276. return err
  277. }
  278. // if IsClosed changed, update milestone numbers of repository
  279. if oldIsClosed != m.IsClosed {
  280. if err := updateRepoMilestoneNum(sess, m.RepoID); err != nil {
  281. return err
  282. }
  283. }
  284. return sess.Commit()
  285. }
  286. func updateMilestoneCompleteness(e Engine, milestoneID int64) error {
  287. _, err := e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?",
  288. milestoneID,
  289. )
  290. return err
  291. }
  292. func countRepoMilestones(e Engine, repoID int64) (int64, error) {
  293. return e.
  294. Where("repo_id=?", repoID).
  295. Count(new(Milestone))
  296. }
  297. func countRepoClosedMilestones(e Engine, repoID int64) (int64, error) {
  298. return e.
  299. Where("repo_id=? AND is_closed=?", repoID, true).
  300. Count(new(Milestone))
  301. }
  302. // CountRepoClosedMilestones returns number of closed milestones in given repository.
  303. func CountRepoClosedMilestones(repoID int64) (int64, error) {
  304. return countRepoClosedMilestones(x, repoID)
  305. }
  306. // MilestoneStats returns number of open and closed milestones of given repository.
  307. func MilestoneStats(repoID int64) (open int64, closed int64, err error) {
  308. open, err = x.
  309. Where("repo_id=? AND is_closed=?", repoID, false).
  310. Count(new(Milestone))
  311. if err != nil {
  312. return 0, 0, nil
  313. }
  314. closed, err = CountRepoClosedMilestones(repoID)
  315. return open, closed, err
  316. }
  317. // ChangeMilestoneStatus changes the milestone open/closed status.
  318. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  319. sess := x.NewSession()
  320. defer sess.Close()
  321. if err = sess.Begin(); err != nil {
  322. return err
  323. }
  324. m.IsClosed = isClosed
  325. if isClosed {
  326. m.ClosedDateUnix = timeutil.TimeStampNow()
  327. }
  328. if _, err := sess.ID(m.ID).Cols("is_closed", "closed_date_unix").Update(m); err != nil {
  329. return err
  330. }
  331. if err := updateRepoMilestoneNum(sess, m.RepoID); err != nil {
  332. return err
  333. }
  334. return sess.Commit()
  335. }
  336. func updateRepoMilestoneNum(e Engine, repoID int64) error {
  337. _, err := e.Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?",
  338. repoID,
  339. repoID,
  340. true,
  341. repoID,
  342. )
  343. return err
  344. }
  345. func updateMilestoneTotalNum(e Engine, milestoneID int64) (err error) {
  346. if _, err = e.Exec("UPDATE `milestone` SET num_issues=(SELECT count(*) FROM issue WHERE milestone_id=?) WHERE id=?",
  347. milestoneID,
  348. milestoneID,
  349. ); err != nil {
  350. return
  351. }
  352. return updateMilestoneCompleteness(e, milestoneID)
  353. }
  354. func updateMilestoneClosedNum(e Engine, milestoneID int64) (err error) {
  355. if _, err = e.Exec("UPDATE `milestone` SET num_closed_issues=(SELECT count(*) FROM issue WHERE milestone_id=? AND is_closed=?) WHERE id=?",
  356. milestoneID,
  357. true,
  358. milestoneID,
  359. ); err != nil {
  360. return
  361. }
  362. return updateMilestoneCompleteness(e, milestoneID)
  363. }
  364. func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilestoneID int64) error {
  365. if err := updateIssueCols(e, issue, "milestone_id"); err != nil {
  366. return err
  367. }
  368. if oldMilestoneID > 0 {
  369. if err := updateMilestoneTotalNum(e, oldMilestoneID); err != nil {
  370. return err
  371. }
  372. if issue.IsClosed {
  373. if err := updateMilestoneClosedNum(e, oldMilestoneID); err != nil {
  374. return err
  375. }
  376. }
  377. }
  378. if issue.MilestoneID > 0 {
  379. if err := updateMilestoneTotalNum(e, issue.MilestoneID); err != nil {
  380. return err
  381. }
  382. if issue.IsClosed {
  383. if err := updateMilestoneClosedNum(e, issue.MilestoneID); err != nil {
  384. return err
  385. }
  386. }
  387. }
  388. if oldMilestoneID > 0 || issue.MilestoneID > 0 {
  389. if err := issue.loadRepo(e); err != nil {
  390. return err
  391. }
  392. var opts = &CreateCommentOptions{
  393. Type: CommentTypeMilestone,
  394. Doer: doer,
  395. Repo: issue.Repo,
  396. Issue: issue,
  397. OldMilestoneID: oldMilestoneID,
  398. MilestoneID: issue.MilestoneID,
  399. }
  400. if _, err := createComment(e, opts); err != nil {
  401. return err
  402. }
  403. }
  404. return nil
  405. }
  406. // ChangeMilestoneAssign changes assignment of milestone for issue.
  407. func ChangeMilestoneAssign(issue *Issue, doer *User, oldMilestoneID int64) (err error) {
  408. sess := x.NewSession()
  409. defer sess.Close()
  410. if err = sess.Begin(); err != nil {
  411. return err
  412. }
  413. if err = changeMilestoneAssign(sess, doer, issue, oldMilestoneID); err != nil {
  414. return err
  415. }
  416. if err = sess.Commit(); err != nil {
  417. return fmt.Errorf("Commit: %v", err)
  418. }
  419. return nil
  420. }
  421. // DeleteMilestoneByRepoID deletes a milestone from a repository.
  422. func DeleteMilestoneByRepoID(repoID, id int64) error {
  423. m, err := GetMilestoneByRepoID(repoID, id)
  424. if err != nil {
  425. if IsErrMilestoneNotExist(err) {
  426. return nil
  427. }
  428. return err
  429. }
  430. repo, err := GetRepositoryByID(m.RepoID)
  431. if err != nil {
  432. return err
  433. }
  434. sess := x.NewSession()
  435. defer sess.Close()
  436. if err = sess.Begin(); err != nil {
  437. return err
  438. }
  439. if _, err = sess.ID(m.ID).Delete(new(Milestone)); err != nil {
  440. return err
  441. }
  442. numMilestones, err := countRepoMilestones(sess, repo.ID)
  443. if err != nil {
  444. return err
  445. }
  446. numClosedMilestones, err := countRepoClosedMilestones(sess, repo.ID)
  447. if err != nil {
  448. return err
  449. }
  450. repo.NumMilestones = int(numMilestones)
  451. repo.NumClosedMilestones = int(numClosedMilestones)
  452. if _, err = sess.ID(repo.ID).Cols("num_milestones, num_closed_milestones").Update(repo); err != nil {
  453. return err
  454. }
  455. if _, err = sess.Exec("UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
  456. return err
  457. }
  458. return sess.Commit()
  459. }
  460. // CountMilestones map from repo conditions to number of milestones matching the options`
  461. func CountMilestones(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
  462. sess := x.Where("is_closed = ?", isClosed)
  463. if repoCond.IsValid() {
  464. sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
  465. }
  466. countsSlice := make([]*struct {
  467. RepoID int64
  468. Count int64
  469. }, 0, 10)
  470. if err := sess.GroupBy("repo_id").
  471. Select("repo_id AS repo_id, COUNT(*) AS count").
  472. Table("milestone").
  473. Find(&countsSlice); err != nil {
  474. return nil, err
  475. }
  476. countMap := make(map[int64]int64, len(countsSlice))
  477. for _, c := range countsSlice {
  478. countMap[c.RepoID] = c.Count
  479. }
  480. return countMap, nil
  481. }
  482. // CountMilestonesByRepoIDs map from repoIDs to number of milestones matching the options`
  483. func CountMilestonesByRepoIDs(repoIDs []int64, isClosed bool) (map[int64]int64, error) {
  484. return CountMilestones(
  485. builder.In("repo_id", repoIDs),
  486. isClosed,
  487. )
  488. }
  489. // SearchMilestones search milestones
  490. func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType string) (MilestoneList, error) {
  491. miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
  492. sess := x.Where("is_closed = ?", isClosed)
  493. if repoCond.IsValid() {
  494. sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
  495. }
  496. if page > 0 {
  497. sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
  498. }
  499. switch sortType {
  500. case "furthestduedate":
  501. sess.Desc("deadline_unix")
  502. case "leastcomplete":
  503. sess.Asc("completeness")
  504. case "mostcomplete":
  505. sess.Desc("completeness")
  506. case "leastissues":
  507. sess.Asc("num_issues")
  508. case "mostissues":
  509. sess.Desc("num_issues")
  510. default:
  511. sess.Asc("deadline_unix")
  512. }
  513. return miles, sess.Find(&miles)
  514. }
  515. // GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
  516. func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
  517. return SearchMilestones(
  518. builder.In("repo_id", repoIDs),
  519. page,
  520. isClosed,
  521. sortType,
  522. )
  523. }
  524. // MilestonesStats represents milestone statistic information.
  525. type MilestonesStats struct {
  526. OpenCount, ClosedCount int64
  527. }
  528. // Total returns the total counts of milestones
  529. func (m MilestonesStats) Total() int64 {
  530. return m.OpenCount + m.ClosedCount
  531. }
  532. // GetMilestonesStats returns milestone statistic information for dashboard by given conditions.
  533. func GetMilestonesStats(repoCond builder.Cond) (*MilestonesStats, error) {
  534. var err error
  535. stats := &MilestonesStats{}
  536. sess := x.Where("is_closed = ?", false)
  537. if repoCond.IsValid() {
  538. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  539. }
  540. stats.OpenCount, err = sess.Count(new(Milestone))
  541. if err != nil {
  542. return nil, err
  543. }
  544. sess = x.Where("is_closed = ?", true)
  545. if repoCond.IsValid() {
  546. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  547. }
  548. stats.ClosedCount, err = sess.Count(new(Milestone))
  549. if err != nil {
  550. return nil, err
  551. }
  552. return stats, nil
  553. }