milestone.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. package database
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/cockroachdb/errors"
  6. api "github.com/gogs/go-gogs-client"
  7. "gorm.io/gorm"
  8. log "unknwon.dev/clog/v2"
  9. "gogs.io/gogs/internal/conf"
  10. "gogs.io/gogs/internal/errutil"
  11. )
  12. // Milestone represents a milestone of repository.
  13. type Milestone struct {
  14. ID int64
  15. RepoID int64 `gorm:"index"`
  16. Name string
  17. Content string `gorm:"type:text"`
  18. RenderedContent string `gorm:"-" json:"-"`
  19. IsClosed bool
  20. NumIssues int
  21. NumClosedIssues int
  22. NumOpenIssues int `gorm:"-" json:"-"`
  23. Completeness int // Percentage(1-100).
  24. IsOverDue bool `gorm:"-" json:"-"`
  25. DeadlineString string `gorm:"-" json:"-"`
  26. Deadline time.Time `gorm:"-" json:"-"`
  27. DeadlineUnix int64
  28. ClosedDate time.Time `gorm:"-" json:"-"`
  29. ClosedDateUnix int64
  30. }
  31. func (m *Milestone) BeforeCreate(tx *gorm.DB) error {
  32. m.DeadlineUnix = m.Deadline.Unix()
  33. return nil
  34. }
  35. func (m *Milestone) BeforeUpdate(tx *gorm.DB) error {
  36. if m.NumIssues > 0 {
  37. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  38. } else {
  39. m.Completeness = 0
  40. }
  41. m.DeadlineUnix = m.Deadline.Unix()
  42. m.ClosedDateUnix = m.ClosedDate.Unix()
  43. return nil
  44. }
  45. func (m *Milestone) AfterFind(tx *gorm.DB) error {
  46. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  47. m.Deadline = time.Unix(m.DeadlineUnix, 0).Local()
  48. if m.Deadline.Year() != 9999 {
  49. m.DeadlineString = m.Deadline.Format("2006-01-02")
  50. if time.Now().Local().After(m.Deadline) {
  51. m.IsOverDue = true
  52. }
  53. }
  54. m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local()
  55. return nil
  56. }
  57. // State returns string representation of milestone status.
  58. func (m *Milestone) State() api.StateType {
  59. if m.IsClosed {
  60. return api.STATE_CLOSED
  61. }
  62. return api.STATE_OPEN
  63. }
  64. func (m *Milestone) ChangeStatus(isClosed bool) error {
  65. return ChangeMilestoneStatus(m, isClosed)
  66. }
  67. func (m *Milestone) APIFormat() *api.Milestone {
  68. apiMilestone := &api.Milestone{
  69. ID: m.ID,
  70. State: m.State(),
  71. Title: m.Name,
  72. Description: m.Content,
  73. OpenIssues: m.NumOpenIssues,
  74. ClosedIssues: m.NumClosedIssues,
  75. }
  76. if m.IsClosed {
  77. apiMilestone.Closed = &m.ClosedDate
  78. }
  79. if m.Deadline.Year() < 9999 {
  80. apiMilestone.Deadline = &m.Deadline
  81. }
  82. return apiMilestone
  83. }
  84. func (m *Milestone) CountIssues(isClosed, includePulls bool) int64 {
  85. query := db.Model(new(Issue)).Where("milestone_id = ? AND is_closed = ?", m.ID, isClosed)
  86. if !includePulls {
  87. query = query.Where("is_pull = ?", false)
  88. }
  89. var count int64
  90. query.Count(&count)
  91. return count
  92. }
  93. // NewMilestone creates new milestone of repository.
  94. func NewMilestone(m *Milestone) (err error) {
  95. return db.Transaction(func(tx *gorm.DB) error {
  96. if err := tx.Create(m).Error; err != nil {
  97. return err
  98. }
  99. return tx.Exec("UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID).Error
  100. })
  101. }
  102. var _ errutil.NotFound = (*ErrMilestoneNotExist)(nil)
  103. type ErrMilestoneNotExist struct {
  104. args map[string]any
  105. }
  106. func IsErrMilestoneNotExist(err error) bool {
  107. _, ok := err.(ErrMilestoneNotExist)
  108. return ok
  109. }
  110. func (err ErrMilestoneNotExist) Error() string {
  111. return fmt.Sprintf("milestone does not exist: %v", err.args)
  112. }
  113. func (ErrMilestoneNotExist) NotFound() bool {
  114. return true
  115. }
  116. func getMilestoneByRepoID(e *gorm.DB, repoID, id int64) (*Milestone, error) {
  117. m := &Milestone{}
  118. err := e.Where("id = ? AND repo_id = ?", id, repoID).First(m).Error
  119. if err != nil {
  120. if errors.Is(err, gorm.ErrRecordNotFound) {
  121. return nil, ErrMilestoneNotExist{args: map[string]any{"repoID": repoID, "milestoneID": id}}
  122. }
  123. return nil, err
  124. }
  125. return m, nil
  126. }
  127. // GetWebhookByRepoID returns the milestone in a repository.
  128. func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
  129. return getMilestoneByRepoID(db, repoID, id)
  130. }
  131. // GetMilestonesByRepoID returns all milestones of a repository.
  132. func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) {
  133. miles := make([]*Milestone, 0, 10)
  134. return miles, db.Where("repo_id = ?", repoID).Find(&miles).Error
  135. }
  136. // GetMilestones returns a list of milestones of given repository and status.
  137. func GetMilestones(repoID int64, page int, isClosed bool) ([]*Milestone, error) {
  138. miles := make([]*Milestone, 0, conf.UI.IssuePagingNum)
  139. query := db.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
  140. if page > 0 {
  141. query = query.Limit(conf.UI.IssuePagingNum).Offset((page - 1) * conf.UI.IssuePagingNum)
  142. }
  143. return miles, query.Find(&miles).Error
  144. }
  145. func updateMilestone(e *gorm.DB, m *Milestone) error {
  146. return e.Model(m).Where("id = ?", m.ID).Updates(m).Error
  147. }
  148. // UpdateMilestone updates information of given milestone.
  149. func UpdateMilestone(m *Milestone) error {
  150. return updateMilestone(db, m)
  151. }
  152. func countRepoMilestones(e *gorm.DB, repoID int64) int64 {
  153. var count int64
  154. e.Model(new(Milestone)).Where("repo_id = ?", repoID).Count(&count)
  155. return count
  156. }
  157. // CountRepoMilestones returns number of milestones in given repository.
  158. func CountRepoMilestones(repoID int64) int64 {
  159. return countRepoMilestones(db, repoID)
  160. }
  161. func countRepoClosedMilestones(e *gorm.DB, repoID int64) int64 {
  162. var count int64
  163. e.Model(new(Milestone)).Where("repo_id = ? AND is_closed = ?", repoID, true).Count(&count)
  164. return count
  165. }
  166. // CountRepoClosedMilestones returns number of closed milestones in given repository.
  167. func CountRepoClosedMilestones(repoID int64) int64 {
  168. return countRepoClosedMilestones(db, repoID)
  169. }
  170. // MilestoneStats returns number of open and closed milestones of given repository.
  171. func MilestoneStats(repoID int64) (open, closed int64) {
  172. db.Model(new(Milestone)).Where("repo_id = ? AND is_closed = ?", repoID, false).Count(&open)
  173. return open, CountRepoClosedMilestones(repoID)
  174. }
  175. // ChangeMilestoneStatus changes the milestone open/closed status.
  176. // If milestone passes with changed values, those values will be
  177. // updated to database as well.
  178. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  179. repo, err := GetRepositoryByID(m.RepoID)
  180. if err != nil {
  181. return err
  182. }
  183. return db.Transaction(func(tx *gorm.DB) error {
  184. m.IsClosed = isClosed
  185. if err := updateMilestone(tx, m); err != nil {
  186. return err
  187. }
  188. repo.NumMilestones = int(countRepoMilestones(tx, repo.ID))
  189. repo.NumClosedMilestones = int(countRepoClosedMilestones(tx, repo.ID))
  190. return tx.Model(repo).Where("id = ?", repo.ID).Updates(repo).Error
  191. })
  192. }
  193. func changeMilestoneIssueStats(e *gorm.DB, issue *Issue) error {
  194. if issue.MilestoneID == 0 {
  195. return nil
  196. }
  197. m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
  198. if err != nil {
  199. return err
  200. }
  201. if issue.IsClosed {
  202. m.NumOpenIssues--
  203. m.NumClosedIssues++
  204. } else {
  205. m.NumOpenIssues++
  206. m.NumClosedIssues--
  207. }
  208. return updateMilestone(e, m)
  209. }
  210. // ChangeMilestoneIssueStats updates the open/closed issues counter and progress
  211. // for the milestone associated with the given issue.
  212. func ChangeMilestoneIssueStats(issue *Issue) (err error) {
  213. return db.Transaction(func(tx *gorm.DB) error {
  214. return changeMilestoneIssueStats(tx, issue)
  215. })
  216. }
  217. func changeMilestoneAssign(e *gorm.DB, issue *Issue, oldMilestoneID int64) error {
  218. if oldMilestoneID > 0 {
  219. m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID)
  220. if err != nil {
  221. return err
  222. }
  223. m.NumIssues--
  224. if issue.IsClosed {
  225. m.NumClosedIssues--
  226. }
  227. if err = updateMilestone(e, m); err != nil {
  228. return err
  229. }
  230. if err = e.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?", issue.ID).Error; err != nil {
  231. return err
  232. }
  233. issue.Milestone = nil
  234. }
  235. if issue.MilestoneID > 0 {
  236. m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
  237. if err != nil {
  238. return err
  239. }
  240. m.NumIssues++
  241. if issue.IsClosed {
  242. m.NumClosedIssues++
  243. }
  244. if err = updateMilestone(e, m); err != nil {
  245. return err
  246. }
  247. if err = e.Exec("UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?", m.ID, issue.ID).Error; err != nil {
  248. return err
  249. }
  250. issue.Milestone = m
  251. }
  252. return updateIssue(e, issue)
  253. }
  254. // ChangeMilestoneAssign changes assignment of milestone for issue.
  255. func ChangeMilestoneAssign(doer *User, issue *Issue, oldMilestoneID int64) (err error) {
  256. err = db.Transaction(func(tx *gorm.DB) error {
  257. return changeMilestoneAssign(tx, issue, oldMilestoneID)
  258. })
  259. if err != nil {
  260. return errors.Newf("transaction: %v", err)
  261. }
  262. var hookAction api.HookIssueAction
  263. if issue.MilestoneID > 0 {
  264. hookAction = api.HOOK_ISSUE_MILESTONED
  265. } else {
  266. hookAction = api.HOOK_ISSUE_DEMILESTONED
  267. }
  268. if issue.IsPull {
  269. err = issue.PullRequest.LoadIssue()
  270. if err != nil {
  271. log.Error("LoadIssue: %v", err)
  272. return err
  273. }
  274. err = PrepareWebhooks(issue.Repo, HookEventTypePullRequest, &api.PullRequestPayload{
  275. Action: hookAction,
  276. Index: issue.Index,
  277. PullRequest: issue.PullRequest.APIFormat(),
  278. Repository: issue.Repo.APIFormatLegacy(nil),
  279. Sender: doer.APIFormat(),
  280. })
  281. } else {
  282. err = PrepareWebhooks(issue.Repo, HookEventTypeIssues, &api.IssuesPayload{
  283. Action: hookAction,
  284. Index: issue.Index,
  285. Issue: issue.APIFormat(),
  286. Repository: issue.Repo.APIFormatLegacy(nil),
  287. Sender: doer.APIFormat(),
  288. })
  289. }
  290. if err != nil {
  291. log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
  292. }
  293. return nil
  294. }
  295. // DeleteMilestoneOfRepoByID deletes a milestone from a repository.
  296. func DeleteMilestoneOfRepoByID(repoID, id int64) error {
  297. m, err := GetMilestoneByRepoID(repoID, id)
  298. if err != nil {
  299. if IsErrMilestoneNotExist(err) {
  300. return nil
  301. }
  302. return err
  303. }
  304. repo, err := GetRepositoryByID(m.RepoID)
  305. if err != nil {
  306. return err
  307. }
  308. return db.Transaction(func(tx *gorm.DB) error {
  309. if err := tx.Where("id = ?", m.ID).Delete(new(Milestone)).Error; err != nil {
  310. return err
  311. }
  312. repo.NumMilestones = int(countRepoMilestones(tx, repo.ID))
  313. repo.NumClosedMilestones = int(countRepoClosedMilestones(tx, repo.ID))
  314. if err := tx.Model(repo).Where("id = ?", repo.ID).Updates(repo).Error; err != nil {
  315. return err
  316. }
  317. if err := tx.Exec("UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID).Error; err != nil {
  318. return err
  319. }
  320. return tx.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE milestone_id = ?", m.ID).Error
  321. })
  322. }