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 45KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737
  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. "errors"
  7. "fmt"
  8. "strings"
  9. "time"
  10. api "code.gitea.io/sdk/gitea"
  11. "github.com/Unknwon/com"
  12. "github.com/go-xorm/xorm"
  13. "code.gitea.io/gitea/modules/base"
  14. "code.gitea.io/gitea/modules/log"
  15. "code.gitea.io/gitea/modules/setting"
  16. )
  17. var (
  18. errMissingIssueNumber = errors.New("No issue number specified")
  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. Deadline time.Time `xorm:"-"`
  43. DeadlineUnix int64 `xorm:"INDEX"`
  44. Created time.Time `xorm:"-"`
  45. CreatedUnix int64 `xorm:"INDEX"`
  46. Updated time.Time `xorm:"-"`
  47. UpdatedUnix int64 `xorm:"INDEX"`
  48. Attachments []*Attachment `xorm:"-"`
  49. Comments []*Comment `xorm:"-"`
  50. }
  51. // BeforeInsert is invoked from XORM before inserting an object of this type.
  52. func (issue *Issue) BeforeInsert() {
  53. issue.CreatedUnix = time.Now().Unix()
  54. issue.UpdatedUnix = issue.CreatedUnix
  55. }
  56. // BeforeUpdate is invoked from XORM before updating this object.
  57. func (issue *Issue) BeforeUpdate() {
  58. issue.UpdatedUnix = time.Now().Unix()
  59. issue.DeadlineUnix = issue.Deadline.Unix()
  60. }
  61. // AfterSet is invoked from XORM after setting the value of a field of
  62. // this object.
  63. func (issue *Issue) AfterSet(colName string, _ xorm.Cell) {
  64. switch colName {
  65. case "deadline_unix":
  66. issue.Deadline = time.Unix(issue.DeadlineUnix, 0).Local()
  67. case "created_unix":
  68. issue.Created = time.Unix(issue.CreatedUnix, 0).Local()
  69. case "updated_unix":
  70. issue.Updated = time.Unix(issue.UpdatedUnix, 0).Local()
  71. }
  72. }
  73. func (issue *Issue) loadRepo(e Engine) (err error) {
  74. if issue.Repo == nil {
  75. issue.Repo, err = getRepositoryByID(e, issue.RepoID)
  76. if err != nil {
  77. return fmt.Errorf("getRepositoryByID [%d]: %v", issue.RepoID, err)
  78. }
  79. }
  80. return nil
  81. }
  82. func (issue *Issue) loadAttributes(e Engine) (err error) {
  83. if err := issue.loadRepo(e); err != nil {
  84. return err
  85. }
  86. if issue.Poster == nil {
  87. issue.Poster, err = getUserByID(e, issue.PosterID)
  88. if err != nil {
  89. issue.PosterID = -1
  90. issue.Poster = NewGhostUser()
  91. if !IsErrUserNotExist(err) {
  92. return fmt.Errorf("getUserByID.(poster) [%d]: %v", issue.PosterID, err)
  93. }
  94. err = nil
  95. return
  96. }
  97. }
  98. if issue.Labels == nil {
  99. issue.Labels, err = getLabelsByIssueID(e, issue.ID)
  100. if err != nil {
  101. return fmt.Errorf("getLabelsByIssueID [%d]: %v", issue.ID, err)
  102. }
  103. }
  104. if issue.Milestone == nil && issue.MilestoneID > 0 {
  105. issue.Milestone, err = getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
  106. if err != nil {
  107. return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v", issue.RepoID, issue.MilestoneID, err)
  108. }
  109. }
  110. if issue.Assignee == nil && issue.AssigneeID > 0 {
  111. issue.Assignee, err = getUserByID(e, issue.AssigneeID)
  112. if err != nil {
  113. return fmt.Errorf("getUserByID.(assignee) [%d]: %v", issue.AssigneeID, err)
  114. }
  115. }
  116. if issue.IsPull && issue.PullRequest == nil {
  117. // It is possible pull request is not yet created.
  118. issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID)
  119. if err != nil && !IsErrPullRequestNotExist(err) {
  120. return fmt.Errorf("getPullRequestByIssueID [%d]: %v", issue.ID, err)
  121. }
  122. }
  123. if issue.Attachments == nil {
  124. issue.Attachments, err = getAttachmentsByIssueID(e, issue.ID)
  125. if err != nil {
  126. return fmt.Errorf("getAttachmentsByIssueID [%d]: %v", issue.ID, err)
  127. }
  128. }
  129. if issue.Comments == nil {
  130. issue.Comments, err = getCommentsByIssueID(e, issue.ID)
  131. if err != nil {
  132. return fmt.Errorf("getCommentsByIssueID [%d]: %v", issue.ID, err)
  133. }
  134. }
  135. return nil
  136. }
  137. // LoadAttributes loads the attribute of this issue.
  138. func (issue *Issue) LoadAttributes() error {
  139. return issue.loadAttributes(x)
  140. }
  141. // HTMLURL returns the absolute URL to this issue.
  142. func (issue *Issue) HTMLURL() string {
  143. var path string
  144. if issue.IsPull {
  145. path = "pulls"
  146. } else {
  147. path = "issues"
  148. }
  149. return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index)
  150. }
  151. // DiffURL returns the absolute URL to this diff
  152. func (issue *Issue) DiffURL() string {
  153. if issue.IsPull {
  154. return fmt.Sprintf("%s/pulls/%d.diff", issue.Repo.HTMLURL(), issue.Index)
  155. }
  156. return ""
  157. }
  158. // PatchURL returns the absolute URL to this patch
  159. func (issue *Issue) PatchURL() string {
  160. if issue.IsPull {
  161. return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index)
  162. }
  163. return ""
  164. }
  165. // State returns string representation of issue status.
  166. func (issue *Issue) State() api.StateType {
  167. if issue.IsClosed {
  168. return api.StateClosed
  169. }
  170. return api.StateOpen
  171. }
  172. // APIFormat assumes some fields assigned with values:
  173. // Required - Poster, Labels,
  174. // Optional - Milestone, Assignee, PullRequest
  175. func (issue *Issue) APIFormat() *api.Issue {
  176. apiLabels := make([]*api.Label, len(issue.Labels))
  177. for i := range issue.Labels {
  178. apiLabels[i] = issue.Labels[i].APIFormat()
  179. }
  180. apiIssue := &api.Issue{
  181. ID: issue.ID,
  182. Index: issue.Index,
  183. Poster: issue.Poster.APIFormat(),
  184. Title: issue.Title,
  185. Body: issue.Content,
  186. Labels: apiLabels,
  187. State: issue.State(),
  188. Comments: issue.NumComments,
  189. Created: issue.Created,
  190. Updated: issue.Updated,
  191. }
  192. if issue.Milestone != nil {
  193. apiIssue.Milestone = issue.Milestone.APIFormat()
  194. }
  195. if issue.Assignee != nil {
  196. apiIssue.Assignee = issue.Assignee.APIFormat()
  197. }
  198. if issue.IsPull {
  199. apiIssue.PullRequest = &api.PullRequestMeta{
  200. HasMerged: issue.PullRequest.HasMerged,
  201. }
  202. if issue.PullRequest.HasMerged {
  203. apiIssue.PullRequest.Merged = &issue.PullRequest.Merged
  204. }
  205. }
  206. return apiIssue
  207. }
  208. // HashTag returns unique hash tag for issue.
  209. func (issue *Issue) HashTag() string {
  210. return "issue-" + com.ToStr(issue.ID)
  211. }
  212. // IsPoster returns true if given user by ID is the poster.
  213. func (issue *Issue) IsPoster(uid int64) bool {
  214. return issue.PosterID == uid
  215. }
  216. func (issue *Issue) hasLabel(e Engine, labelID int64) bool {
  217. return hasIssueLabel(e, issue.ID, labelID)
  218. }
  219. // HasLabel returns true if issue has been labeled by given ID.
  220. func (issue *Issue) HasLabel(labelID int64) bool {
  221. return issue.hasLabel(x, labelID)
  222. }
  223. func (issue *Issue) sendLabelUpdatedWebhook(doer *User) {
  224. var err error
  225. if issue.IsPull {
  226. err = issue.PullRequest.LoadIssue()
  227. if err != nil {
  228. log.Error(4, "LoadIssue: %v", err)
  229. return
  230. }
  231. err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
  232. Action: api.HookIssueLabelUpdated,
  233. Index: issue.Index,
  234. PullRequest: issue.PullRequest.APIFormat(),
  235. Repository: issue.Repo.APIFormat(AccessModeNone),
  236. Sender: doer.APIFormat(),
  237. })
  238. }
  239. if err != nil {
  240. log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
  241. } else {
  242. go HookQueue.Add(issue.RepoID)
  243. }
  244. }
  245. func (issue *Issue) addLabel(e *xorm.Session, label *Label) error {
  246. return newIssueLabel(e, issue, label)
  247. }
  248. // AddLabel adds a new label to the issue.
  249. func (issue *Issue) AddLabel(doer *User, label *Label) error {
  250. if err := NewIssueLabel(issue, label); err != nil {
  251. return err
  252. }
  253. issue.sendLabelUpdatedWebhook(doer)
  254. return nil
  255. }
  256. func (issue *Issue) addLabels(e *xorm.Session, labels []*Label) error {
  257. return newIssueLabels(e, issue, labels)
  258. }
  259. // AddLabels adds a list of new labels to the issue.
  260. func (issue *Issue) AddLabels(doer *User, labels []*Label) error {
  261. if err := NewIssueLabels(issue, labels); err != nil {
  262. return err
  263. }
  264. issue.sendLabelUpdatedWebhook(doer)
  265. return nil
  266. }
  267. func (issue *Issue) getLabels(e Engine) (err error) {
  268. if len(issue.Labels) > 0 {
  269. return nil
  270. }
  271. issue.Labels, err = getLabelsByIssueID(e, issue.ID)
  272. if err != nil {
  273. return fmt.Errorf("getLabelsByIssueID: %v", err)
  274. }
  275. return nil
  276. }
  277. func (issue *Issue) removeLabel(e *xorm.Session, label *Label) error {
  278. return deleteIssueLabel(e, issue, label)
  279. }
  280. // RemoveLabel removes a label from issue by given ID.
  281. func (issue *Issue) RemoveLabel(doer *User, label *Label) error {
  282. if err := issue.loadRepo(x); err != nil {
  283. return err
  284. }
  285. if has, err := HasAccess(doer, issue.Repo, AccessModeWrite); err != nil {
  286. return err
  287. } else if !has {
  288. return ErrLabelNotExist{}
  289. }
  290. if err := DeleteIssueLabel(issue, label); err != nil {
  291. return err
  292. }
  293. issue.sendLabelUpdatedWebhook(doer)
  294. return nil
  295. }
  296. func (issue *Issue) clearLabels(e *xorm.Session) (err error) {
  297. if err = issue.getLabels(e); err != nil {
  298. return fmt.Errorf("getLabels: %v", err)
  299. }
  300. for i := range issue.Labels {
  301. if err = issue.removeLabel(e, issue.Labels[i]); err != nil {
  302. return fmt.Errorf("removeLabel: %v", err)
  303. }
  304. }
  305. return nil
  306. }
  307. // ClearLabels removes all issue labels as the given user.
  308. // Triggers appropriate WebHooks, if any.
  309. func (issue *Issue) ClearLabels(doer *User) (err error) {
  310. sess := x.NewSession()
  311. defer sessionRelease(sess)
  312. if err = sess.Begin(); err != nil {
  313. return err
  314. }
  315. if err := issue.loadRepo(sess); err != nil {
  316. return err
  317. }
  318. if has, err := hasAccess(sess, doer, issue.Repo, AccessModeWrite); err != nil {
  319. return err
  320. } else if !has {
  321. return ErrLabelNotExist{}
  322. }
  323. if err = issue.clearLabels(sess); err != nil {
  324. return err
  325. }
  326. if err = sess.Commit(); err != nil {
  327. return fmt.Errorf("Commit: %v", err)
  328. }
  329. if issue.IsPull {
  330. err = issue.PullRequest.LoadIssue()
  331. if err != nil {
  332. log.Error(4, "LoadIssue: %v", err)
  333. return
  334. }
  335. err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
  336. Action: api.HookIssueLabelCleared,
  337. Index: issue.Index,
  338. PullRequest: issue.PullRequest.APIFormat(),
  339. Repository: issue.Repo.APIFormat(AccessModeNone),
  340. Sender: doer.APIFormat(),
  341. })
  342. }
  343. if err != nil {
  344. log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
  345. } else {
  346. go HookQueue.Add(issue.RepoID)
  347. }
  348. return nil
  349. }
  350. // ReplaceLabels removes all current labels and add new labels to the issue.
  351. // Triggers appropriate WebHooks, if any.
  352. func (issue *Issue) ReplaceLabels(labels []*Label) (err error) {
  353. sess := x.NewSession()
  354. defer sessionRelease(sess)
  355. if err = sess.Begin(); err != nil {
  356. return err
  357. }
  358. if err = issue.clearLabels(sess); err != nil {
  359. return fmt.Errorf("clearLabels: %v", err)
  360. } else if err = issue.addLabels(sess, labels); err != nil {
  361. return fmt.Errorf("addLabels: %v", err)
  362. }
  363. return sess.Commit()
  364. }
  365. // GetAssignee sets the Assignee attribute of this issue.
  366. func (issue *Issue) GetAssignee() (err error) {
  367. if issue.AssigneeID == 0 || issue.Assignee != nil {
  368. return nil
  369. }
  370. issue.Assignee, err = GetUserByID(issue.AssigneeID)
  371. if IsErrUserNotExist(err) {
  372. return nil
  373. }
  374. return err
  375. }
  376. // ReadBy sets issue to be read by given user.
  377. func (issue *Issue) ReadBy(userID int64) error {
  378. if err := UpdateIssueUserByRead(userID, issue.ID); err != nil {
  379. return err
  380. }
  381. if err := setNotificationStatusReadIfUnread(x, userID, issue.ID); err != nil {
  382. return err
  383. }
  384. return nil
  385. }
  386. func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
  387. _, err := e.Id(issue.ID).Cols(cols...).Update(issue)
  388. return err
  389. }
  390. // UpdateIssueCols only updates values of specific columns for given issue.
  391. func UpdateIssueCols(issue *Issue, cols ...string) error {
  392. return updateIssueCols(x, issue, cols...)
  393. }
  394. func (issue *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository, isClosed bool) (err error) {
  395. // Nothing should be performed if current status is same as target status
  396. if issue.IsClosed == isClosed {
  397. return nil
  398. }
  399. issue.IsClosed = isClosed
  400. if err = updateIssueCols(e, issue, "is_closed"); err != nil {
  401. return err
  402. } else if err = updateIssueUsersByStatus(e, issue.ID, isClosed); err != nil {
  403. return err
  404. }
  405. // Update issue count of labels
  406. if err = issue.getLabels(e); err != nil {
  407. return err
  408. }
  409. for idx := range issue.Labels {
  410. if issue.IsClosed {
  411. issue.Labels[idx].NumClosedIssues++
  412. } else {
  413. issue.Labels[idx].NumClosedIssues--
  414. }
  415. if err = updateLabel(e, issue.Labels[idx]); err != nil {
  416. return err
  417. }
  418. }
  419. // Update issue count of milestone
  420. if err = changeMilestoneIssueStats(e, issue); err != nil {
  421. return err
  422. }
  423. // New action comment
  424. if _, err = createStatusComment(e, doer, repo, issue); err != nil {
  425. return err
  426. }
  427. return nil
  428. }
  429. // ChangeStatus changes issue status to open or closed.
  430. func (issue *Issue) ChangeStatus(doer *User, repo *Repository, isClosed bool) (err error) {
  431. sess := x.NewSession()
  432. defer sessionRelease(sess)
  433. if err = sess.Begin(); err != nil {
  434. return err
  435. }
  436. if err = issue.changeStatus(sess, doer, repo, isClosed); err != nil {
  437. return err
  438. }
  439. if err = sess.Commit(); err != nil {
  440. return fmt.Errorf("Commit: %v", err)
  441. }
  442. if issue.IsPull {
  443. // Merge pull request calls issue.changeStatus so we need to handle separately.
  444. issue.PullRequest.Issue = issue
  445. apiPullRequest := &api.PullRequestPayload{
  446. Index: issue.Index,
  447. PullRequest: issue.PullRequest.APIFormat(),
  448. Repository: repo.APIFormat(AccessModeNone),
  449. Sender: doer.APIFormat(),
  450. }
  451. if isClosed {
  452. apiPullRequest.Action = api.HookIssueClosed
  453. } else {
  454. apiPullRequest.Action = api.HookIssueReOpened
  455. }
  456. err = PrepareWebhooks(repo, HookEventPullRequest, apiPullRequest)
  457. }
  458. if err != nil {
  459. log.Error(4, "PrepareWebhooks [is_pull: %v, is_closed: %v]: %v", issue.IsPull, isClosed, err)
  460. } else {
  461. go HookQueue.Add(repo.ID)
  462. }
  463. return nil
  464. }
  465. // ChangeTitle changes the title of this issue, as the given user.
  466. func (issue *Issue) ChangeTitle(doer *User, title string) (err error) {
  467. oldTitle := issue.Title
  468. issue.Title = title
  469. if err = UpdateIssueCols(issue, "name"); err != nil {
  470. return fmt.Errorf("UpdateIssueCols: %v", err)
  471. }
  472. if issue.IsPull {
  473. issue.PullRequest.Issue = issue
  474. err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
  475. Action: api.HookIssueEdited,
  476. Index: issue.Index,
  477. Changes: &api.ChangesPayload{
  478. Title: &api.ChangesFromPayload{
  479. From: oldTitle,
  480. },
  481. },
  482. PullRequest: issue.PullRequest.APIFormat(),
  483. Repository: issue.Repo.APIFormat(AccessModeNone),
  484. Sender: doer.APIFormat(),
  485. })
  486. }
  487. if err != nil {
  488. log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
  489. } else {
  490. go HookQueue.Add(issue.RepoID)
  491. }
  492. return nil
  493. }
  494. // ChangeContent changes issue content, as the given user.
  495. func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
  496. oldContent := issue.Content
  497. issue.Content = content
  498. if err = UpdateIssueCols(issue, "content"); err != nil {
  499. return fmt.Errorf("UpdateIssueCols: %v", err)
  500. }
  501. if issue.IsPull {
  502. issue.PullRequest.Issue = issue
  503. err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
  504. Action: api.HookIssueEdited,
  505. Index: issue.Index,
  506. Changes: &api.ChangesPayload{
  507. Body: &api.ChangesFromPayload{
  508. From: oldContent,
  509. },
  510. },
  511. PullRequest: issue.PullRequest.APIFormat(),
  512. Repository: issue.Repo.APIFormat(AccessModeNone),
  513. Sender: doer.APIFormat(),
  514. })
  515. }
  516. if err != nil {
  517. log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
  518. } else {
  519. go HookQueue.Add(issue.RepoID)
  520. }
  521. return nil
  522. }
  523. // ChangeAssignee changes the Asssignee field of this issue.
  524. func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
  525. issue.AssigneeID = assigneeID
  526. if err = UpdateIssueUserByAssignee(issue); err != nil {
  527. return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
  528. }
  529. issue.Assignee, err = GetUserByID(issue.AssigneeID)
  530. if err != nil && !IsErrUserNotExist(err) {
  531. log.Error(4, "GetUserByID [assignee_id: %v]: %v", issue.AssigneeID, err)
  532. return nil
  533. }
  534. // Error not nil here means user does not exist, which is remove assignee.
  535. isRemoveAssignee := err != nil
  536. if issue.IsPull {
  537. issue.PullRequest.Issue = issue
  538. apiPullRequest := &api.PullRequestPayload{
  539. Index: issue.Index,
  540. PullRequest: issue.PullRequest.APIFormat(),
  541. Repository: issue.Repo.APIFormat(AccessModeNone),
  542. Sender: doer.APIFormat(),
  543. }
  544. if isRemoveAssignee {
  545. apiPullRequest.Action = api.HookIssueUnassigned
  546. } else {
  547. apiPullRequest.Action = api.HookIssueAssigned
  548. }
  549. err = PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest)
  550. }
  551. if err != nil {
  552. log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, isRemoveAssignee, err)
  553. } else {
  554. go HookQueue.Add(issue.RepoID)
  555. }
  556. return nil
  557. }
  558. // NewIssueOptions represents the options of a new issue.
  559. type NewIssueOptions struct {
  560. Repo *Repository
  561. Issue *Issue
  562. LableIDs []int64
  563. Attachments []string // In UUID format.
  564. IsPull bool
  565. }
  566. func newIssue(e *xorm.Session, opts NewIssueOptions) (err error) {
  567. opts.Issue.Title = strings.TrimSpace(opts.Issue.Title)
  568. opts.Issue.Index = opts.Repo.NextIssueIndex()
  569. if opts.Issue.MilestoneID > 0 {
  570. milestone, err := getMilestoneByRepoID(e, opts.Issue.RepoID, opts.Issue.MilestoneID)
  571. if err != nil && !IsErrMilestoneNotExist(err) {
  572. return fmt.Errorf("getMilestoneByID: %v", err)
  573. }
  574. // Assume milestone is invalid and drop silently.
  575. opts.Issue.MilestoneID = 0
  576. if milestone != nil {
  577. opts.Issue.MilestoneID = milestone.ID
  578. opts.Issue.Milestone = milestone
  579. if err = changeMilestoneAssign(e, opts.Issue, -1); err != nil {
  580. return err
  581. }
  582. }
  583. }
  584. if opts.Issue.AssigneeID > 0 {
  585. assignee, err := getUserByID(e, opts.Issue.AssigneeID)
  586. if err != nil && !IsErrUserNotExist(err) {
  587. return fmt.Errorf("getUserByID: %v", err)
  588. }
  589. // Assume assignee is invalid and drop silently.
  590. opts.Issue.AssigneeID = 0
  591. if assignee != nil {
  592. valid, err := hasAccess(e, assignee, opts.Repo, AccessModeWrite)
  593. if err != nil {
  594. return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assignee.ID, opts.Repo.ID, err)
  595. }
  596. if valid {
  597. opts.Issue.AssigneeID = assignee.ID
  598. opts.Issue.Assignee = assignee
  599. }
  600. }
  601. }
  602. // Milestone and assignee validation should happen before insert actual object.
  603. if _, err = e.Insert(opts.Issue); err != nil {
  604. return err
  605. }
  606. if opts.IsPull {
  607. _, err = e.Exec("UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?", opts.Issue.RepoID)
  608. } else {
  609. _, err = e.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", opts.Issue.RepoID)
  610. }
  611. if err != nil {
  612. return err
  613. }
  614. if len(opts.LableIDs) > 0 {
  615. // During the session, SQLite3 driver cannot handle retrieve objects after update something.
  616. // So we have to get all needed labels first.
  617. labels := make([]*Label, 0, len(opts.LableIDs))
  618. if err = e.In("id", opts.LableIDs).Find(&labels); err != nil {
  619. return fmt.Errorf("find all labels [label_ids: %v]: %v", opts.LableIDs, err)
  620. }
  621. for _, label := range labels {
  622. // Silently drop invalid labels.
  623. if label.RepoID != opts.Repo.ID {
  624. continue
  625. }
  626. if err = opts.Issue.addLabel(e, label); err != nil {
  627. return fmt.Errorf("addLabel [id: %d]: %v", label.ID, err)
  628. }
  629. }
  630. }
  631. if err = newIssueUsers(e, opts.Repo, opts.Issue); err != nil {
  632. return err
  633. }
  634. if len(opts.Attachments) > 0 {
  635. attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
  636. if err != nil {
  637. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err)
  638. }
  639. for i := 0; i < len(attachments); i++ {
  640. attachments[i].IssueID = opts.Issue.ID
  641. if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
  642. return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
  643. }
  644. }
  645. }
  646. return opts.Issue.loadAttributes(e)
  647. }
  648. // NewIssue creates new issue with labels for repository.
  649. func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
  650. sess := x.NewSession()
  651. defer sessionRelease(sess)
  652. if err = sess.Begin(); err != nil {
  653. return err
  654. }
  655. if err = newIssue(sess, NewIssueOptions{
  656. Repo: repo,
  657. Issue: issue,
  658. LableIDs: labelIDs,
  659. Attachments: uuids,
  660. }); err != nil {
  661. return fmt.Errorf("newIssue: %v", err)
  662. }
  663. if err = sess.Commit(); err != nil {
  664. return fmt.Errorf("Commit: %v", err)
  665. }
  666. if err = NotifyWatchers(&Action{
  667. ActUserID: issue.Poster.ID,
  668. ActUserName: issue.Poster.Name,
  669. OpType: ActionCreateIssue,
  670. Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
  671. RepoID: repo.ID,
  672. RepoUserName: repo.Owner.Name,
  673. RepoName: repo.Name,
  674. IsPrivate: repo.IsPrivate,
  675. }); err != nil {
  676. log.Error(4, "NotifyWatchers: %v", err)
  677. }
  678. if err = issue.MailParticipants(); err != nil {
  679. log.Error(4, "MailParticipants: %v", err)
  680. }
  681. return nil
  682. }
  683. // GetIssueByRef returns an Issue specified by a GFM reference.
  684. // See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
  685. func GetIssueByRef(ref string) (*Issue, error) {
  686. n := strings.IndexByte(ref, byte('#'))
  687. if n == -1 {
  688. return nil, errMissingIssueNumber
  689. }
  690. index, err := com.StrTo(ref[n+1:]).Int64()
  691. if err != nil {
  692. return nil, err
  693. }
  694. repo, err := GetRepositoryByRef(ref[:n])
  695. if err != nil {
  696. return nil, err
  697. }
  698. issue, err := GetIssueByIndex(repo.ID, index)
  699. if err != nil {
  700. return nil, err
  701. }
  702. return issue, issue.LoadAttributes()
  703. }
  704. // GetRawIssueByIndex returns raw issue without loading attributes by index in a repository.
  705. func GetRawIssueByIndex(repoID, index int64) (*Issue, error) {
  706. issue := &Issue{
  707. RepoID: repoID,
  708. Index: index,
  709. }
  710. has, err := x.Get(issue)
  711. if err != nil {
  712. return nil, err
  713. } else if !has {
  714. return nil, ErrIssueNotExist{0, repoID, index}
  715. }
  716. return issue, nil
  717. }
  718. // GetIssueByIndex returns issue by index in a repository.
  719. func GetIssueByIndex(repoID, index int64) (*Issue, error) {
  720. issue, err := GetRawIssueByIndex(repoID, index)
  721. if err != nil {
  722. return nil, err
  723. }
  724. return issue, issue.LoadAttributes()
  725. }
  726. func getIssueByID(e Engine, id int64) (*Issue, error) {
  727. issue := new(Issue)
  728. has, err := e.Id(id).Get(issue)
  729. if err != nil {
  730. return nil, err
  731. } else if !has {
  732. return nil, ErrIssueNotExist{id, 0, 0}
  733. }
  734. return issue, issue.LoadAttributes()
  735. }
  736. // GetIssueByID returns an issue by given ID.
  737. func GetIssueByID(id int64) (*Issue, error) {
  738. return getIssueByID(x, id)
  739. }
  740. // IssuesOptions represents options of an issue.
  741. type IssuesOptions struct {
  742. RepoID int64
  743. AssigneeID int64
  744. PosterID int64
  745. MentionedID int64
  746. MilestoneID int64
  747. RepoIDs []int64
  748. Page int
  749. IsClosed bool
  750. IsPull bool
  751. Labels string
  752. SortType string
  753. }
  754. // sortIssuesSession sort an issues-related session based on the provided
  755. // sortType string
  756. func sortIssuesSession(sess *xorm.Session, sortType string) {
  757. switch sortType {
  758. case "oldest":
  759. sess.Asc("issue.created_unix")
  760. case "recentupdate":
  761. sess.Desc("issue.updated_unix")
  762. case "leastupdate":
  763. sess.Asc("issue.updated_unix")
  764. case "mostcomment":
  765. sess.Desc("issue.num_comments")
  766. case "leastcomment":
  767. sess.Asc("issue.num_comments")
  768. case "priority":
  769. sess.Desc("issue.priority")
  770. default:
  771. sess.Desc("issue.created_unix")
  772. }
  773. }
  774. // Issues returns a list of issues by given conditions.
  775. func Issues(opts *IssuesOptions) ([]*Issue, error) {
  776. if opts.Page <= 0 {
  777. opts.Page = 1
  778. }
  779. sess := x.Limit(setting.UI.IssuePagingNum, (opts.Page-1)*setting.UI.IssuePagingNum)
  780. if opts.RepoID > 0 {
  781. sess.And("issue.repo_id=?", opts.RepoID)
  782. } else if len(opts.RepoIDs) > 0 {
  783. // In case repository IDs are provided but actually no repository has issue.
  784. sess.In("issue.repo_id", opts.RepoIDs)
  785. }
  786. sess.And("issue.is_closed=?", opts.IsClosed)
  787. if opts.AssigneeID > 0 {
  788. sess.And("issue.assignee_id=?", opts.AssigneeID)
  789. }
  790. if opts.PosterID > 0 {
  791. sess.And("issue.poster_id=?", opts.PosterID)
  792. }
  793. if opts.MentionedID > 0 {
  794. sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
  795. And("issue_user.is_mentioned = ?", true).
  796. And("issue_user.uid = ?", opts.MentionedID)
  797. }
  798. if opts.MilestoneID > 0 {
  799. sess.And("issue.milestone_id=?", opts.MilestoneID)
  800. }
  801. sess.And("issue.is_pull=?", opts.IsPull)
  802. sortIssuesSession(sess, opts.SortType)
  803. if len(opts.Labels) > 0 && opts.Labels != "0" {
  804. labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ","))
  805. if err != nil {
  806. return nil, err
  807. }
  808. if len(labelIDs) > 0 {
  809. sess.
  810. Join("INNER", "issue_label", "issue.id = issue_label.issue_id").
  811. In("issue_label.label_id", labelIDs)
  812. }
  813. }
  814. issues := make([]*Issue, 0, setting.UI.IssuePagingNum)
  815. if err := sess.Find(&issues); err != nil {
  816. return nil, fmt.Errorf("Find: %v", err)
  817. }
  818. // FIXME: use IssueList to improve performance.
  819. for i := range issues {
  820. if err := issues[i].LoadAttributes(); err != nil {
  821. return nil, fmt.Errorf("LoadAttributes [%d]: %v", issues[i].ID, err)
  822. }
  823. }
  824. return issues, nil
  825. }
  826. // .___ ____ ___
  827. // | | ______ ________ __ ____ | | \______ ___________
  828. // | |/ ___// ___/ | \_/ __ \| | / ___// __ \_ __ \
  829. // | |\___ \ \___ \| | /\ ___/| | /\___ \\ ___/| | \/
  830. // |___/____ >____ >____/ \___ >______//____ >\___ >__|
  831. // \/ \/ \/ \/ \/
  832. // IssueUser represents an issue-user relation.
  833. type IssueUser struct {
  834. ID int64 `xorm:"pk autoincr"`
  835. UID int64 `xorm:"INDEX"` // User ID.
  836. IssueID int64
  837. RepoID int64 `xorm:"INDEX"`
  838. MilestoneID int64
  839. IsRead bool
  840. IsAssigned bool
  841. IsMentioned bool
  842. IsPoster bool
  843. IsClosed bool
  844. }
  845. func newIssueUsers(e *xorm.Session, repo *Repository, issue *Issue) error {
  846. assignees, err := repo.getAssignees(e)
  847. if err != nil {
  848. return fmt.Errorf("getAssignees: %v", err)
  849. }
  850. // Poster can be anyone, append later if not one of assignees.
  851. isPosterAssignee := false
  852. // Leave a seat for poster itself to append later, but if poster is one of assignee
  853. // and just waste 1 unit is cheaper than re-allocate memory once.
  854. issueUsers := make([]*IssueUser, 0, len(assignees)+1)
  855. for _, assignee := range assignees {
  856. isPoster := assignee.ID == issue.PosterID
  857. issueUsers = append(issueUsers, &IssueUser{
  858. IssueID: issue.ID,
  859. RepoID: repo.ID,
  860. UID: assignee.ID,
  861. IsPoster: isPoster,
  862. IsAssigned: assignee.ID == issue.AssigneeID,
  863. })
  864. if !isPosterAssignee && isPoster {
  865. isPosterAssignee = true
  866. }
  867. }
  868. if !isPosterAssignee {
  869. issueUsers = append(issueUsers, &IssueUser{
  870. IssueID: issue.ID,
  871. RepoID: repo.ID,
  872. UID: issue.PosterID,
  873. IsPoster: true,
  874. })
  875. }
  876. if _, err = e.Insert(issueUsers); err != nil {
  877. return err
  878. }
  879. return nil
  880. }
  881. // NewIssueUsers adds new issue-user relations for new issue of repository.
  882. func NewIssueUsers(repo *Repository, issue *Issue) (err error) {
  883. sess := x.NewSession()
  884. defer sessionRelease(sess)
  885. if err = sess.Begin(); err != nil {
  886. return err
  887. }
  888. if err = newIssueUsers(sess, repo, issue); err != nil {
  889. return err
  890. }
  891. return sess.Commit()
  892. }
  893. // PairsContains returns true when pairs list contains given issue.
  894. func PairsContains(ius []*IssueUser, issueID, uid int64) int {
  895. for i := range ius {
  896. if ius[i].IssueID == issueID &&
  897. ius[i].UID == uid {
  898. return i
  899. }
  900. }
  901. return -1
  902. }
  903. // GetIssueUsers returns issue-user pairs by given repository and user.
  904. func GetIssueUsers(rid, uid int64, isClosed bool) ([]*IssueUser, error) {
  905. ius := make([]*IssueUser, 0, 10)
  906. err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoID: rid, UID: uid})
  907. return ius, err
  908. }
  909. // GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs.
  910. func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) {
  911. if len(rids) == 0 {
  912. return []*IssueUser{}, nil
  913. }
  914. ius := make([]*IssueUser, 0, 10)
  915. sess := x.
  916. Limit(20, (page-1)*20).
  917. Where("is_closed=?", isClosed).
  918. In("repo_id", rids)
  919. err := sess.Find(&ius)
  920. return ius, err
  921. }
  922. // GetIssueUserPairsByMode returns issue-user pairs by given repository and user.
  923. func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int) ([]*IssueUser, error) {
  924. ius := make([]*IssueUser, 0, 10)
  925. sess := x.
  926. Limit(20, (page-1)*20).
  927. Where("uid=?", uid).
  928. And("is_closed=?", isClosed)
  929. if rid > 0 {
  930. sess.And("repo_id=?", rid)
  931. }
  932. switch filterMode {
  933. case FilterModeAssign:
  934. sess.And("is_assigned=?", true)
  935. case FilterModeCreate:
  936. sess.And("is_poster=?", true)
  937. default:
  938. return ius, nil
  939. }
  940. err := sess.Find(&ius)
  941. return ius, err
  942. }
  943. // UpdateIssueMentions extracts mentioned people from content and
  944. // updates issue-user relations for them.
  945. func UpdateIssueMentions(e Engine, issueID int64, mentions []string) error {
  946. if len(mentions) == 0 {
  947. return nil
  948. }
  949. for i := range mentions {
  950. mentions[i] = strings.ToLower(mentions[i])
  951. }
  952. users := make([]*User, 0, len(mentions))
  953. if err := e.In("lower_name", mentions).Asc("lower_name").Find(&users); err != nil {
  954. return fmt.Errorf("find mentioned users: %v", err)
  955. }
  956. ids := make([]int64, 0, len(mentions))
  957. for _, user := range users {
  958. ids = append(ids, user.ID)
  959. if !user.IsOrganization() || user.NumMembers == 0 {
  960. continue
  961. }
  962. memberIDs := make([]int64, 0, user.NumMembers)
  963. orgUsers, err := GetOrgUsersByOrgID(user.ID)
  964. if err != nil {
  965. return fmt.Errorf("GetOrgUsersByOrgID [%d]: %v", user.ID, err)
  966. }
  967. for _, orgUser := range orgUsers {
  968. memberIDs = append(memberIDs, orgUser.ID)
  969. }
  970. ids = append(ids, memberIDs...)
  971. }
  972. if err := UpdateIssueUsersByMentions(e, issueID, ids); err != nil {
  973. return fmt.Errorf("UpdateIssueUsersByMentions: %v", err)
  974. }
  975. return nil
  976. }
  977. // IssueStats represents issue statistic information.
  978. type IssueStats struct {
  979. OpenCount, ClosedCount int64
  980. AllCount int64
  981. AssignCount int64
  982. CreateCount int64
  983. MentionCount int64
  984. }
  985. // Filter modes.
  986. const (
  987. FilterModeAll = iota
  988. FilterModeAssign
  989. FilterModeCreate
  990. FilterModeMention
  991. )
  992. func parseCountResult(results []map[string][]byte) int64 {
  993. if len(results) == 0 {
  994. return 0
  995. }
  996. for _, result := range results[0] {
  997. return com.StrTo(string(result)).MustInt64()
  998. }
  999. return 0
  1000. }
  1001. // IssueStatsOptions contains parameters accepted by GetIssueStats.
  1002. type IssueStatsOptions struct {
  1003. RepoID int64
  1004. Labels string
  1005. MilestoneID int64
  1006. AssigneeID int64
  1007. MentionedID int64
  1008. PosterID int64
  1009. IsPull bool
  1010. }
  1011. // GetIssueStats returns issue statistic information by given conditions.
  1012. func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
  1013. stats := &IssueStats{}
  1014. countSession := func(opts *IssueStatsOptions) *xorm.Session {
  1015. sess := x.
  1016. Where("issue.repo_id = ?", opts.RepoID).
  1017. And("is_pull = ?", opts.IsPull)
  1018. if len(opts.Labels) > 0 && opts.Labels != "0" {
  1019. labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ","))
  1020. if err != nil {
  1021. log.Warn("Malformed Labels argument: %s", opts.Labels)
  1022. } else if len(labelIDs) > 0 {
  1023. sess.Join("INNER", "issue_label", "issue.id = issue_id").
  1024. In("label_id", labelIDs)
  1025. }
  1026. }
  1027. if opts.MilestoneID > 0 {
  1028. sess.And("issue.milestone_id = ?", opts.MilestoneID)
  1029. }
  1030. if opts.AssigneeID > 0 {
  1031. sess.And("assignee_id = ?", opts.AssigneeID)
  1032. }
  1033. if opts.PosterID > 0 {
  1034. sess.And("poster_id = ?", opts.PosterID)
  1035. }
  1036. if opts.MentionedID > 0 {
  1037. sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
  1038. And("issue_user.uid = ?", opts.MentionedID).
  1039. And("issue_user.is_mentioned = ?", true)
  1040. }
  1041. return sess
  1042. }
  1043. stats.OpenCount, _ = countSession(opts).
  1044. And("is_closed = ?", false).
  1045. Count(&Issue{})
  1046. stats.ClosedCount, _ = countSession(opts).
  1047. And("is_closed = ?", true).
  1048. Count(&Issue{})
  1049. return stats
  1050. }
  1051. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
  1052. func GetUserIssueStats(repoID, uid int64, repoIDs []int64, filterMode int, isPull bool) *IssueStats {
  1053. stats := &IssueStats{}
  1054. countSession := func(isClosed, isPull bool, repoID int64, repoIDs []int64) *xorm.Session {
  1055. sess := x.
  1056. Where("issue.is_closed = ?", isClosed).
  1057. And("issue.is_pull = ?", isPull)
  1058. if repoID > 0 {
  1059. sess.And("repo_id = ?", repoID)
  1060. } else if len(repoIDs) > 0 {
  1061. sess.In("repo_id", repoIDs)
  1062. }
  1063. return sess
  1064. }
  1065. stats.AssignCount, _ = countSession(false, isPull, repoID, repoIDs).
  1066. And("assignee_id = ?", uid).
  1067. Count(&Issue{})
  1068. stats.CreateCount, _ = countSession(false, isPull, repoID, repoIDs).
  1069. And("poster_id = ?", uid).
  1070. Count(&Issue{})
  1071. openCountSession := countSession(false, isPull, repoID, repoIDs)
  1072. closedCountSession := countSession(true, isPull, repoID, repoIDs)
  1073. switch filterMode {
  1074. case FilterModeAssign:
  1075. openCountSession.And("assignee_id = ?", uid)
  1076. closedCountSession.And("assignee_id = ?", uid)
  1077. case FilterModeCreate:
  1078. openCountSession.And("poster_id = ?", uid)
  1079. closedCountSession.And("poster_id = ?", uid)
  1080. }
  1081. stats.OpenCount, _ = openCountSession.Count(&Issue{})
  1082. stats.ClosedCount, _ = closedCountSession.Count(&Issue{})
  1083. return stats
  1084. }
  1085. // GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
  1086. func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen int64, numClosed int64) {
  1087. countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session {
  1088. sess := x.
  1089. Where("issue.repo_id = ?", isClosed).
  1090. And("is_pull = ?", isPull).
  1091. And("repo_id = ?", repoID)
  1092. return sess
  1093. }
  1094. openCountSession := countSession(false, isPull, repoID)
  1095. closedCountSession := countSession(true, isPull, repoID)
  1096. switch filterMode {
  1097. case FilterModeAssign:
  1098. openCountSession.And("assignee_id = ?", uid)
  1099. closedCountSession.And("assignee_id = ?", uid)
  1100. case FilterModeCreate:
  1101. openCountSession.And("poster_id = ?", uid)
  1102. closedCountSession.And("poster_id = ?", uid)
  1103. }
  1104. openResult, _ := openCountSession.Count(&Issue{})
  1105. closedResult, _ := closedCountSession.Count(&Issue{})
  1106. return openResult, closedResult
  1107. }
  1108. func updateIssue(e Engine, issue *Issue) error {
  1109. _, err := e.Id(issue.ID).AllCols().Update(issue)
  1110. return err
  1111. }
  1112. // UpdateIssue updates all fields of given issue.
  1113. func UpdateIssue(issue *Issue) error {
  1114. return updateIssue(x, issue)
  1115. }
  1116. func updateIssueUsersByStatus(e Engine, issueID int64, isClosed bool) error {
  1117. _, err := e.Exec("UPDATE `issue_user` SET is_closed=? WHERE issue_id=?", isClosed, issueID)
  1118. return err
  1119. }
  1120. // UpdateIssueUsersByStatus updates issue-user relations by issue status.
  1121. func UpdateIssueUsersByStatus(issueID int64, isClosed bool) error {
  1122. return updateIssueUsersByStatus(x, issueID, isClosed)
  1123. }
  1124. func updateIssueUserByAssignee(e *xorm.Session, issue *Issue) (err error) {
  1125. if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?", false, issue.ID); err != nil {
  1126. return err
  1127. }
  1128. // Assignee ID equals to 0 means clear assignee.
  1129. if issue.AssigneeID > 0 {
  1130. if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?", true, issue.AssigneeID, issue.ID); err != nil {
  1131. return err
  1132. }
  1133. }
  1134. return updateIssue(e, issue)
  1135. }
  1136. // UpdateIssueUserByAssignee updates issue-user relation for assignee.
  1137. func UpdateIssueUserByAssignee(issue *Issue) (err error) {
  1138. sess := x.NewSession()
  1139. defer sessionRelease(sess)
  1140. if err = sess.Begin(); err != nil {
  1141. return err
  1142. }
  1143. if err = updateIssueUserByAssignee(sess, issue); err != nil {
  1144. return err
  1145. }
  1146. return sess.Commit()
  1147. }
  1148. // UpdateIssueUserByRead updates issue-user relation for reading.
  1149. func UpdateIssueUserByRead(uid, issueID int64) error {
  1150. _, err := x.Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID)
  1151. return err
  1152. }
  1153. // UpdateIssueUsersByMentions updates issue-user pairs by mentioning.
  1154. func UpdateIssueUsersByMentions(e Engine, issueID int64, uids []int64) error {
  1155. for _, uid := range uids {
  1156. iu := &IssueUser{
  1157. UID: uid,
  1158. IssueID: issueID,
  1159. }
  1160. has, err := e.Get(iu)
  1161. if err != nil {
  1162. return err
  1163. }
  1164. iu.IsMentioned = true
  1165. if has {
  1166. _, err = e.Id(iu.ID).AllCols().Update(iu)
  1167. } else {
  1168. _, err = e.Insert(iu)
  1169. }
  1170. if err != nil {
  1171. return err
  1172. }
  1173. }
  1174. return nil
  1175. }
  1176. // _____ .__.__ __
  1177. // / \ |__| | ____ _______/ |_ ____ ____ ____
  1178. // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
  1179. // / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
  1180. // \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
  1181. // \/ \/ \/ \/ \/
  1182. // Milestone represents a milestone of repository.
  1183. type Milestone struct {
  1184. ID int64 `xorm:"pk autoincr"`
  1185. RepoID int64 `xorm:"INDEX"`
  1186. Name string
  1187. Content string `xorm:"TEXT"`
  1188. RenderedContent string `xorm:"-"`
  1189. IsClosed bool
  1190. NumIssues int
  1191. NumClosedIssues int
  1192. NumOpenIssues int `xorm:"-"`
  1193. Completeness int // Percentage(1-100).
  1194. IsOverDue bool `xorm:"-"`
  1195. DeadlineString string `xorm:"-"`
  1196. Deadline time.Time `xorm:"-"`
  1197. DeadlineUnix int64
  1198. ClosedDate time.Time `xorm:"-"`
  1199. ClosedDateUnix int64
  1200. }
  1201. // BeforeInsert is invoked from XORM before inserting an object of this type.
  1202. func (m *Milestone) BeforeInsert() {
  1203. m.DeadlineUnix = m.Deadline.Unix()
  1204. }
  1205. // BeforeUpdate is invoked from XORM before updating this object.
  1206. func (m *Milestone) BeforeUpdate() {
  1207. if m.NumIssues > 0 {
  1208. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  1209. } else {
  1210. m.Completeness = 0
  1211. }
  1212. m.DeadlineUnix = m.Deadline.Unix()
  1213. m.ClosedDateUnix = m.ClosedDate.Unix()
  1214. }
  1215. // AfterSet is invoked from XORM after setting the value of a field of
  1216. // this object.
  1217. func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
  1218. switch colName {
  1219. case "num_closed_issues":
  1220. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  1221. case "deadline_unix":
  1222. m.Deadline = time.Unix(m.DeadlineUnix, 0).Local()
  1223. if m.Deadline.Year() == 9999 {
  1224. return
  1225. }
  1226. m.DeadlineString = m.Deadline.Format("2006-01-02")
  1227. if time.Now().Local().After(m.Deadline) {
  1228. m.IsOverDue = true
  1229. }
  1230. case "closed_date_unix":
  1231. m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local()
  1232. }
  1233. }
  1234. // State returns string representation of milestone status.
  1235. func (m *Milestone) State() api.StateType {
  1236. if m.IsClosed {
  1237. return api.StateClosed
  1238. }
  1239. return api.StateOpen
  1240. }
  1241. // APIFormat returns this Milestone in API format.
  1242. func (m *Milestone) APIFormat() *api.Milestone {
  1243. apiMilestone := &api.Milestone{
  1244. ID: m.ID,
  1245. State: m.State(),
  1246. Title: m.Name,
  1247. Description: m.Content,
  1248. OpenIssues: m.NumOpenIssues,
  1249. ClosedIssues: m.NumClosedIssues,
  1250. }
  1251. if m.IsClosed {
  1252. apiMilestone.Closed = &m.ClosedDate
  1253. }
  1254. if m.Deadline.Year() < 9999 {
  1255. apiMilestone.Deadline = &m.Deadline
  1256. }
  1257. return apiMilestone
  1258. }
  1259. // NewMilestone creates new milestone of repository.
  1260. func NewMilestone(m *Milestone) (err error) {
  1261. sess := x.NewSession()
  1262. defer sessionRelease(sess)
  1263. if err = sess.Begin(); err != nil {
  1264. return err
  1265. }
  1266. if _, err = sess.Insert(m); err != nil {
  1267. return err
  1268. }
  1269. if _, err = sess.Exec("UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil {
  1270. return err
  1271. }
  1272. return sess.Commit()
  1273. }
  1274. func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
  1275. m := &Milestone{
  1276. ID: id,
  1277. RepoID: repoID,
  1278. }
  1279. has, err := e.Get(m)
  1280. if err != nil {
  1281. return nil, err
  1282. } else if !has {
  1283. return nil, ErrMilestoneNotExist{id, repoID}
  1284. }
  1285. return m, nil
  1286. }
  1287. // GetMilestoneByRepoID returns the milestone in a repository.
  1288. func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
  1289. return getMilestoneByRepoID(x, repoID, id)
  1290. }
  1291. // GetMilestonesByRepoID returns all milestones of a repository.
  1292. func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) {
  1293. miles := make([]*Milestone, 0, 10)
  1294. return miles, x.Where("repo_id = ?", repoID).Find(&miles)
  1295. }
  1296. // GetMilestones returns a list of milestones of given repository and status.
  1297. func GetMilestones(repoID int64, page int, isClosed bool, sortType string) ([]*Milestone, error) {
  1298. miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
  1299. sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
  1300. if page > 0 {
  1301. sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
  1302. }
  1303. switch sortType {
  1304. case "furthestduedate":
  1305. sess.Desc("deadline_unix")
  1306. case "leastcomplete":
  1307. sess.Asc("completeness")
  1308. case "mostcomplete":
  1309. sess.Desc("completeness")
  1310. case "leastissues":
  1311. sess.Asc("num_issues")
  1312. case "mostissues":
  1313. sess.Desc("num_issues")
  1314. default:
  1315. sess.Asc("deadline_unix")
  1316. }
  1317. return miles, sess.Find(&miles)
  1318. }
  1319. func updateMilestone(e Engine, m *Milestone) error {
  1320. _, err := e.Id(m.ID).AllCols().Update(m)
  1321. return err
  1322. }
  1323. // UpdateMilestone updates information of given milestone.
  1324. func UpdateMilestone(m *Milestone) error {
  1325. return updateMilestone(x, m)
  1326. }
  1327. func countRepoMilestones(e Engine, repoID int64) int64 {
  1328. count, _ := e.
  1329. Where("repo_id=?", repoID).
  1330. Count(new(Milestone))
  1331. return count
  1332. }
  1333. // CountRepoMilestones returns number of milestones in given repository.
  1334. func CountRepoMilestones(repoID int64) int64 {
  1335. return countRepoMilestones(x, repoID)
  1336. }
  1337. func countRepoClosedMilestones(e Engine, repoID int64) int64 {
  1338. closed, _ := e.
  1339. Where("repo_id=? AND is_closed=?", repoID, true).
  1340. Count(new(Milestone))
  1341. return closed
  1342. }
  1343. // CountRepoClosedMilestones returns number of closed milestones in given repository.
  1344. func CountRepoClosedMilestones(repoID int64) int64 {
  1345. return countRepoClosedMilestones(x, repoID)
  1346. }
  1347. // MilestoneStats returns number of open and closed milestones of given repository.
  1348. func MilestoneStats(repoID int64) (open int64, closed int64) {
  1349. open, _ = x.
  1350. Where("repo_id=? AND is_closed=?", repoID, false).
  1351. Count(new(Milestone))
  1352. return open, CountRepoClosedMilestones(repoID)
  1353. }
  1354. // ChangeMilestoneStatus changes the milestone open/closed status.
  1355. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  1356. repo, err := GetRepositoryByID(m.RepoID)
  1357. if err != nil {
  1358. return err
  1359. }
  1360. sess := x.NewSession()
  1361. defer sessionRelease(sess)
  1362. if err = sess.Begin(); err != nil {
  1363. return err
  1364. }
  1365. m.IsClosed = isClosed
  1366. if err = updateMilestone(sess, m); err != nil {
  1367. return err
  1368. }
  1369. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  1370. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  1371. if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
  1372. return err
  1373. }
  1374. return sess.Commit()
  1375. }
  1376. func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error {
  1377. if issue.MilestoneID == 0 {
  1378. return nil
  1379. }
  1380. m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
  1381. if err != nil {
  1382. return err
  1383. }
  1384. if issue.IsClosed {
  1385. m.NumOpenIssues--
  1386. m.NumClosedIssues++
  1387. } else {
  1388. m.NumOpenIssues++
  1389. m.NumClosedIssues--
  1390. }
  1391. return updateMilestone(e, m)
  1392. }
  1393. // ChangeMilestoneIssueStats updates the open/closed issues counter and progress
  1394. // for the milestone associated with the given issue.
  1395. func ChangeMilestoneIssueStats(issue *Issue) (err error) {
  1396. sess := x.NewSession()
  1397. defer sessionRelease(sess)
  1398. if err = sess.Begin(); err != nil {
  1399. return err
  1400. }
  1401. if err = changeMilestoneIssueStats(sess, issue); err != nil {
  1402. return err
  1403. }
  1404. return sess.Commit()
  1405. }
  1406. func changeMilestoneAssign(e *xorm.Session, issue *Issue, oldMilestoneID int64) error {
  1407. if oldMilestoneID > 0 {
  1408. m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID)
  1409. if err != nil {
  1410. return err
  1411. }
  1412. m.NumIssues--
  1413. if issue.IsClosed {
  1414. m.NumClosedIssues--
  1415. }
  1416. if err = updateMilestone(e, m); err != nil {
  1417. return err
  1418. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?", issue.ID); err != nil {
  1419. return err
  1420. }
  1421. }
  1422. if issue.MilestoneID > 0 {
  1423. m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
  1424. if err != nil {
  1425. return err
  1426. }
  1427. m.NumIssues++
  1428. if issue.IsClosed {
  1429. m.NumClosedIssues++
  1430. }
  1431. if err = updateMilestone(e, m); err != nil {
  1432. return err
  1433. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?", m.ID, issue.ID); err != nil {
  1434. return err
  1435. }
  1436. }
  1437. return updateIssue(e, issue)
  1438. }
  1439. // ChangeMilestoneAssign changes assignment of milestone for issue.
  1440. func ChangeMilestoneAssign(issue *Issue, oldMilestoneID int64) (err error) {
  1441. sess := x.NewSession()
  1442. defer sess.Close()
  1443. if err = sess.Begin(); err != nil {
  1444. return err
  1445. }
  1446. if err = changeMilestoneAssign(sess, issue, oldMilestoneID); err != nil {
  1447. return err
  1448. }
  1449. return sess.Commit()
  1450. }
  1451. // DeleteMilestoneByRepoID deletes a milestone from a repository.
  1452. func DeleteMilestoneByRepoID(repoID, id int64) error {
  1453. m, err := GetMilestoneByRepoID(repoID, id)
  1454. if err != nil {
  1455. if IsErrMilestoneNotExist(err) {
  1456. return nil
  1457. }
  1458. return err
  1459. }
  1460. repo, err := GetRepositoryByID(m.RepoID)
  1461. if err != nil {
  1462. return err
  1463. }
  1464. sess := x.NewSession()
  1465. defer sessionRelease(sess)
  1466. if err = sess.Begin(); err != nil {
  1467. return err
  1468. }
  1469. if _, err = sess.Id(m.ID).Delete(new(Milestone)); err != nil {
  1470. return err
  1471. }
  1472. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  1473. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  1474. if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
  1475. return err
  1476. }
  1477. if _, err = sess.Exec("UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
  1478. return err
  1479. } else if _, err = sess.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
  1480. return err
  1481. }
  1482. return sess.Commit()
  1483. }