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.

milestone.go 10.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "fmt"
  7. "strings"
  8. "code.gitea.io/gitea/models/db"
  9. repo_model "code.gitea.io/gitea/models/repo"
  10. api "code.gitea.io/gitea/modules/structs"
  11. "code.gitea.io/gitea/modules/timeutil"
  12. "code.gitea.io/gitea/modules/util"
  13. "xorm.io/builder"
  14. )
  15. // ErrMilestoneNotExist represents a "MilestoneNotExist" kind of error.
  16. type ErrMilestoneNotExist struct {
  17. ID int64
  18. RepoID int64
  19. Name string
  20. }
  21. // IsErrMilestoneNotExist checks if an error is a ErrMilestoneNotExist.
  22. func IsErrMilestoneNotExist(err error) bool {
  23. _, ok := err.(ErrMilestoneNotExist)
  24. return ok
  25. }
  26. func (err ErrMilestoneNotExist) Error() string {
  27. if len(err.Name) > 0 {
  28. return fmt.Sprintf("milestone does not exist [name: %s, repo_id: %d]", err.Name, err.RepoID)
  29. }
  30. return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID)
  31. }
  32. func (err ErrMilestoneNotExist) Unwrap() error {
  33. return util.ErrNotExist
  34. }
  35. // Milestone represents a milestone of repository.
  36. type Milestone struct {
  37. ID int64 `xorm:"pk autoincr"`
  38. RepoID int64 `xorm:"INDEX"`
  39. Repo *repo_model.Repository `xorm:"-"`
  40. Name string
  41. Content string `xorm:"TEXT"`
  42. RenderedContent string `xorm:"-"`
  43. IsClosed bool
  44. NumIssues int
  45. NumClosedIssues int
  46. NumOpenIssues int `xorm:"-"`
  47. Completeness int // Percentage(1-100).
  48. IsOverdue bool `xorm:"-"`
  49. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  50. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  51. DeadlineUnix timeutil.TimeStamp
  52. ClosedDateUnix timeutil.TimeStamp
  53. DeadlineString string `xorm:"-"`
  54. TotalTrackedTime int64 `xorm:"-"`
  55. }
  56. func init() {
  57. db.RegisterModel(new(Milestone))
  58. }
  59. // BeforeUpdate is invoked from XORM before updating this object.
  60. func (m *Milestone) BeforeUpdate() {
  61. if m.NumIssues > 0 {
  62. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  63. } else {
  64. m.Completeness = 0
  65. }
  66. }
  67. // AfterLoad is invoked from XORM after setting the value of a field of
  68. // this object.
  69. func (m *Milestone) AfterLoad() {
  70. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  71. if m.DeadlineUnix.Year() == 9999 {
  72. return
  73. }
  74. m.DeadlineString = m.DeadlineUnix.Format("2006-01-02")
  75. if m.IsClosed {
  76. m.IsOverdue = m.ClosedDateUnix >= m.DeadlineUnix
  77. } else {
  78. m.IsOverdue = timeutil.TimeStampNow() >= m.DeadlineUnix
  79. }
  80. }
  81. // State returns string representation of milestone status.
  82. func (m *Milestone) State() api.StateType {
  83. if m.IsClosed {
  84. return api.StateClosed
  85. }
  86. return api.StateOpen
  87. }
  88. // NewMilestone creates new milestone of repository.
  89. func NewMilestone(ctx context.Context, m *Milestone) (err error) {
  90. ctx, committer, err := db.TxContext(ctx)
  91. if err != nil {
  92. return err
  93. }
  94. defer committer.Close()
  95. m.Name = strings.TrimSpace(m.Name)
  96. if err = db.Insert(ctx, m); err != nil {
  97. return err
  98. }
  99. if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil {
  100. return err
  101. }
  102. return committer.Commit()
  103. }
  104. // HasMilestoneByRepoID returns if the milestone exists in the repository.
  105. func HasMilestoneByRepoID(ctx context.Context, repoID, id int64) (bool, error) {
  106. return db.GetEngine(ctx).ID(id).Where("repo_id=?", repoID).Exist(new(Milestone))
  107. }
  108. // GetMilestoneByRepoID returns the milestone in a repository.
  109. func GetMilestoneByRepoID(ctx context.Context, repoID, id int64) (*Milestone, error) {
  110. m := new(Milestone)
  111. has, err := db.GetEngine(ctx).ID(id).Where("repo_id=?", repoID).Get(m)
  112. if err != nil {
  113. return nil, err
  114. } else if !has {
  115. return nil, ErrMilestoneNotExist{ID: id, RepoID: repoID}
  116. }
  117. return m, nil
  118. }
  119. // GetMilestoneByRepoIDANDName return a milestone if one exist by name and repo
  120. func GetMilestoneByRepoIDANDName(ctx context.Context, repoID int64, name string) (*Milestone, error) {
  121. var mile Milestone
  122. has, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, name).Get(&mile)
  123. if err != nil {
  124. return nil, err
  125. }
  126. if !has {
  127. return nil, ErrMilestoneNotExist{Name: name, RepoID: repoID}
  128. }
  129. return &mile, nil
  130. }
  131. // UpdateMilestone updates information of given milestone.
  132. func UpdateMilestone(ctx context.Context, m *Milestone, oldIsClosed bool) error {
  133. ctx, committer, err := db.TxContext(ctx)
  134. if err != nil {
  135. return err
  136. }
  137. defer committer.Close()
  138. if m.IsClosed && !oldIsClosed {
  139. m.ClosedDateUnix = timeutil.TimeStampNow()
  140. }
  141. if err := updateMilestone(ctx, m); err != nil {
  142. return err
  143. }
  144. // if IsClosed changed, update milestone numbers of repository
  145. if oldIsClosed != m.IsClosed {
  146. if err := updateRepoMilestoneNum(ctx, m.RepoID); err != nil {
  147. return err
  148. }
  149. }
  150. return committer.Commit()
  151. }
  152. func updateMilestone(ctx context.Context, m *Milestone) error {
  153. m.Name = strings.TrimSpace(m.Name)
  154. _, err := db.GetEngine(ctx).ID(m.ID).AllCols().Update(m)
  155. if err != nil {
  156. return err
  157. }
  158. return UpdateMilestoneCounters(ctx, m.ID)
  159. }
  160. // UpdateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness
  161. func UpdateMilestoneCounters(ctx context.Context, id int64) error {
  162. e := db.GetEngine(ctx)
  163. _, err := e.ID(id).
  164. SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
  165. builder.Eq{"milestone_id": id},
  166. )).
  167. SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where(
  168. builder.Eq{
  169. "milestone_id": id,
  170. "is_closed": true,
  171. },
  172. )).
  173. Update(&Milestone{})
  174. if err != nil {
  175. return err
  176. }
  177. _, err = e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?",
  178. id,
  179. )
  180. return err
  181. }
  182. // ChangeMilestoneStatusByRepoIDAndID changes a milestone open/closed status if the milestone ID is in the repo.
  183. func ChangeMilestoneStatusByRepoIDAndID(ctx context.Context, repoID, milestoneID int64, isClosed bool) error {
  184. ctx, committer, err := db.TxContext(ctx)
  185. if err != nil {
  186. return err
  187. }
  188. defer committer.Close()
  189. m := &Milestone{
  190. ID: milestoneID,
  191. RepoID: repoID,
  192. }
  193. has, err := db.GetEngine(ctx).ID(milestoneID).Where("repo_id = ?", repoID).Get(m)
  194. if err != nil {
  195. return err
  196. } else if !has {
  197. return ErrMilestoneNotExist{ID: milestoneID, RepoID: repoID}
  198. }
  199. if err := changeMilestoneStatus(ctx, m, isClosed); err != nil {
  200. return err
  201. }
  202. return committer.Commit()
  203. }
  204. // ChangeMilestoneStatus changes the milestone open/closed status.
  205. func ChangeMilestoneStatus(ctx context.Context, m *Milestone, isClosed bool) (err error) {
  206. ctx, committer, err := db.TxContext(ctx)
  207. if err != nil {
  208. return err
  209. }
  210. defer committer.Close()
  211. if err := changeMilestoneStatus(ctx, m, isClosed); err != nil {
  212. return err
  213. }
  214. return committer.Commit()
  215. }
  216. func changeMilestoneStatus(ctx context.Context, m *Milestone, isClosed bool) error {
  217. m.IsClosed = isClosed
  218. if isClosed {
  219. m.ClosedDateUnix = timeutil.TimeStampNow()
  220. }
  221. count, err := db.GetEngine(ctx).ID(m.ID).Where("repo_id = ? AND is_closed = ?", m.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(m)
  222. if err != nil {
  223. return err
  224. }
  225. if count < 1 {
  226. return nil
  227. }
  228. return updateRepoMilestoneNum(ctx, m.RepoID)
  229. }
  230. // DeleteMilestoneByRepoID deletes a milestone from a repository.
  231. func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
  232. m, err := GetMilestoneByRepoID(ctx, repoID, id)
  233. if err != nil {
  234. if IsErrMilestoneNotExist(err) {
  235. return nil
  236. }
  237. return err
  238. }
  239. repo, err := repo_model.GetRepositoryByID(ctx, m.RepoID)
  240. if err != nil {
  241. return err
  242. }
  243. ctx, committer, err := db.TxContext(ctx)
  244. if err != nil {
  245. return err
  246. }
  247. defer committer.Close()
  248. sess := db.GetEngine(ctx)
  249. if _, err = sess.ID(m.ID).Delete(new(Milestone)); err != nil {
  250. return err
  251. }
  252. numMilestones, err := CountMilestones(ctx, GetMilestonesOption{
  253. RepoID: repo.ID,
  254. State: api.StateAll,
  255. })
  256. if err != nil {
  257. return err
  258. }
  259. numClosedMilestones, err := CountMilestones(ctx, GetMilestonesOption{
  260. RepoID: repo.ID,
  261. State: api.StateClosed,
  262. })
  263. if err != nil {
  264. return err
  265. }
  266. repo.NumMilestones = int(numMilestones)
  267. repo.NumClosedMilestones = int(numClosedMilestones)
  268. if _, err = sess.ID(repo.ID).Cols("num_milestones, num_closed_milestones").Update(repo); err != nil {
  269. return err
  270. }
  271. if _, err = db.Exec(ctx, "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
  272. return err
  273. }
  274. return committer.Commit()
  275. }
  276. func updateRepoMilestoneNum(ctx context.Context, repoID int64) error {
  277. _, err := db.GetEngine(ctx).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=?",
  278. repoID,
  279. repoID,
  280. true,
  281. repoID,
  282. )
  283. return err
  284. }
  285. // LoadTotalTrackedTime loads the tracked time for the milestone
  286. func (m *Milestone) LoadTotalTrackedTime(ctx context.Context) error {
  287. type totalTimesByMilestone struct {
  288. MilestoneID int64
  289. Time int64
  290. }
  291. totalTime := &totalTimesByMilestone{MilestoneID: m.ID}
  292. has, err := db.GetEngine(ctx).Table("issue").
  293. Join("INNER", "milestone", "issue.milestone_id = milestone.id").
  294. Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
  295. Where("tracked_time.deleted = ?", false).
  296. Select("milestone_id, sum(time) as time").
  297. Where("milestone_id = ?", m.ID).
  298. GroupBy("milestone_id").
  299. Get(totalTime)
  300. if err != nil {
  301. return err
  302. } else if !has {
  303. return nil
  304. }
  305. m.TotalTrackedTime = totalTime.Time
  306. return nil
  307. }
  308. // InsertMilestones creates milestones of repository.
  309. func InsertMilestones(ctx context.Context, ms ...*Milestone) (err error) {
  310. if len(ms) == 0 {
  311. return nil
  312. }
  313. ctx, committer, err := db.TxContext(ctx)
  314. if err != nil {
  315. return err
  316. }
  317. defer committer.Close()
  318. sess := db.GetEngine(ctx)
  319. // to return the id, so we should not use batch insert
  320. for _, m := range ms {
  321. if _, err = sess.NoAutoTime().Insert(m); err != nil {
  322. return err
  323. }
  324. }
  325. if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID); err != nil {
  326. return err
  327. }
  328. return committer.Commit()
  329. }