milestone.go 9.8 KB

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