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.

1494 lines
38 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
10 years ago
8 years ago
10 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "fmt"
  7. "path"
  8. "regexp"
  9. "sort"
  10. "strings"
  11. "code.gitea.io/gitea/modules/base"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/setting"
  14. "code.gitea.io/gitea/modules/util"
  15. api "code.gitea.io/sdk/gitea"
  16. "github.com/Unknwon/com"
  17. "github.com/go-xorm/builder"
  18. "github.com/go-xorm/xorm"
  19. )
  20. // Issue represents an issue or pull request of repository.
  21. type Issue struct {
  22. ID int64 `xorm:"pk autoincr"`
  23. RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
  24. Repo *Repository `xorm:"-"`
  25. Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
  26. PosterID int64 `xorm:"INDEX"`
  27. Poster *User `xorm:"-"`
  28. Title string `xorm:"name"`
  29. Content string `xorm:"TEXT"`
  30. RenderedContent string `xorm:"-"`
  31. Labels []*Label `xorm:"-"`
  32. MilestoneID int64 `xorm:"INDEX"`
  33. Milestone *Milestone `xorm:"-"`
  34. Priority int
  35. AssigneeID int64 `xorm:"INDEX"`
  36. Assignee *User `xorm:"-"`
  37. IsClosed bool `xorm:"INDEX"`
  38. IsRead bool `xorm:"-"`
  39. IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
  40. PullRequest *PullRequest `xorm:"-"`
  41. NumComments int
  42. Ref string
  43. DeadlineUnix util.TimeStamp `xorm:"INDEX"`
  44. CreatedUnix util.TimeStamp `xorm:"INDEX created"`
  45. UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
  46. Attachments []*Attachment `xorm:"-"`
  47. Comments []*Comment `xorm:"-"`
  48. Reactions ReactionList `xorm:"-"`
  49. }
  50. var (
  51. issueTasksPat *regexp.Regexp
  52. issueTasksDonePat *regexp.Regexp
  53. )
  54. const issueTasksRegexpStr = `(^\s*[-*]\s\[[\sx]\]\s.)|(\n\s*[-*]\s\[[\sx]\]\s.)`
  55. const issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[x]\]\s.)|(\n\s*[-*]\s\[[x]\]\s.)`
  56. func init() {
  57. issueTasksPat = regexp.MustCompile(issueTasksRegexpStr)
  58. issueTasksDonePat = regexp.MustCompile(issueTasksDoneRegexpStr)
  59. }
  60. func (issue *Issue) loadRepo(e Engine) (err error) {
  61. if issue.Repo == nil {
  62. issue.Repo, err = getRepositoryByID(e, issue.RepoID)
  63. if err != nil {
  64. return fmt.Errorf("getRepositoryByID [%d]: %v", issue.RepoID, err)
  65. }
  66. }
  67. return nil
  68. }
  69. // GetPullRequest returns the issue pull request
  70. func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
  71. if !issue.IsPull {
  72. return nil, fmt.Errorf("Issue is not a pull request")
  73. }
  74. pr, err = getPullRequestByIssueID(x, issue.ID)
  75. return
  76. }
  77. func (issue *Issue) loadLabels(e Engine) (err error) {
  78. if issue.Labels == nil {
  79. issue.Labels, err = getLabelsByIssueID(e, issue.ID)
  80. if err != nil {
  81. return fmt.Errorf("getLabelsByIssueID [%d]: %v", issue.ID, err)
  82. }
  83. }
  84. return nil
  85. }
  86. func (issue *Issue) loadPoster(e Engine) (err error) {
  87. if issue.Poster == nil {
  88. issue.Poster, err = getUserByID(e, issue.PosterID)
  89. if err != nil {
  90. issue.PosterID = -1
  91. issue.Poster = NewGhostUser()
  92. if !IsErrUserNotExist(err) {
  93. return fmt.Errorf("getUserByID.(poster) [%d]: %v", issue.PosterID, err)
  94. }
  95. err = nil
  96. return
  97. }
  98. }
  99. return
  100. }
  101. func (issue *Issue) loadAssignee(e Engine) (err error) {
  102. if issue.Assignee == nil && issue.AssigneeID > 0 {
  103. issue.Assignee, err = getUserByID(e, issue.AssigneeID)
  104. if err != nil {
  105. issue.AssigneeID = -1
  106. issue.Assignee = NewGhostUser()
  107. if !IsErrUserNotExist(err) {
  108. return fmt.Errorf("getUserByID.(assignee) [%d]: %v", issue.AssigneeID, err)
  109. }
  110. err = nil
  111. return
  112. }
  113. }
  114. return
  115. }
  116. func (issue *Issue) loadPullRequest(e Engine) (err error) {
  117. if issue.IsPull && issue.PullRequest == nil {
  118. issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID)
  119. if err != nil {
  120. if IsErrPullRequestNotExist(err) {
  121. return err
  122. }
  123. return fmt.Errorf("getPullRequestByIssueID [%d]: %v", issue.ID, err)
  124. }
  125. }
  126. return nil
  127. }
  128. func (issue *Issue) loadComments(e Engine) (err error) {
  129. if issue.Comments != nil {
  130. return nil
  131. }
  132. issue.Comments, err = findComments(e, FindCommentsOptions{
  133. IssueID: issue.ID,
  134. Type: CommentTypeUnknown,
  135. })
  136. return err
  137. }
  138. func (issue *Issue) loadReactions(e Engine) (err error) {
  139. if issue.Reactions != nil {
  140. return nil
  141. }
  142. reactions, err := findReactions(e, FindReactionsOptions{
  143. IssueID: issue.ID,
  144. })
  145. if err != nil {
  146. return err
  147. }
  148. // Load reaction user data
  149. if _, err := ReactionList(reactions).LoadUsers(); err != nil {
  150. return err
  151. }
  152. // Cache comments to map
  153. comments := make(map[int64]*Comment)
  154. for _, comment := range issue.Comments {
  155. comments[comment.ID] = comment
  156. }
  157. // Add reactions either to issue or comment
  158. for _, react := range reactions {
  159. if react.CommentID == 0 {
  160. issue.Reactions = append(issue.Reactions, react)
  161. } else if comment, ok := comments[react.CommentID]; ok {
  162. comment.Reactions = append(comment.Reactions, react)
  163. }
  164. }
  165. return nil
  166. }
  167. func (issue *Issue) loadAttributes(e Engine) (err error) {
  168. if err = issue.loadRepo(e); err != nil {
  169. return
  170. }
  171. if err = issue.loadPoster(e); err != nil {
  172. return
  173. }
  174. if err = issue.loadLabels(e); err != nil {
  175. return
  176. }
  177. if issue.Milestone == nil && issue.MilestoneID > 0 {
  178. issue.Milestone, err = getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
  179. if err != nil && !IsErrMilestoneNotExist(err) {
  180. return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v", issue.RepoID, issue.MilestoneID, err)
  181. }
  182. }
  183. if err = issue.loadAssignee(e); err != nil {
  184. return
  185. }
  186. if err = issue.loadPullRequest(e); err != nil && !IsErrPullRequestNotExist(err) {
  187. // It is possible pull request is not yet created.
  188. return err
  189. }
  190. if issue.Attachments == nil {
  191. issue.Attachments, err = getAttachmentsByIssueID(e, issue.ID)
  192. if err != nil {
  193. return fmt.Errorf("getAttachmentsByIssueID [%d]: %v", issue.ID, err)
  194. }
  195. }
  196. if err = issue.loadComments(e); err != nil {
  197. return err
  198. }
  199. return issue.loadReactions(e)
  200. }
  201. // LoadAttributes loads the attribute of this issue.
  202. func (issue *Issue) LoadAttributes() error {
  203. return issue.loadAttributes(x)
  204. }
  205. // GetIsRead load the `IsRead` field of the issue
  206. func (issue *Issue) GetIsRead(userID int64) error {
  207. issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
  208. if has, err := x.Get(issueUser); err != nil {
  209. return err
  210. } else if !has {
  211. issue.IsRead = false
  212. return nil
  213. }
  214. issue.IsRead = issueUser.IsRead
  215. return nil
  216. }
  217. // APIURL returns the absolute APIURL to this issue.
  218. func (issue *Issue) APIURL() string {
  219. return issue.Repo.APIURL() + "/" + path.Join("issues", fmt.Sprint(issue.Index))
  220. }
  221. // HTMLURL returns the absolute URL to this issue.
  222. func (issue *Issue) HTMLURL() string {
  223. var path string
  224. if issue.IsPull {
  225. path = "pulls"
  226. } else {
  227. path = "issues"
  228. }
  229. return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index)
  230. }
  231. // DiffURL returns the absolute URL to this diff
  232. func (issue *Issue) DiffURL() string {
  233. if issue.IsPull {
  234. return fmt.Sprintf("%s/pulls/%d.diff", issue.Repo.HTMLURL(), issue.Index)
  235. }
  236. return ""
  237. }
  238. // PatchURL returns the absolute URL to this patch
  239. func (issue *Issue) PatchURL() string {
  240. if issue.IsPull {
  241. return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index)
  242. }
  243. return ""
  244. }
  245. // State returns string representation of issue status.
  246. func (issue *Issue) State() api.StateType {
  247. if issue.IsClosed {
  248. return api.StateClosed
  249. }
  250. return api.StateOpen
  251. }
  252. // APIFormat assumes some fields assigned with values:
  253. // Required - Poster, Labels,
  254. // Optional - Milestone, Assignee, PullRequest
  255. func (issue *Issue) APIFormat() *api.Issue {
  256. apiLabels := make([]*api.Label, len(issue.Labels))
  257. for i := range issue.Labels {
  258. apiLabels[i] = issue.Labels[i].APIFormat()
  259. }
  260. apiIssue := &api.Issue{
  261. ID: issue.ID,
  262. URL: issue.APIURL(),
  263. Index: issue.Index,
  264. Poster: issue.Poster.APIFormat(),
  265. Title: issue.Title,
  266. Body: issue.Content,
  267. Labels: apiLabels,
  268. State: issue.State(),
  269. Comments: issue.NumComments,
  270. Created: issue.CreatedUnix.AsTime(),
  271. Updated: issue.UpdatedUnix.AsTime(),
  272. }
  273. if issue.Milestone != nil {
  274. apiIssue.Milestone = issue.Milestone.APIFormat()
  275. }
  276. if issue.Assignee != nil {
  277. apiIssue.Assignee = issue.Assignee.APIFormat()
  278. }
  279. if issue.IsPull {
  280. apiIssue.PullRequest = &api.PullRequestMeta{
  281. HasMerged: issue.PullRequest.HasMerged,
  282. }
  283. if issue.PullRequest.HasMerged {
  284. apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
  285. }
  286. }
  287. return apiIssue
  288. }
  289. // HashTag returns unique hash tag for issue.
  290. func (issue *Issue) HashTag() string {
  291. return "issue-" + com.ToStr(issue.ID)
  292. }
  293. // IsPoster returns true if given user by ID is the poster.
  294. func (issue *Issue) IsPoster(uid int64) bool {
  295. return issue.PosterID == uid
  296. }
  297. func (issue *Issue) hasLabel(e Engine, labelID int64) bool {
  298. return hasIssueLabel(e, issue.ID, labelID)
  299. }
  300. // HasLabel returns true if issue has been labeled by given ID.
  301. func (issue *Issue) HasLabel(labelID int64) bool {
  302. return issue.hasLabel(x, labelID)
  303. }
  304. func (issue *Issue) sendLabelUpdatedWebhook(doer *User) {
  305. var err error
  306. if issue.IsPull {
  307. if err = issue.loadRepo(x); err != nil {
  308. log.Error(4, "loadRepo: %v", err)
  309. return
  310. }
  311. if err = issue.loadPullRequest(x); err != nil {
  312. log.Error(4, "loadPullRequest: %v", err)
  313. return
  314. }
  315. if err = issue.PullRequest.LoadIssue(); err != nil {
  316. log.Error(4, "LoadIssue: %v", err)
  317. return
  318. }
  319. err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
  320. Action: api.HookIssueLabelUpdated,
  321. Index: issue.Index,
  322. PullRequest: issue.PullRequest.APIFormat(),
  323. Repository: issue.Repo.APIFormat(AccessModeNone),
  324. Sender: doer.APIFormat(),
  325. })
  326. }
  327. if err != nil {
  328. log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
  329. } else {
  330. go HookQueue.Add(issue.RepoID)
  331. }
  332. }
  333. func (issue *Issue) addLabel(e *xorm.Session, label *Label, doer *User) error {
  334. return newIssueLabel(e, issue, label, doer)
  335. }
  336. // AddLabel adds a new label to the issue.
  337. func (issue *Issue) AddLabel(doer *User, label *Label) error {
  338. if err := NewIssueLabel(issue, label, doer); err != nil {
  339. return err
  340. }
  341. issue.sendLabelUpdatedWebhook(doer)
  342. return nil
  343. }
  344. func (issue *Issue) addLabels(e *xorm.Session, labels []*Label, doer *User) error {
  345. return newIssueLabels(e, issue, labels, doer)
  346. }
  347. // AddLabels adds a list of new labels to the issue.
  348. func (issue *Issue) AddLabels(doer *User, labels []*Label) error {
  349. if err := NewIssueLabels(issue, labels, doer); err != nil {
  350. return err
  351. }
  352. issue.sendLabelUpdatedWebhook(doer)
  353. return nil
  354. }
  355. func (issue *Issue) getLabels(e Engine) (err error) {
  356. if len(issue.Labels) > 0 {
  357. return nil
  358. }
  359. issue.Labels, err = getLabelsByIssueID(e, issue.ID)
  360. if err != nil {
  361. return fmt.Errorf("getLabelsByIssueID: %v", err)
  362. }
  363. return nil
  364. }
  365. func (issue *Issue) removeLabel(e *xorm.Session, doer *User, label *Label) error {
  366. return deleteIssueLabel(e, issue, label, doer)
  367. }
  368. // RemoveLabel removes a label from issue by given ID.
  369. func (issue *Issue) RemoveLabel(doer *User, label *Label) error {
  370. if err := issue.loadRepo(x); err != nil {
  371. return err
  372. }
  373. if has, err := HasAccess(doer.ID, issue.Repo, AccessModeWrite); err != nil {
  374. return err
  375. } else if !has {
  376. return ErrLabelNotExist{}
  377. }
  378. if err := DeleteIssueLabel(issue, label, doer); err != nil {
  379. return err
  380. }
  381. issue.sendLabelUpdatedWebhook(doer)
  382. return nil
  383. }
  384. func (issue *Issue) clearLabels(e *xorm.Session, doer *User) (err error) {
  385. if err = issue.getLabels(e); err != nil {
  386. return fmt.Errorf("getLabels: %v", err)
  387. }
  388. for i := range issue.Labels {
  389. if err = issue.removeLabel(e, doer, issue.Labels[i]); err != nil {
  390. return fmt.Errorf("removeLabel: %v", err)
  391. }
  392. }
  393. return nil
  394. }
  395. // ClearLabels removes all issue labels as the given user.
  396. // Triggers appropriate WebHooks, if any.
  397. func (issue *Issue) ClearLabels(doer *User) (err error) {
  398. sess := x.NewSession()
  399. defer sess.Close()
  400. if err = sess.Begin(); err != nil {
  401. return err
  402. }
  403. if err := issue.loadRepo(sess); err != nil {
  404. return err
  405. } else if err = issue.loadPullRequest(sess); err != nil {
  406. return err
  407. }
  408. if has, err := hasAccess(sess, doer.ID, issue.Repo, AccessModeWrite); err != nil {
  409. return err
  410. } else if !has {
  411. return ErrLabelNotExist{}
  412. }
  413. if err = issue.clearLabels(sess, doer); err != nil {
  414. return err
  415. }
  416. if err = sess.Commit(); err != nil {
  417. return fmt.Errorf("Commit: %v", err)
  418. }
  419. if issue.IsPull {
  420. err = issue.PullRequest.LoadIssue()
  421. if err != nil {
  422. log.Error(4, "LoadIssue: %v", err)
  423. return
  424. }
  425. err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
  426. Action: api.HookIssueLabelCleared,
  427. Index: issue.Index,
  428. PullRequest: issue.PullRequest.APIFormat(),
  429. Repository: issue.Repo.APIFormat(AccessModeNone),
  430. Sender: doer.APIFormat(),
  431. })
  432. }
  433. if err != nil {
  434. log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
  435. } else {
  436. go HookQueue.Add(issue.RepoID)
  437. }
  438. return nil
  439. }
  440. type labelSorter []*Label
  441. func (ts labelSorter) Len() int {
  442. return len([]*Label(ts))
  443. }
  444. func (ts labelSorter) Less(i, j int) bool {
  445. return []*Label(ts)[i].ID < []*Label(ts)[j].ID
  446. }
  447. func (ts labelSorter) Swap(i, j int) {
  448. []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i]
  449. }
  450. // ReplaceLabels removes all current labels and add new labels to the issue.
  451. // Triggers appropriate WebHooks, if any.
  452. func (issue *Issue) ReplaceLabels(labels []*Label, doer *User) (err error) {
  453. sess := x.NewSession()
  454. defer sess.Close()
  455. if err = sess.Begin(); err != nil {
  456. return err
  457. }
  458. if err = issue.loadLabels(sess); err != nil {
  459. return err
  460. }
  461. sort.Sort(labelSorter(labels))
  462. sort.Sort(labelSorter(issue.Labels))
  463. var toAdd, toRemove []*Label
  464. addIndex, removeIndex := 0, 0
  465. for addIndex < len(labels) && removeIndex < len(issue.Labels) {
  466. addLabel := labels[addIndex]
  467. removeLabel := issue.Labels[removeIndex]
  468. if addLabel.ID == removeLabel.ID {
  469. addIndex++
  470. removeIndex++
  471. } else if addLabel.ID < removeLabel.ID {
  472. toAdd = append(toAdd, addLabel)
  473. addIndex++
  474. } else {
  475. toRemove = append(toRemove, removeLabel)
  476. removeIndex++
  477. }
  478. }
  479. toAdd = append(toAdd, labels[addIndex:]...)
  480. toRemove = append(toRemove, issue.Labels[removeIndex:]...)
  481. if len(toAdd) > 0 {
  482. if err = issue.addLabels(sess, toAdd, doer); err != nil {
  483. return fmt.Errorf("addLabels: %v", err)
  484. }
  485. }
  486. for _, l := range toRemove {
  487. if err = issue.removeLabel(sess, doer, l); err != nil {
  488. return fmt.Errorf("removeLabel: %v", err)
  489. }
  490. }
  491. return sess.Commit()
  492. }
  493. // GetAssignee sets the Assignee attribute of this issue.
  494. func (issue *Issue) GetAssignee() (err error) {
  495. if issue.AssigneeID == 0 || issue.Assignee != nil {
  496. return nil
  497. }
  498. issue.Assignee, err = GetUserByID(issue.AssigneeID)
  499. if IsErrUserNotExist(err) {
  500. return nil
  501. }
  502. return err
  503. }
  504. // ReadBy sets issue to be read by given user.
  505. func (issue *Issue) ReadBy(userID int64) error {
  506. if err := UpdateIssueUserByRead(userID, issue.ID); err != nil {
  507. return err
  508. }
  509. return setNotificationStatusReadIfUnread(x, userID, issue.ID)
  510. }
  511. func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
  512. if _, err := e.ID(issue.ID).Cols(cols...).Update(issue); err != nil {
  513. return err
  514. }
  515. UpdateIssueIndexerCols(issue.ID, cols...)
  516. return nil
  517. }
  518. // UpdateIssueCols only updates values of specific columns for given issue.
  519. func UpdateIssueCols(issue *Issue, cols ...string) error {
  520. return updateIssueCols(x, issue, cols...)
  521. }
  522. func (issue *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository, isClosed bool) (err error) {
  523. // Nothing should be performed if current status is same as target status
  524. if issue.IsClosed == isClosed {
  525. return nil
  526. }
  527. issue.IsClosed = isClosed
  528. if err = updateIssueCols(e, issue, "is_closed"); err != nil {
  529. return err
  530. }
  531. // Update issue count of labels
  532. if err = issue.getLabels(e); err != nil {
  533. return err
  534. }
  535. for idx := range issue.Labels {
  536. if issue.IsClosed {
  537. issue.Labels[idx].NumClosedIssues++
  538. } else {
  539. issue.Labels[idx].NumClosedIssues--
  540. }
  541. if err = updateLabel(e, issue.Labels[idx]); err != nil {
  542. return err
  543. }
  544. }
  545. // Update issue count of milestone
  546. if err = changeMilestoneIssueStats(e, issue); err != nil {
  547. return err
  548. }
  549. // New action comment
  550. if _, err = createStatusComment(e, doer, repo, issue); err != nil {
  551. return err
  552. }
  553. return nil
  554. }
  555. // ChangeStatus changes issue status to open or closed.
  556. func (issue *Issue) ChangeStatus(doer *User, repo *Repository, isClosed bool) (err error) {
  557. sess := x.NewSession()
  558. defer sess.Close()
  559. if err = sess.Begin(); err != nil {
  560. return err
  561. }
  562. if err = issue.changeStatus(sess, doer, repo, isClosed); err != nil {
  563. return err
  564. }
  565. if err = sess.Commit(); err != nil {
  566. return fmt.Errorf("Commit: %v", err)
  567. }
  568. if issue.IsPull {
  569. // Merge pull request calls issue.changeStatus so we need to handle separately.
  570. issue.PullRequest.Issue = issue
  571. apiPullRequest := &api.PullRequestPayload{
  572. Index: issue.Index,
  573. PullRequest: issue.PullRequest.APIFormat(),
  574. Repository: repo.APIFormat(AccessModeNone),
  575. Sender: doer.APIFormat(),
  576. }
  577. if isClosed {
  578. apiPullRequest.Action = api.HookIssueClosed
  579. } else {
  580. apiPullRequest.Action = api.HookIssueReOpened
  581. }
  582. err = PrepareWebhooks(repo, HookEventPullRequest, apiPullRequest)
  583. }
  584. if err != nil {
  585. log.Error(4, "PrepareWebhooks [is_pull: %v, is_closed: %v]: %v", issue.IsPull, isClosed, err)
  586. } else {
  587. go HookQueue.Add(repo.ID)
  588. }
  589. return nil
  590. }
  591. // ChangeTitle changes the title of this issue, as the given user.
  592. func (issue *Issue) ChangeTitle(doer *User, title string) (err error) {
  593. oldTitle := issue.Title
  594. issue.Title = title
  595. sess := x.NewSession()
  596. defer sess.Close()
  597. if err = sess.Begin(); err != nil {
  598. return err
  599. }
  600. if err = updateIssueCols(sess, issue, "name"); err != nil {
  601. return fmt.Errorf("updateIssueCols: %v", err)
  602. }
  603. if _, err = createChangeTitleComment(sess, doer, issue.Repo, issue, oldTitle, title); err != nil {
  604. return fmt.Errorf("createChangeTitleComment: %v", err)
  605. }
  606. if err = sess.Commit(); err != nil {
  607. return err
  608. }
  609. if issue.IsPull {
  610. issue.PullRequest.Issue = issue
  611. err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
  612. Action: api.HookIssueEdited,
  613. Index: issue.Index,
  614. Changes: &api.ChangesPayload{
  615. Title: &api.ChangesFromPayload{
  616. From: oldTitle,
  617. },
  618. },
  619. PullRequest: issue.PullRequest.APIFormat(),
  620. Repository: issue.Repo.APIFormat(AccessModeNone),
  621. Sender: doer.APIFormat(),
  622. })
  623. }
  624. if err != nil {
  625. log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
  626. } else {
  627. go HookQueue.Add(issue.RepoID)
  628. }
  629. return nil
  630. }
  631. // AddDeletePRBranchComment adds delete branch comment for pull request issue
  632. func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branchName string) error {
  633. issue, err := getIssueByID(x, issueID)
  634. if err != nil {
  635. return err
  636. }
  637. sess := x.NewSession()
  638. defer sess.Close()
  639. if err := sess.Begin(); err != nil {
  640. return err
  641. }
  642. if _, err := createDeleteBranchComment(sess, doer, repo, issue, branchName); err != nil {
  643. return err
  644. }
  645. return sess.Commit()
  646. }
  647. // ChangeContent changes issue content, as the given user.
  648. func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
  649. oldContent := issue.Content
  650. issue.Content = content
  651. if err = UpdateIssueCols(issue, "content"); err != nil {
  652. return fmt.Errorf("UpdateIssueCols: %v", err)
  653. }
  654. if issue.IsPull {
  655. issue.PullRequest.Issue = issue
  656. err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
  657. Action: api.HookIssueEdited,
  658. Index: issue.Index,
  659. Changes: &api.ChangesPayload{
  660. Body: &api.ChangesFromPayload{
  661. From: oldContent,
  662. },
  663. },
  664. PullRequest: issue.PullRequest.APIFormat(),
  665. Repository: issue.Repo.APIFormat(AccessModeNone),
  666. Sender: doer.APIFormat(),
  667. })
  668. }
  669. if err != nil {
  670. log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
  671. } else {
  672. go HookQueue.Add(issue.RepoID)
  673. }
  674. return nil
  675. }
  676. // ChangeAssignee changes the Assignee field of this issue.
  677. func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
  678. var oldAssigneeID = issue.AssigneeID
  679. issue.AssigneeID = assigneeID
  680. if err = UpdateIssueUserByAssignee(issue); err != nil {
  681. return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
  682. }
  683. sess := x.NewSession()
  684. defer sess.Close()
  685. if err = issue.loadRepo(sess); err != nil {
  686. return fmt.Errorf("loadRepo: %v", err)
  687. }
  688. if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, oldAssigneeID, assigneeID); err != nil {
  689. return fmt.Errorf("createAssigneeComment: %v", err)
  690. }
  691. issue.Assignee, err = GetUserByID(issue.AssigneeID)
  692. if err != nil && !IsErrUserNotExist(err) {
  693. log.Error(4, "GetUserByID [assignee_id: %v]: %v", issue.AssigneeID, err)
  694. return nil
  695. }
  696. // Error not nil here means user does not exist, which is remove assignee.
  697. isRemoveAssignee := err != nil
  698. if issue.IsPull {
  699. issue.PullRequest.Issue = issue
  700. apiPullRequest := &api.PullRequestPayload{
  701. Index: issue.Index,
  702. PullRequest: issue.PullRequest.APIFormat(),
  703. Repository: issue.Repo.APIFormat(AccessModeNone),
  704. Sender: doer.APIFormat(),
  705. }
  706. if isRemoveAssignee {
  707. apiPullRequest.Action = api.HookIssueUnassigned
  708. } else {
  709. apiPullRequest.Action = api.HookIssueAssigned
  710. }
  711. if err := PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest); err != nil {
  712. log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, isRemoveAssignee, err)
  713. return nil
  714. }
  715. }
  716. go HookQueue.Add(issue.RepoID)
  717. return nil
  718. }
  719. // GetTasks returns the amount of tasks in the issues content
  720. func (issue *Issue) GetTasks() int {
  721. return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
  722. }
  723. // GetTasksDone returns the amount of completed tasks in the issues content
  724. func (issue *Issue) GetTasksDone() int {
  725. return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
  726. }
  727. // NewIssueOptions represents the options of a new issue.
  728. type NewIssueOptions struct {
  729. Repo *Repository
  730. Issue *Issue
  731. LabelIDs []int64
  732. Attachments []string // In UUID format.
  733. IsPull bool
  734. }
  735. func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
  736. opts.Issue.Title = strings.TrimSpace(opts.Issue.Title)
  737. opts.Issue.Index = opts.Repo.NextIssueIndex()
  738. if opts.Issue.MilestoneID > 0 {
  739. milestone, err := getMilestoneByRepoID(e, opts.Issue.RepoID, opts.Issue.MilestoneID)
  740. if err != nil && !IsErrMilestoneNotExist(err) {
  741. return fmt.Errorf("getMilestoneByID: %v", err)
  742. }
  743. // Assume milestone is invalid and drop silently.
  744. opts.Issue.MilestoneID = 0
  745. if milestone != nil {
  746. opts.Issue.MilestoneID = milestone.ID
  747. opts.Issue.Milestone = milestone
  748. }
  749. }
  750. if assigneeID := opts.Issue.AssigneeID; assigneeID > 0 {
  751. valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite)
  752. if err != nil {
  753. return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
  754. }
  755. if !valid {
  756. opts.Issue.AssigneeID = 0
  757. opts.Issue.Assignee = nil
  758. }
  759. }
  760. // Milestone and assignee validation should happen before insert actual object.
  761. if _, err = e.Insert(opts.Issue); err != nil {
  762. return err
  763. }
  764. if opts.Issue.MilestoneID > 0 {
  765. if err = changeMilestoneAssign(e, doer, opts.Issue, -1); err != nil {
  766. return err
  767. }
  768. }
  769. if opts.Issue.AssigneeID > 0 {
  770. if err = opts.Issue.loadRepo(e); err != nil {
  771. return err
  772. }
  773. if _, err = createAssigneeComment(e, doer, opts.Issue.Repo, opts.Issue, -1, opts.Issue.AssigneeID); err != nil {
  774. return err
  775. }
  776. }
  777. if opts.IsPull {
  778. _, err = e.Exec("UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?", opts.Issue.RepoID)
  779. } else {
  780. _, err = e.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", opts.Issue.RepoID)
  781. }
  782. if err != nil {
  783. return err
  784. }
  785. if len(opts.LabelIDs) > 0 {
  786. // During the session, SQLite3 driver cannot handle retrieve objects after update something.
  787. // So we have to get all needed labels first.
  788. labels := make([]*Label, 0, len(opts.LabelIDs))
  789. if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil {
  790. return fmt.Errorf("find all labels [label_ids: %v]: %v", opts.LabelIDs, err)
  791. }
  792. if err = opts.Issue.loadPoster(e); err != nil {
  793. return err
  794. }
  795. for _, label := range labels {
  796. // Silently drop invalid labels.
  797. if label.RepoID != opts.Repo.ID {
  798. continue
  799. }
  800. if err = opts.Issue.addLabel(e, label, opts.Issue.Poster); err != nil {
  801. return fmt.Errorf("addLabel [id: %d]: %v", label.ID, err)
  802. }
  803. }
  804. }
  805. if err = newIssueUsers(e, opts.Repo, opts.Issue); err != nil {
  806. return err
  807. }
  808. if len(opts.Attachments) > 0 {
  809. attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
  810. if err != nil {
  811. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err)
  812. }
  813. for i := 0; i < len(attachments); i++ {
  814. attachments[i].IssueID = opts.Issue.ID
  815. if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
  816. return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
  817. }
  818. }
  819. }
  820. return opts.Issue.loadAttributes(e)
  821. }
  822. // NewIssue creates new issue with labels for repository.
  823. func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
  824. sess := x.NewSession()
  825. defer sess.Close()
  826. if err = sess.Begin(); err != nil {
  827. return err
  828. }
  829. if err = newIssue(sess, issue.Poster, NewIssueOptions{
  830. Repo: repo,
  831. Issue: issue,
  832. LabelIDs: labelIDs,
  833. Attachments: uuids,
  834. }); err != nil {
  835. return fmt.Errorf("newIssue: %v", err)
  836. }
  837. if err = sess.Commit(); err != nil {
  838. return fmt.Errorf("Commit: %v", err)
  839. }
  840. UpdateIssueIndexer(issue.ID)
  841. if err = NotifyWatchers(&Action{
  842. ActUserID: issue.Poster.ID,
  843. ActUser: issue.Poster,
  844. OpType: ActionCreateIssue,
  845. Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
  846. RepoID: repo.ID,
  847. Repo: repo,
  848. IsPrivate: repo.IsPrivate,
  849. }); err != nil {
  850. log.Error(4, "NotifyWatchers: %v", err)
  851. }
  852. if err = issue.MailParticipants(); err != nil {
  853. log.Error(4, "MailParticipants: %v", err)
  854. }
  855. return nil
  856. }
  857. // GetRawIssueByIndex returns raw issue without loading attributes by index in a repository.
  858. func GetRawIssueByIndex(repoID, index int64) (*Issue, error) {
  859. issue := &Issue{
  860. RepoID: repoID,
  861. Index: index,
  862. }
  863. has, err := x.Get(issue)
  864. if err != nil {
  865. return nil, err
  866. } else if !has {
  867. return nil, ErrIssueNotExist{0, repoID, index}
  868. }
  869. return issue, nil
  870. }
  871. // GetIssueByIndex returns issue by index in a repository.
  872. func GetIssueByIndex(repoID, index int64) (*Issue, error) {
  873. issue, err := GetRawIssueByIndex(repoID, index)
  874. if err != nil {
  875. return nil, err
  876. }
  877. return issue, issue.LoadAttributes()
  878. }
  879. func getIssueByID(e Engine, id int64) (*Issue, error) {
  880. issue := new(Issue)
  881. has, err := e.ID(id).Get(issue)
  882. if err != nil {
  883. return nil, err
  884. } else if !has {
  885. return nil, ErrIssueNotExist{id, 0, 0}
  886. }
  887. return issue, issue.loadAttributes(e)
  888. }
  889. // GetIssueByID returns an issue by given ID.
  890. func GetIssueByID(id int64) (*Issue, error) {
  891. return getIssueByID(x, id)
  892. }
  893. func getIssuesByIDs(e Engine, issueIDs []int64) ([]*Issue, error) {
  894. issues := make([]*Issue, 0, 10)
  895. return issues, e.In("id", issueIDs).Find(&issues)
  896. }
  897. // GetIssuesByIDs return issues with the given IDs.
  898. func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) {
  899. return getIssuesByIDs(x, issueIDs)
  900. }
  901. // IssuesOptions represents options of an issue.
  902. type IssuesOptions struct {
  903. RepoIDs []int64 // include all repos if empty
  904. AssigneeID int64
  905. PosterID int64
  906. MentionedID int64
  907. MilestoneID int64
  908. Page int
  909. PageSize int
  910. IsClosed util.OptionalBool
  911. IsPull util.OptionalBool
  912. Labels string
  913. SortType string
  914. IssueIDs []int64
  915. }
  916. // sortIssuesSession sort an issues-related session based on the provided
  917. // sortType string
  918. func sortIssuesSession(sess *xorm.Session, sortType string) {
  919. switch sortType {
  920. case "oldest":
  921. sess.Asc("issue.created_unix")
  922. case "recentupdate":
  923. sess.Desc("issue.updated_unix")
  924. case "leastupdate":
  925. sess.Asc("issue.updated_unix")
  926. case "mostcomment":
  927. sess.Desc("issue.num_comments")
  928. case "leastcomment":
  929. sess.Asc("issue.num_comments")
  930. case "priority":
  931. sess.Desc("issue.priority")
  932. default:
  933. sess.Desc("issue.created_unix")
  934. }
  935. }
  936. func (opts *IssuesOptions) setupSession(sess *xorm.Session) error {
  937. if opts.Page >= 0 && opts.PageSize > 0 {
  938. var start int
  939. if opts.Page == 0 {
  940. start = 0
  941. } else {
  942. start = (opts.Page - 1) * opts.PageSize
  943. }
  944. sess.Limit(opts.PageSize, start)
  945. }
  946. if len(opts.IssueIDs) > 0 {
  947. sess.In("issue.id", opts.IssueIDs)
  948. }
  949. if len(opts.RepoIDs) > 0 {
  950. // In case repository IDs are provided but actually no repository has issue.
  951. sess.In("issue.repo_id", opts.RepoIDs)
  952. }
  953. switch opts.IsClosed {
  954. case util.OptionalBoolTrue:
  955. sess.And("issue.is_closed=?", true)
  956. case util.OptionalBoolFalse:
  957. sess.And("issue.is_closed=?", false)
  958. }
  959. if opts.AssigneeID > 0 {
  960. sess.And("issue.assignee_id=?", opts.AssigneeID)
  961. }
  962. if opts.PosterID > 0 {
  963. sess.And("issue.poster_id=?", opts.PosterID)
  964. }
  965. if opts.MentionedID > 0 {
  966. sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
  967. And("issue_user.is_mentioned = ?", true).
  968. And("issue_user.uid = ?", opts.MentionedID)
  969. }
  970. if opts.MilestoneID > 0 {
  971. sess.And("issue.milestone_id=?", opts.MilestoneID)
  972. }
  973. switch opts.IsPull {
  974. case util.OptionalBoolTrue:
  975. sess.And("issue.is_pull=?", true)
  976. case util.OptionalBoolFalse:
  977. sess.And("issue.is_pull=?", false)
  978. }
  979. if len(opts.Labels) > 0 && opts.Labels != "0" {
  980. labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ","))
  981. if err != nil {
  982. return err
  983. }
  984. if len(labelIDs) > 0 {
  985. sess.
  986. Join("INNER", "issue_label", "issue.id = issue_label.issue_id").
  987. In("issue_label.label_id", labelIDs)
  988. }
  989. }
  990. return nil
  991. }
  992. // CountIssuesByRepo map from repoID to number of issues matching the options
  993. func CountIssuesByRepo(opts *IssuesOptions) (map[int64]int64, error) {
  994. sess := x.NewSession()
  995. defer sess.Close()
  996. if err := opts.setupSession(sess); err != nil {
  997. return nil, err
  998. }
  999. countsSlice := make([]*struct {
  1000. RepoID int64
  1001. Count int64
  1002. }, 0, 10)
  1003. if err := sess.GroupBy("issue.repo_id").
  1004. Select("issue.repo_id AS repo_id, COUNT(*) AS count").
  1005. Table("issue").
  1006. Find(&countsSlice); err != nil {
  1007. return nil, err
  1008. }
  1009. countMap := make(map[int64]int64, len(countsSlice))
  1010. for _, c := range countsSlice {
  1011. countMap[c.RepoID] = c.Count
  1012. }
  1013. return countMap, nil
  1014. }
  1015. // Issues returns a list of issues by given conditions.
  1016. func Issues(opts *IssuesOptions) ([]*Issue, error) {
  1017. sess := x.NewSession()
  1018. defer sess.Close()
  1019. if err := opts.setupSession(sess); err != nil {
  1020. return nil, err
  1021. }
  1022. sortIssuesSession(sess, opts.SortType)
  1023. issues := make([]*Issue, 0, setting.UI.IssuePagingNum)
  1024. if err := sess.Find(&issues); err != nil {
  1025. return nil, fmt.Errorf("Find: %v", err)
  1026. }
  1027. if err := IssueList(issues).LoadAttributes(); err != nil {
  1028. return nil, fmt.Errorf("LoadAttributes: %v", err)
  1029. }
  1030. return issues, nil
  1031. }
  1032. // GetParticipantsByIssueID returns all users who are participated in comments of an issue.
  1033. func GetParticipantsByIssueID(issueID int64) ([]*User, error) {
  1034. return getParticipantsByIssueID(x, issueID)
  1035. }
  1036. func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) {
  1037. userIDs := make([]int64, 0, 5)
  1038. if err := e.Table("comment").Cols("poster_id").
  1039. Where("`comment`.issue_id = ?", issueID).
  1040. And("`comment`.type = ?", CommentTypeComment).
  1041. And("`user`.is_active = ?", true).
  1042. And("`user`.prohibit_login = ?", false).
  1043. Join("INNER", "user", "`user`.id = `comment`.poster_id").
  1044. Distinct("poster_id").
  1045. Find(&userIDs); err != nil {
  1046. return nil, fmt.Errorf("get poster IDs: %v", err)
  1047. }
  1048. if len(userIDs) == 0 {
  1049. return nil, nil
  1050. }
  1051. users := make([]*User, 0, len(userIDs))
  1052. return users, e.In("id", userIDs).Find(&users)
  1053. }
  1054. // UpdateIssueMentions extracts mentioned people from content and
  1055. // updates issue-user relations for them.
  1056. func UpdateIssueMentions(e Engine, issueID int64, mentions []string) error {
  1057. if len(mentions) == 0 {
  1058. return nil
  1059. }
  1060. for i := range mentions {
  1061. mentions[i] = strings.ToLower(mentions[i])
  1062. }
  1063. users := make([]*User, 0, len(mentions))
  1064. if err := e.In("lower_name", mentions).Asc("lower_name").Find(&users); err != nil {
  1065. return fmt.Errorf("find mentioned users: %v", err)
  1066. }
  1067. ids := make([]int64, 0, len(mentions))
  1068. for _, user := range users {
  1069. ids = append(ids, user.ID)
  1070. if !user.IsOrganization() || user.NumMembers == 0 {
  1071. continue
  1072. }
  1073. memberIDs := make([]int64, 0, user.NumMembers)
  1074. orgUsers, err := GetOrgUsersByOrgID(user.ID)
  1075. if err != nil {
  1076. return fmt.Errorf("GetOrgUsersByOrgID [%d]: %v", user.ID, err)
  1077. }
  1078. for _, orgUser := range orgUsers {
  1079. memberIDs = append(memberIDs, orgUser.ID)
  1080. }
  1081. ids = append(ids, memberIDs...)
  1082. }
  1083. if err := UpdateIssueUsersByMentions(e, issueID, ids); err != nil {
  1084. return fmt.Errorf("UpdateIssueUsersByMentions: %v", err)
  1085. }
  1086. return nil
  1087. }
  1088. // IssueStats represents issue statistic information.
  1089. type IssueStats struct {
  1090. OpenCount, ClosedCount int64
  1091. YourRepositoriesCount int64
  1092. AssignCount int64
  1093. CreateCount int64
  1094. MentionCount int64
  1095. }
  1096. // Filter modes.
  1097. const (
  1098. FilterModeAll = iota
  1099. FilterModeAssign
  1100. FilterModeCreate
  1101. FilterModeMention
  1102. )
  1103. func parseCountResult(results []map[string][]byte) int64 {
  1104. if len(results) == 0 {
  1105. return 0
  1106. }
  1107. for _, result := range results[0] {
  1108. return com.StrTo(string(result)).MustInt64()
  1109. }
  1110. return 0
  1111. }
  1112. // IssueStatsOptions contains parameters accepted by GetIssueStats.
  1113. type IssueStatsOptions struct {
  1114. RepoID int64
  1115. Labels string
  1116. MilestoneID int64
  1117. AssigneeID int64
  1118. MentionedID int64
  1119. PosterID int64
  1120. IsPull bool
  1121. IssueIDs []int64
  1122. }
  1123. // GetIssueStats returns issue statistic information by given conditions.
  1124. func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
  1125. stats := &IssueStats{}
  1126. countSession := func(opts *IssueStatsOptions) *xorm.Session {
  1127. sess := x.
  1128. Where("issue.repo_id = ?", opts.RepoID).
  1129. And("issue.is_pull = ?", opts.IsPull)
  1130. if len(opts.IssueIDs) > 0 {
  1131. sess.In("issue.id", opts.IssueIDs)
  1132. }
  1133. if len(opts.Labels) > 0 && opts.Labels != "0" {
  1134. labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ","))
  1135. if err != nil {
  1136. log.Warn("Malformed Labels argument: %s", opts.Labels)
  1137. } else if len(labelIDs) > 0 {
  1138. sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id").
  1139. In("issue_label.label_id", labelIDs)
  1140. }
  1141. }
  1142. if opts.MilestoneID > 0 {
  1143. sess.And("issue.milestone_id = ?", opts.MilestoneID)
  1144. }
  1145. if opts.AssigneeID > 0 {
  1146. sess.And("issue.assignee_id = ?", opts.AssigneeID)
  1147. }
  1148. if opts.PosterID > 0 {
  1149. sess.And("issue.poster_id = ?", opts.PosterID)
  1150. }
  1151. if opts.MentionedID > 0 {
  1152. sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
  1153. And("issue_user.uid = ?", opts.MentionedID).
  1154. And("issue_user.is_mentioned = ?", true)
  1155. }
  1156. return sess
  1157. }
  1158. var err error
  1159. stats.OpenCount, err = countSession(opts).
  1160. And("issue.is_closed = ?", false).
  1161. Count(new(Issue))
  1162. if err != nil {
  1163. return stats, err
  1164. }
  1165. stats.ClosedCount, err = countSession(opts).
  1166. And("issue.is_closed = ?", true).
  1167. Count(new(Issue))
  1168. return stats, err
  1169. }
  1170. // UserIssueStatsOptions contains parameters accepted by GetUserIssueStats.
  1171. type UserIssueStatsOptions struct {
  1172. UserID int64
  1173. RepoID int64
  1174. UserRepoIDs []int64
  1175. FilterMode int
  1176. IsPull bool
  1177. IsClosed bool
  1178. }
  1179. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
  1180. func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
  1181. var err error
  1182. stats := &IssueStats{}
  1183. cond := builder.NewCond()
  1184. cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull})
  1185. if opts.RepoID > 0 {
  1186. cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
  1187. }
  1188. switch opts.FilterMode {
  1189. case FilterModeAll:
  1190. stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
  1191. And(builder.In("issue.repo_id", opts.UserRepoIDs)).
  1192. Count(new(Issue))
  1193. if err != nil {
  1194. return nil, err
  1195. }
  1196. stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
  1197. And(builder.In("issue.repo_id", opts.UserRepoIDs)).
  1198. Count(new(Issue))
  1199. if err != nil {
  1200. return nil, err
  1201. }
  1202. case FilterModeAssign:
  1203. stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
  1204. And("assignee_id = ?", opts.UserID).
  1205. Count(new(Issue))
  1206. if err != nil {
  1207. return nil, err
  1208. }
  1209. stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
  1210. And("assignee_id = ?", opts.UserID).
  1211. Count(new(Issue))
  1212. if err != nil {
  1213. return nil, err
  1214. }
  1215. case FilterModeCreate:
  1216. stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
  1217. And("poster_id = ?", opts.UserID).
  1218. Count(new(Issue))
  1219. if err != nil {
  1220. return nil, err
  1221. }
  1222. stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
  1223. And("poster_id = ?", opts.UserID).
  1224. Count(new(Issue))
  1225. if err != nil {
  1226. return nil, err
  1227. }
  1228. }
  1229. cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed})
  1230. stats.AssignCount, err = x.Where(cond).
  1231. And("assignee_id = ?", opts.UserID).
  1232. Count(new(Issue))
  1233. if err != nil {
  1234. return nil, err
  1235. }
  1236. stats.CreateCount, err = x.Where(cond).
  1237. And("poster_id = ?", opts.UserID).
  1238. Count(new(Issue))
  1239. if err != nil {
  1240. return nil, err
  1241. }
  1242. stats.YourRepositoriesCount, err = x.Where(cond).
  1243. And(builder.In("issue.repo_id", opts.UserRepoIDs)).
  1244. Count(new(Issue))
  1245. if err != nil {
  1246. return nil, err
  1247. }
  1248. return stats, nil
  1249. }
  1250. // GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
  1251. func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen int64, numClosed int64) {
  1252. countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session {
  1253. sess := x.
  1254. Where("is_closed = ?", isClosed).
  1255. And("is_pull = ?", isPull).
  1256. And("repo_id = ?", repoID)
  1257. return sess
  1258. }
  1259. openCountSession := countSession(false, isPull, repoID)
  1260. closedCountSession := countSession(true, isPull, repoID)
  1261. switch filterMode {
  1262. case FilterModeAssign:
  1263. openCountSession.And("assignee_id = ?", uid)
  1264. closedCountSession.And("assignee_id = ?", uid)
  1265. case FilterModeCreate:
  1266. openCountSession.And("poster_id = ?", uid)
  1267. closedCountSession.And("poster_id = ?", uid)
  1268. }
  1269. openResult, _ := openCountSession.Count(new(Issue))
  1270. closedResult, _ := closedCountSession.Count(new(Issue))
  1271. return openResult, closedResult
  1272. }
  1273. func updateIssue(e Engine, issue *Issue) error {
  1274. _, err := e.ID(issue.ID).AllCols().Update(issue)
  1275. if err != nil {
  1276. return err
  1277. }
  1278. UpdateIssueIndexer(issue.ID)
  1279. return nil
  1280. }
  1281. // UpdateIssue updates all fields of given issue.
  1282. func UpdateIssue(issue *Issue) error {
  1283. return updateIssue(x, issue)
  1284. }