issue.go 40 KB

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