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

issue.go 40KB

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