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 10KB

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