milestone.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. // Copyright 2015 - Present, The Gogs Authors. All rights reserved.
  2. // Copyright 2018 - Present, Gitote. All rights reserved.
  3. //
  4. // This source code is licensed under the MIT license found in the
  5. // LICENSE file in the root directory of this source tree.
  6. package models
  7. import (
  8. "fmt"
  9. "gitote/gitote/pkg/setting"
  10. "time"
  11. raven "github.com/getsentry/raven-go"
  12. "github.com/go-xorm/xorm"
  13. api "gitlab.com/gitote/go-gitote-client"
  14. log "gopkg.in/clog.v1"
  15. )
  16. // Milestone represents a milestone of repository.
  17. type Milestone struct {
  18. ID int64
  19. RepoID int64 `xorm:"INDEX"`
  20. Name string
  21. Content string `xorm:"TEXT"`
  22. RenderedContent string `xorm:"-" json:"-"`
  23. IsClosed bool
  24. NumIssues int
  25. NumClosedIssues int
  26. NumOpenIssues int `xorm:"-" json:"-"`
  27. Completeness int // Percentage(1-100).
  28. IsOverDue bool `xorm:"-" json:"-"`
  29. DeadlineString string `xorm:"-" json:"-"`
  30. Deadline time.Time `xorm:"-" json:"-"`
  31. DeadlineUnix int64
  32. ClosedDate time.Time `xorm:"-" json:"-"`
  33. ClosedDateUnix int64
  34. }
  35. // BeforeInsert will be invoked by XORM before inserting a record
  36. func (m *Milestone) BeforeInsert() {
  37. m.DeadlineUnix = m.Deadline.Unix()
  38. }
  39. // BeforeUpdate is invoked from XORM before updating this object.
  40. func (m *Milestone) BeforeUpdate() {
  41. if m.NumIssues > 0 {
  42. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  43. } else {
  44. m.Completeness = 0
  45. }
  46. m.DeadlineUnix = m.Deadline.Unix()
  47. m.ClosedDateUnix = m.ClosedDate.Unix()
  48. }
  49. // AfterSet is invoked from XORM after setting the values of all fields of this object.
  50. func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
  51. switch colName {
  52. case "num_closed_issues":
  53. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  54. case "deadline_unix":
  55. m.Deadline = time.Unix(m.DeadlineUnix, 0).Local()
  56. if m.Deadline.Year() == 9999 {
  57. return
  58. }
  59. m.DeadlineString = m.Deadline.Format("2006-01-02")
  60. if time.Now().Local().After(m.Deadline) {
  61. m.IsOverDue = true
  62. }
  63. case "closed_date_unix":
  64. m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local()
  65. }
  66. }
  67. // State returns string representation of milestone status.
  68. func (m *Milestone) State() api.StateType {
  69. if m.IsClosed {
  70. return api.STATE_CLOSED
  71. }
  72. return api.STATE_OPEN
  73. }
  74. // ChangeStatus changes milestone status to open or closed.
  75. func (m *Milestone) ChangeStatus(isClosed bool) error {
  76. return ChangeMilestoneStatus(m, isClosed)
  77. }
  78. // APIFormat returns this Milestone in API format.
  79. func (m *Milestone) APIFormat() *api.Milestone {
  80. apiMilestone := &api.Milestone{
  81. ID: m.ID,
  82. State: m.State(),
  83. Title: m.Name,
  84. Description: m.Content,
  85. OpenIssues: m.NumOpenIssues,
  86. ClosedIssues: m.NumClosedIssues,
  87. }
  88. if m.IsClosed {
  89. apiMilestone.Closed = &m.ClosedDate
  90. }
  91. if m.Deadline.Year() < 9999 {
  92. apiMilestone.Deadline = &m.Deadline
  93. }
  94. return apiMilestone
  95. }
  96. // CountIssues counts issues in milestone.
  97. func (m *Milestone) CountIssues(isClosed, includePulls bool) int64 {
  98. sess := x.Where("milestone_id = ?", m.ID).And("is_closed = ?", isClosed)
  99. if !includePulls {
  100. sess.And("is_pull = ?", false)
  101. }
  102. count, _ := sess.Count(new(Issue))
  103. return count
  104. }
  105. // NewMilestone creates new milestone of repository.
  106. func NewMilestone(m *Milestone) (err error) {
  107. sess := x.NewSession()
  108. defer sess.Close()
  109. if err = sess.Begin(); err != nil {
  110. return err
  111. }
  112. if _, err = sess.Insert(m); err != nil {
  113. return err
  114. }
  115. if _, err = sess.Exec("UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil {
  116. return err
  117. }
  118. return sess.Commit()
  119. }
  120. func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
  121. m := &Milestone{
  122. ID: id,
  123. RepoID: repoID,
  124. }
  125. has, err := e.Get(m)
  126. if err != nil {
  127. return nil, err
  128. } else if !has {
  129. return nil, ErrMilestoneNotExist{id, repoID}
  130. }
  131. return m, nil
  132. }
  133. // GetMilestoneByRepoID returns the milestone in a repository.
  134. func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
  135. return getMilestoneByRepoID(x, repoID, id)
  136. }
  137. // GetMilestonesByRepoID returns all milestones of a repository.
  138. func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) {
  139. miles := make([]*Milestone, 0, 10)
  140. return miles, x.Where("repo_id = ?", repoID).Find(&miles)
  141. }
  142. // GetMilestones returns a list of milestones of given repository and status.
  143. func GetMilestones(repoID int64, page int, isClosed bool) ([]*Milestone, error) {
  144. miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
  145. sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
  146. if page > 0 {
  147. sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
  148. }
  149. return miles, sess.Find(&miles)
  150. }
  151. func updateMilestone(e Engine, m *Milestone) error {
  152. _, err := e.ID(m.ID).AllCols().Update(m)
  153. return err
  154. }
  155. // UpdateMilestone updates information of given milestone.
  156. func UpdateMilestone(m *Milestone) error {
  157. return updateMilestone(x, m)
  158. }
  159. func countRepoMilestones(e Engine, repoID int64) int64 {
  160. count, _ := e.Where("repo_id=?", repoID).Count(new(Milestone))
  161. return count
  162. }
  163. // CountRepoMilestones returns number of milestones in given repository.
  164. func CountRepoMilestones(repoID int64) int64 {
  165. return countRepoMilestones(x, repoID)
  166. }
  167. func countRepoClosedMilestones(e Engine, repoID int64) int64 {
  168. closed, _ := e.Where("repo_id=? AND is_closed=?", repoID, true).Count(new(Milestone))
  169. return closed
  170. }
  171. // CountRepoClosedMilestones returns number of closed milestones in given repository.
  172. func CountRepoClosedMilestones(repoID int64) int64 {
  173. return countRepoClosedMilestones(x, repoID)
  174. }
  175. // MilestoneStats returns number of open and closed milestones of given repository.
  176. func MilestoneStats(repoID int64) (open int64, closed int64) {
  177. open, _ = x.Where("repo_id=? AND is_closed=?", repoID, false).Count(new(Milestone))
  178. return open, CountRepoClosedMilestones(repoID)
  179. }
  180. // ChangeMilestoneStatus changes the milestone open/closed status.
  181. // If milestone passes with changed values, those values will be
  182. // updated to database as well.
  183. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  184. repo, err := GetRepositoryByID(m.RepoID)
  185. if err != nil {
  186. return err
  187. }
  188. sess := x.NewSession()
  189. defer sess.Close()
  190. if err = sess.Begin(); err != nil {
  191. return err
  192. }
  193. m.IsClosed = isClosed
  194. if err = updateMilestone(sess, m); err != nil {
  195. return err
  196. }
  197. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  198. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  199. if _, err = sess.ID(repo.ID).AllCols().Update(repo); err != nil {
  200. return err
  201. }
  202. return sess.Commit()
  203. }
  204. func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error {
  205. if issue.MilestoneID == 0 {
  206. return nil
  207. }
  208. m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
  209. if err != nil {
  210. return err
  211. }
  212. if issue.IsClosed {
  213. m.NumOpenIssues--
  214. m.NumClosedIssues++
  215. } else {
  216. m.NumOpenIssues++
  217. m.NumClosedIssues--
  218. }
  219. return updateMilestone(e, m)
  220. }
  221. // ChangeMilestoneIssueStats updates the open/closed issues counter and progress
  222. // for the milestone associated with the given issue.
  223. func ChangeMilestoneIssueStats(issue *Issue) (err error) {
  224. sess := x.NewSession()
  225. defer sess.Close()
  226. if err = sess.Begin(); err != nil {
  227. return err
  228. }
  229. if err = changeMilestoneIssueStats(sess, issue); err != nil {
  230. return err
  231. }
  232. return sess.Commit()
  233. }
  234. func changeMilestoneAssign(e *xorm.Session, issue *Issue, oldMilestoneID int64) error {
  235. if oldMilestoneID > 0 {
  236. m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID)
  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. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?", issue.ID); err != nil {
  247. return err
  248. }
  249. issue.Milestone = nil
  250. }
  251. if issue.MilestoneID > 0 {
  252. m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
  253. if err != nil {
  254. return err
  255. }
  256. m.NumIssues++
  257. if issue.IsClosed {
  258. m.NumClosedIssues++
  259. }
  260. if err = updateMilestone(e, m); err != nil {
  261. return err
  262. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?", m.ID, issue.ID); err != nil {
  263. return err
  264. }
  265. issue.Milestone = m
  266. }
  267. return updateIssue(e, issue)
  268. }
  269. // ChangeMilestoneAssign changes assignment of milestone for issue.
  270. func ChangeMilestoneAssign(doer *User, issue *Issue, oldMilestoneID int64) (err error) {
  271. sess := x.NewSession()
  272. defer sess.Close()
  273. if err = sess.Begin(); err != nil {
  274. return err
  275. }
  276. if err = changeMilestoneAssign(sess, issue, oldMilestoneID); err != nil {
  277. return err
  278. }
  279. if err = sess.Commit(); err != nil {
  280. return fmt.Errorf("Commit: %v", err)
  281. }
  282. var hookAction api.HookIssueAction
  283. if issue.MilestoneID > 0 {
  284. hookAction = api.HOOK_ISSUE_MILESTONED
  285. } else {
  286. hookAction = api.HOOK_ISSUE_DEMILESTONED
  287. }
  288. if issue.IsPull {
  289. err = issue.PullRequest.LoadIssue()
  290. if err != nil {
  291. raven.CaptureErrorAndWait(err, nil)
  292. log.Error(2, "LoadIssue: %v", err)
  293. return
  294. }
  295. err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
  296. Action: hookAction,
  297. Index: issue.Index,
  298. PullRequest: issue.PullRequest.APIFormat(),
  299. Repository: issue.Repo.APIFormat(nil),
  300. Sender: doer.APIFormat(),
  301. })
  302. } else {
  303. err = PrepareWebhooks(issue.Repo, HookEventIssues, &api.IssuesPayload{
  304. Action: hookAction,
  305. Index: issue.Index,
  306. Issue: issue.APIFormat(),
  307. Repository: issue.Repo.APIFormat(nil),
  308. Sender: doer.APIFormat(),
  309. })
  310. }
  311. if err != nil {
  312. raven.CaptureErrorAndWait(err, nil)
  313. log.Error(2, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
  314. }
  315. return nil
  316. }
  317. // DeleteMilestoneOfRepoByID deletes a milestone from a repository.
  318. func DeleteMilestoneOfRepoByID(repoID, id int64) error {
  319. m, err := GetMilestoneByRepoID(repoID, id)
  320. if err != nil {
  321. if IsErrMilestoneNotExist(err) {
  322. return nil
  323. }
  324. return err
  325. }
  326. repo, err := GetRepositoryByID(m.RepoID)
  327. if err != nil {
  328. return err
  329. }
  330. sess := x.NewSession()
  331. defer sess.Close()
  332. if err = sess.Begin(); err != nil {
  333. return err
  334. }
  335. if _, err = sess.ID(m.ID).Delete(new(Milestone)); err != nil {
  336. return err
  337. }
  338. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  339. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  340. if _, err = sess.ID(repo.ID).AllCols().Update(repo); err != nil {
  341. return err
  342. }
  343. if _, err = sess.Exec("UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
  344. return err
  345. } else if _, err = sess.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
  346. return err
  347. }
  348. return sess.Commit()
  349. }