You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue.go 39 kB

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