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

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