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 38 kB

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