gitea源码

milestone.go 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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 == 0 {
  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. return db.WithTx(ctx, func(ctx context.Context) error {
  93. m.Name = strings.TrimSpace(m.Name)
  94. if err = db.Insert(ctx, m); err != nil {
  95. return err
  96. }
  97. _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID)
  98. return err
  99. })
  100. }
  101. // HasMilestoneByRepoID returns if the milestone exists in the repository.
  102. func HasMilestoneByRepoID(ctx context.Context, repoID, id int64) (bool, error) {
  103. return db.GetEngine(ctx).ID(id).Where("repo_id=?", repoID).Exist(new(Milestone))
  104. }
  105. // GetMilestoneByRepoID returns the milestone in a repository.
  106. func GetMilestoneByRepoID(ctx context.Context, repoID, id int64) (*Milestone, error) {
  107. m := new(Milestone)
  108. has, err := db.GetEngine(ctx).ID(id).Where("repo_id=?", repoID).Get(m)
  109. if err != nil {
  110. return nil, err
  111. } else if !has {
  112. return nil, ErrMilestoneNotExist{ID: id, RepoID: repoID}
  113. }
  114. return m, nil
  115. }
  116. // GetMilestoneByRepoIDANDName return a milestone if one exist by name and repo
  117. func GetMilestoneByRepoIDANDName(ctx context.Context, repoID int64, name string) (*Milestone, error) {
  118. var mile Milestone
  119. has, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, name).Get(&mile)
  120. if err != nil {
  121. return nil, err
  122. }
  123. if !has {
  124. return nil, ErrMilestoneNotExist{Name: name, RepoID: repoID}
  125. }
  126. return &mile, nil
  127. }
  128. // UpdateMilestone updates information of given milestone.
  129. func UpdateMilestone(ctx context.Context, m *Milestone, oldIsClosed bool) error {
  130. return db.WithTx(ctx, func(ctx context.Context) error {
  131. if m.IsClosed && !oldIsClosed {
  132. m.ClosedDateUnix = timeutil.TimeStampNow()
  133. }
  134. if err := updateMilestone(ctx, m); err != nil {
  135. return err
  136. }
  137. // if IsClosed changed, update milestone numbers of repository
  138. if oldIsClosed != m.IsClosed {
  139. if err := updateRepoMilestoneNum(ctx, m.RepoID); err != nil {
  140. return err
  141. }
  142. }
  143. return nil
  144. })
  145. }
  146. func updateMilestone(ctx context.Context, m *Milestone) error {
  147. m.Name = strings.TrimSpace(m.Name)
  148. _, err := db.GetEngine(ctx).ID(m.ID).AllCols().Update(m)
  149. if err != nil {
  150. return err
  151. }
  152. return UpdateMilestoneCounters(ctx, m.ID)
  153. }
  154. // UpdateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness
  155. func UpdateMilestoneCounters(ctx context.Context, id int64) error {
  156. e := db.GetEngine(ctx)
  157. _, err := e.ID(id).
  158. SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
  159. builder.Eq{"milestone_id": id},
  160. )).
  161. SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where(
  162. builder.Eq{
  163. "milestone_id": id,
  164. "is_closed": true,
  165. },
  166. )).
  167. Update(&Milestone{})
  168. if err != nil {
  169. return err
  170. }
  171. _, err = e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?",
  172. id,
  173. )
  174. return err
  175. }
  176. // ChangeMilestoneStatusByRepoIDAndID changes a milestone open/closed status if the milestone ID is in the repo.
  177. func ChangeMilestoneStatusByRepoIDAndID(ctx context.Context, repoID, milestoneID int64, isClosed bool) error {
  178. return db.WithTx(ctx, func(ctx context.Context) error {
  179. m := &Milestone{
  180. ID: milestoneID,
  181. RepoID: repoID,
  182. }
  183. has, err := db.GetEngine(ctx).ID(milestoneID).Where("repo_id = ?", repoID).Get(m)
  184. if err != nil {
  185. return err
  186. } else if !has {
  187. return ErrMilestoneNotExist{ID: milestoneID, RepoID: repoID}
  188. }
  189. return changeMilestoneStatus(ctx, m, isClosed)
  190. })
  191. }
  192. // ChangeMilestoneStatus changes the milestone open/closed status.
  193. func ChangeMilestoneStatus(ctx context.Context, m *Milestone, isClosed bool) (err error) {
  194. return db.WithTx(ctx, func(ctx context.Context) error {
  195. return changeMilestoneStatus(ctx, m, isClosed)
  196. })
  197. }
  198. func changeMilestoneStatus(ctx context.Context, m *Milestone, isClosed bool) error {
  199. m.IsClosed = isClosed
  200. if isClosed {
  201. m.ClosedDateUnix = timeutil.TimeStampNow()
  202. }
  203. 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)
  204. if err != nil {
  205. return err
  206. }
  207. if count < 1 {
  208. return nil
  209. }
  210. return updateRepoMilestoneNum(ctx, m.RepoID)
  211. }
  212. // DeleteMilestoneByRepoID deletes a milestone from a repository.
  213. func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
  214. m, err := GetMilestoneByRepoID(ctx, repoID, id)
  215. if err != nil {
  216. if IsErrMilestoneNotExist(err) {
  217. return nil
  218. }
  219. return err
  220. }
  221. repo, err := repo_model.GetRepositoryByID(ctx, m.RepoID)
  222. if err != nil {
  223. return err
  224. }
  225. return db.WithTx(ctx, func(ctx context.Context) error {
  226. if _, err = db.DeleteByID[Milestone](ctx, m.ID); err != nil {
  227. return err
  228. }
  229. numMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
  230. RepoID: repo.ID,
  231. })
  232. if err != nil {
  233. return err
  234. }
  235. numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
  236. RepoID: repo.ID,
  237. IsClosed: optional.Some(true),
  238. })
  239. if err != nil {
  240. return err
  241. }
  242. repo.NumMilestones = int(numMilestones)
  243. repo.NumClosedMilestones = int(numClosedMilestones)
  244. if _, err = db.GetEngine(ctx).ID(repo.ID).Cols("num_milestones, num_closed_milestones").Update(repo); err != nil {
  245. return err
  246. }
  247. _, err = db.Exec(ctx, "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID)
  248. return err
  249. })
  250. }
  251. func updateRepoMilestoneNum(ctx context.Context, repoID int64) error {
  252. _, 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=?",
  253. repoID,
  254. repoID,
  255. true,
  256. repoID,
  257. )
  258. return err
  259. }
  260. // LoadTotalTrackedTime loads the tracked time for the milestone
  261. func (m *Milestone) LoadTotalTrackedTime(ctx context.Context) error {
  262. type totalTimesByMilestone struct {
  263. MilestoneID int64
  264. Time int64
  265. }
  266. totalTime := &totalTimesByMilestone{MilestoneID: m.ID}
  267. has, err := db.GetEngine(ctx).Table("issue").
  268. Join("INNER", "milestone", "issue.milestone_id = milestone.id").
  269. Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
  270. Where("tracked_time.deleted = ?", false).
  271. Select("milestone_id, sum(time) as time").
  272. Where("milestone_id = ?", m.ID).
  273. GroupBy("milestone_id").
  274. Get(totalTime)
  275. if err != nil {
  276. return err
  277. } else if !has {
  278. return nil
  279. }
  280. m.TotalTrackedTime = totalTime.Time
  281. return nil
  282. }
  283. // InsertMilestones creates milestones of repository.
  284. func InsertMilestones(ctx context.Context, ms ...*Milestone) (err error) {
  285. if len(ms) == 0 {
  286. return nil
  287. }
  288. return db.WithTx(ctx, func(ctx context.Context) error {
  289. // to return the id, so we should not use batch insert
  290. for _, m := range ms {
  291. if _, err = db.GetEngine(ctx).NoAutoTime().Insert(m); err != nil {
  292. return err
  293. }
  294. }
  295. _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID)
  296. return err
  297. })
  298. }