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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package repo
  5. import (
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "code.gitea.io/gitea/models/db"
  13. issues_model "code.gitea.io/gitea/models/issues"
  14. "code.gitea.io/gitea/models/organization"
  15. access_model "code.gitea.io/gitea/models/perm/access"
  16. repo_model "code.gitea.io/gitea/models/repo"
  17. "code.gitea.io/gitea/models/unit"
  18. user_model "code.gitea.io/gitea/models/user"
  19. issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
  20. "code.gitea.io/gitea/modules/optional"
  21. "code.gitea.io/gitea/modules/setting"
  22. api "code.gitea.io/gitea/modules/structs"
  23. "code.gitea.io/gitea/modules/timeutil"
  24. "code.gitea.io/gitea/modules/web"
  25. "code.gitea.io/gitea/routers/api/v1/utils"
  26. "code.gitea.io/gitea/services/context"
  27. "code.gitea.io/gitea/services/convert"
  28. issue_service "code.gitea.io/gitea/services/issue"
  29. )
  30. // SearchIssues searches for issues across the repositories that the user has access to
  31. func SearchIssues(ctx *context.APIContext) {
  32. // swagger:operation GET /repos/issues/search issue issueSearchIssues
  33. // ---
  34. // summary: Search for issues across the repositories that the user has access to
  35. // produces:
  36. // - application/json
  37. // parameters:
  38. // - name: state
  39. // in: query
  40. // description: whether issue is open or closed
  41. // type: string
  42. // - name: labels
  43. // in: query
  44. // description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
  45. // type: string
  46. // - name: milestones
  47. // in: query
  48. // description: comma separated list of milestone names. Fetch only issues that have any of this milestones. Non existent are discarded
  49. // type: string
  50. // - name: q
  51. // in: query
  52. // description: search string
  53. // type: string
  54. // - name: priority_repo_id
  55. // in: query
  56. // description: repository to prioritize in the results
  57. // type: integer
  58. // format: int64
  59. // - name: type
  60. // in: query
  61. // description: filter by type (issues / pulls) if set
  62. // type: string
  63. // - name: since
  64. // in: query
  65. // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
  66. // type: string
  67. // format: date-time
  68. // required: false
  69. // - name: before
  70. // in: query
  71. // description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
  72. // type: string
  73. // format: date-time
  74. // required: false
  75. // - name: assigned
  76. // in: query
  77. // description: filter (issues / pulls) assigned to you, default is false
  78. // type: boolean
  79. // - name: created
  80. // in: query
  81. // description: filter (issues / pulls) created by you, default is false
  82. // type: boolean
  83. // - name: mentioned
  84. // in: query
  85. // description: filter (issues / pulls) mentioning you, default is false
  86. // type: boolean
  87. // - name: review_requested
  88. // in: query
  89. // description: filter pulls requesting your review, default is false
  90. // type: boolean
  91. // - name: reviewed
  92. // in: query
  93. // description: filter pulls reviewed by you, default is false
  94. // type: boolean
  95. // - name: owner
  96. // in: query
  97. // description: filter by owner
  98. // type: string
  99. // - name: team
  100. // in: query
  101. // description: filter by team (requires organization owner parameter to be provided)
  102. // type: string
  103. // - name: page
  104. // in: query
  105. // description: page number of results to return (1-based)
  106. // type: integer
  107. // - name: limit
  108. // in: query
  109. // description: page size of results
  110. // type: integer
  111. // responses:
  112. // "200":
  113. // "$ref": "#/responses/IssueList"
  114. before, since, err := context.GetQueryBeforeSince(ctx.Base)
  115. if err != nil {
  116. ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
  117. return
  118. }
  119. var isClosed optional.Option[bool]
  120. switch ctx.FormString("state") {
  121. case "closed":
  122. isClosed = optional.Some(true)
  123. case "all":
  124. isClosed = optional.None[bool]()
  125. default:
  126. isClosed = optional.Some(false)
  127. }
  128. var (
  129. repoIDs []int64
  130. allPublic bool
  131. )
  132. {
  133. // find repos user can access (for issue search)
  134. opts := &repo_model.SearchRepoOptions{
  135. Private: false,
  136. AllPublic: true,
  137. TopicOnly: false,
  138. Collaborate: optional.None[bool](),
  139. // This needs to be a column that is not nil in fixtures or
  140. // MySQL will return different results when sorting by null in some cases
  141. OrderBy: db.SearchOrderByAlphabetically,
  142. Actor: ctx.Doer,
  143. }
  144. if ctx.IsSigned {
  145. opts.Private = true
  146. opts.AllLimited = true
  147. }
  148. if ctx.FormString("owner") != "" {
  149. owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
  150. if err != nil {
  151. if user_model.IsErrUserNotExist(err) {
  152. ctx.Error(http.StatusBadRequest, "Owner not found", err)
  153. } else {
  154. ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
  155. }
  156. return
  157. }
  158. opts.OwnerID = owner.ID
  159. opts.AllLimited = false
  160. opts.AllPublic = false
  161. opts.Collaborate = optional.Some(false)
  162. }
  163. if ctx.FormString("team") != "" {
  164. if ctx.FormString("owner") == "" {
  165. ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
  166. return
  167. }
  168. team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
  169. if err != nil {
  170. if organization.IsErrTeamNotExist(err) {
  171. ctx.Error(http.StatusBadRequest, "Team not found", err)
  172. } else {
  173. ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
  174. }
  175. return
  176. }
  177. opts.TeamID = team.ID
  178. }
  179. if opts.AllPublic {
  180. allPublic = true
  181. opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
  182. }
  183. repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
  184. if err != nil {
  185. ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err)
  186. return
  187. }
  188. if len(repoIDs) == 0 {
  189. // no repos found, don't let the indexer return all repos
  190. repoIDs = []int64{0}
  191. }
  192. }
  193. keyword := ctx.FormTrim("q")
  194. if strings.IndexByte(keyword, 0) >= 0 {
  195. keyword = ""
  196. }
  197. var isPull optional.Option[bool]
  198. switch ctx.FormString("type") {
  199. case "pulls":
  200. isPull = optional.Some(true)
  201. case "issues":
  202. isPull = optional.Some(false)
  203. default:
  204. isPull = optional.None[bool]()
  205. }
  206. var includedAnyLabels []int64
  207. {
  208. labels := ctx.FormTrim("labels")
  209. var includedLabelNames []string
  210. if len(labels) > 0 {
  211. includedLabelNames = strings.Split(labels, ",")
  212. }
  213. includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
  214. if err != nil {
  215. ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err)
  216. return
  217. }
  218. }
  219. var includedMilestones []int64
  220. {
  221. milestones := ctx.FormTrim("milestones")
  222. var includedMilestoneNames []string
  223. if len(milestones) > 0 {
  224. includedMilestoneNames = strings.Split(milestones, ",")
  225. }
  226. includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
  227. if err != nil {
  228. ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err)
  229. return
  230. }
  231. }
  232. // this api is also used in UI,
  233. // so the default limit is set to fit UI needs
  234. limit := ctx.FormInt("limit")
  235. if limit == 0 {
  236. limit = setting.UI.IssuePagingNum
  237. } else if limit > setting.API.MaxResponseItems {
  238. limit = setting.API.MaxResponseItems
  239. }
  240. searchOpt := &issue_indexer.SearchOptions{
  241. Paginator: &db.ListOptions{
  242. PageSize: limit,
  243. Page: ctx.FormInt("page"),
  244. },
  245. Keyword: keyword,
  246. RepoIDs: repoIDs,
  247. AllPublic: allPublic,
  248. IsPull: isPull,
  249. IsClosed: isClosed,
  250. IncludedAnyLabelIDs: includedAnyLabels,
  251. MilestoneIDs: includedMilestones,
  252. SortBy: issue_indexer.SortByCreatedDesc,
  253. }
  254. if since != 0 {
  255. searchOpt.UpdatedAfterUnix = optional.Some(since)
  256. }
  257. if before != 0 {
  258. searchOpt.UpdatedBeforeUnix = optional.Some(before)
  259. }
  260. if ctx.IsSigned {
  261. ctxUserID := ctx.Doer.ID
  262. if ctx.FormBool("created") {
  263. searchOpt.PosterID = optional.Some(ctxUserID)
  264. }
  265. if ctx.FormBool("assigned") {
  266. searchOpt.AssigneeID = optional.Some(ctxUserID)
  267. }
  268. if ctx.FormBool("mentioned") {
  269. searchOpt.MentionID = optional.Some(ctxUserID)
  270. }
  271. if ctx.FormBool("review_requested") {
  272. searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
  273. }
  274. if ctx.FormBool("reviewed") {
  275. searchOpt.ReviewedID = optional.Some(ctxUserID)
  276. }
  277. }
  278. // FIXME: It's unsupported to sort by priority repo when searching by indexer,
  279. // it's indeed an regression, but I think it is worth to support filtering by indexer first.
  280. _ = ctx.FormInt64("priority_repo_id")
  281. ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
  282. if err != nil {
  283. ctx.Error(http.StatusInternalServerError, "SearchIssues", err)
  284. return
  285. }
  286. issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
  287. if err != nil {
  288. ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err)
  289. return
  290. }
  291. ctx.SetLinkHeader(int(total), limit)
  292. ctx.SetTotalCountHeader(total)
  293. ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
  294. }
  295. // ListIssues list the issues of a repository
  296. func ListIssues(ctx *context.APIContext) {
  297. // swagger:operation GET /repos/{owner}/{repo}/issues issue issueListIssues
  298. // ---
  299. // summary: List a repository's issues
  300. // produces:
  301. // - application/json
  302. // parameters:
  303. // - name: owner
  304. // in: path
  305. // description: owner of the repo
  306. // type: string
  307. // required: true
  308. // - name: repo
  309. // in: path
  310. // description: name of the repo
  311. // type: string
  312. // required: true
  313. // - name: state
  314. // in: query
  315. // description: whether issue is open or closed
  316. // type: string
  317. // enum: [closed, open, all]
  318. // - name: labels
  319. // in: query
  320. // description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
  321. // type: string
  322. // - name: q
  323. // in: query
  324. // description: search string
  325. // type: string
  326. // - name: type
  327. // in: query
  328. // description: filter by type (issues / pulls) if set
  329. // type: string
  330. // enum: [issues, pulls]
  331. // - name: milestones
  332. // in: query
  333. // description: comma separated list of milestone names or ids. It uses names and fall back to ids. Fetch only issues that have any of this milestones. Non existent milestones are discarded
  334. // type: string
  335. // - name: since
  336. // in: query
  337. // description: Only show items updated after the given time. This is a timestamp in RFC 3339 format
  338. // type: string
  339. // format: date-time
  340. // required: false
  341. // - name: before
  342. // in: query
  343. // description: Only show items updated before the given time. This is a timestamp in RFC 3339 format
  344. // type: string
  345. // format: date-time
  346. // required: false
  347. // - name: created_by
  348. // in: query
  349. // description: Only show items which were created by the given user
  350. // type: string
  351. // - name: assigned_by
  352. // in: query
  353. // description: Only show items for which the given user is assigned
  354. // type: string
  355. // - name: mentioned_by
  356. // in: query
  357. // description: Only show items in which the given user was mentioned
  358. // type: string
  359. // - name: page
  360. // in: query
  361. // description: page number of results to return (1-based)
  362. // type: integer
  363. // - name: limit
  364. // in: query
  365. // description: page size of results
  366. // type: integer
  367. // responses:
  368. // "200":
  369. // "$ref": "#/responses/IssueList"
  370. // "404":
  371. // "$ref": "#/responses/notFound"
  372. before, since, err := context.GetQueryBeforeSince(ctx.Base)
  373. if err != nil {
  374. ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
  375. return
  376. }
  377. var isClosed optional.Option[bool]
  378. switch ctx.FormString("state") {
  379. case "closed":
  380. isClosed = optional.Some(true)
  381. case "all":
  382. isClosed = optional.None[bool]()
  383. default:
  384. isClosed = optional.Some(false)
  385. }
  386. keyword := ctx.FormTrim("q")
  387. if strings.IndexByte(keyword, 0) >= 0 {
  388. keyword = ""
  389. }
  390. var labelIDs []int64
  391. if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
  392. labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted)
  393. if err != nil {
  394. ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
  395. return
  396. }
  397. }
  398. var mileIDs []int64
  399. if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
  400. for i := range part {
  401. // uses names and fall back to ids
  402. // non existent milestones are discarded
  403. mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i])
  404. if err == nil {
  405. mileIDs = append(mileIDs, mile.ID)
  406. continue
  407. }
  408. if !issues_model.IsErrMilestoneNotExist(err) {
  409. ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoIDANDName", err)
  410. return
  411. }
  412. id, err := strconv.ParseInt(part[i], 10, 64)
  413. if err != nil {
  414. continue
  415. }
  416. mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id)
  417. if err == nil {
  418. mileIDs = append(mileIDs, mile.ID)
  419. continue
  420. }
  421. if issues_model.IsErrMilestoneNotExist(err) {
  422. continue
  423. }
  424. ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
  425. }
  426. }
  427. listOptions := utils.GetListOptions(ctx)
  428. isPull := optional.None[bool]()
  429. switch ctx.FormString("type") {
  430. case "pulls":
  431. isPull = optional.Some(true)
  432. case "issues":
  433. isPull = optional.Some(false)
  434. }
  435. if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) {
  436. ctx.NotFound()
  437. return
  438. }
  439. if !isPull.Has() {
  440. canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
  441. canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
  442. if !canReadIssues && !canReadPulls {
  443. ctx.NotFound()
  444. return
  445. } else if !canReadIssues {
  446. isPull = optional.Some(true)
  447. } else if !canReadPulls {
  448. isPull = optional.Some(false)
  449. }
  450. }
  451. // FIXME: we should be more efficient here
  452. createdByID := getUserIDForFilter(ctx, "created_by")
  453. if ctx.Written() {
  454. return
  455. }
  456. assignedByID := getUserIDForFilter(ctx, "assigned_by")
  457. if ctx.Written() {
  458. return
  459. }
  460. mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
  461. if ctx.Written() {
  462. return
  463. }
  464. searchOpt := &issue_indexer.SearchOptions{
  465. Paginator: &listOptions,
  466. Keyword: keyword,
  467. RepoIDs: []int64{ctx.Repo.Repository.ID},
  468. IsPull: isPull,
  469. IsClosed: isClosed,
  470. SortBy: issue_indexer.SortByCreatedDesc,
  471. }
  472. if since != 0 {
  473. searchOpt.UpdatedAfterUnix = optional.Some(since)
  474. }
  475. if before != 0 {
  476. searchOpt.UpdatedBeforeUnix = optional.Some(before)
  477. }
  478. if len(labelIDs) == 1 && labelIDs[0] == 0 {
  479. searchOpt.NoLabelOnly = true
  480. } else {
  481. for _, labelID := range labelIDs {
  482. if labelID > 0 {
  483. searchOpt.IncludedLabelIDs = append(searchOpt.IncludedLabelIDs, labelID)
  484. } else {
  485. searchOpt.ExcludedLabelIDs = append(searchOpt.ExcludedLabelIDs, -labelID)
  486. }
  487. }
  488. }
  489. if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
  490. searchOpt.MilestoneIDs = []int64{0}
  491. } else {
  492. searchOpt.MilestoneIDs = mileIDs
  493. }
  494. if createdByID > 0 {
  495. searchOpt.PosterID = optional.Some(createdByID)
  496. }
  497. if assignedByID > 0 {
  498. searchOpt.AssigneeID = optional.Some(assignedByID)
  499. }
  500. if mentionedByID > 0 {
  501. searchOpt.MentionID = optional.Some(mentionedByID)
  502. }
  503. ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
  504. if err != nil {
  505. ctx.Error(http.StatusInternalServerError, "SearchIssues", err)
  506. return
  507. }
  508. issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
  509. if err != nil {
  510. ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err)
  511. return
  512. }
  513. ctx.SetLinkHeader(int(total), listOptions.PageSize)
  514. ctx.SetTotalCountHeader(total)
  515. ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
  516. }
  517. func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
  518. userName := ctx.FormString(queryName)
  519. if len(userName) == 0 {
  520. return 0
  521. }
  522. user, err := user_model.GetUserByName(ctx, userName)
  523. if user_model.IsErrUserNotExist(err) {
  524. ctx.NotFound(err)
  525. return 0
  526. }
  527. if err != nil {
  528. ctx.InternalServerError(err)
  529. return 0
  530. }
  531. return user.ID
  532. }
  533. // GetIssue get an issue of a repository
  534. func GetIssue(ctx *context.APIContext) {
  535. // swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
  536. // ---
  537. // summary: Get an issue
  538. // produces:
  539. // - application/json
  540. // parameters:
  541. // - name: owner
  542. // in: path
  543. // description: owner of the repo
  544. // type: string
  545. // required: true
  546. // - name: repo
  547. // in: path
  548. // description: name of the repo
  549. // type: string
  550. // required: true
  551. // - name: index
  552. // in: path
  553. // description: index of the issue to get
  554. // type: integer
  555. // format: int64
  556. // required: true
  557. // responses:
  558. // "200":
  559. // "$ref": "#/responses/Issue"
  560. // "404":
  561. // "$ref": "#/responses/notFound"
  562. issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  563. if err != nil {
  564. if issues_model.IsErrIssueNotExist(err) {
  565. ctx.NotFound()
  566. } else {
  567. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  568. }
  569. return
  570. }
  571. if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
  572. ctx.NotFound()
  573. return
  574. }
  575. ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue))
  576. }
  577. // CreateIssue create an issue of a repository
  578. func CreateIssue(ctx *context.APIContext) {
  579. // swagger:operation POST /repos/{owner}/{repo}/issues issue issueCreateIssue
  580. // ---
  581. // summary: Create an issue. If using deadline only the date will be taken into account, and time of day ignored.
  582. // consumes:
  583. // - application/json
  584. // produces:
  585. // - application/json
  586. // parameters:
  587. // - name: owner
  588. // in: path
  589. // description: owner of the repo
  590. // type: string
  591. // required: true
  592. // - name: repo
  593. // in: path
  594. // description: name of the repo
  595. // type: string
  596. // required: true
  597. // - name: body
  598. // in: body
  599. // schema:
  600. // "$ref": "#/definitions/CreateIssueOption"
  601. // responses:
  602. // "201":
  603. // "$ref": "#/responses/Issue"
  604. // "403":
  605. // "$ref": "#/responses/forbidden"
  606. // "404":
  607. // "$ref": "#/responses/notFound"
  608. // "412":
  609. // "$ref": "#/responses/error"
  610. // "422":
  611. // "$ref": "#/responses/validationError"
  612. // "423":
  613. // "$ref": "#/responses/repoArchivedError"
  614. form := web.GetForm(ctx).(*api.CreateIssueOption)
  615. var deadlineUnix timeutil.TimeStamp
  616. if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
  617. deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
  618. }
  619. issue := &issues_model.Issue{
  620. RepoID: ctx.Repo.Repository.ID,
  621. Repo: ctx.Repo.Repository,
  622. Title: form.Title,
  623. PosterID: ctx.Doer.ID,
  624. Poster: ctx.Doer,
  625. Content: form.Body,
  626. Ref: form.Ref,
  627. DeadlineUnix: deadlineUnix,
  628. }
  629. assigneeIDs := make([]int64, 0)
  630. var err error
  631. if ctx.Repo.CanWrite(unit.TypeIssues) {
  632. issue.MilestoneID = form.Milestone
  633. assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
  634. if err != nil {
  635. if user_model.IsErrUserNotExist(err) {
  636. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
  637. } else {
  638. ctx.Error(http.StatusInternalServerError, "AddAssigneeByName", err)
  639. }
  640. return
  641. }
  642. // Check if the passed assignees is assignable
  643. for _, aID := range assigneeIDs {
  644. assignee, err := user_model.GetUserByID(ctx, aID)
  645. if err != nil {
  646. ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
  647. return
  648. }
  649. valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository, false)
  650. if err != nil {
  651. ctx.Error(http.StatusInternalServerError, "canBeAssigned", err)
  652. return
  653. }
  654. if !valid {
  655. ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
  656. return
  657. }
  658. }
  659. } else {
  660. // setting labels is not allowed if user is not a writer
  661. form.Labels = make([]int64, 0)
  662. }
  663. if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
  664. if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
  665. ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
  666. } else if errors.Is(err, user_model.ErrBlockedUser) {
  667. ctx.Error(http.StatusForbidden, "NewIssue", err)
  668. } else {
  669. ctx.Error(http.StatusInternalServerError, "NewIssue", err)
  670. }
  671. return
  672. }
  673. if form.Closed {
  674. if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", true); err != nil {
  675. if issues_model.IsErrDependenciesLeft(err) {
  676. ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
  677. return
  678. }
  679. ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
  680. return
  681. }
  682. }
  683. // Refetch from database to assign some automatic values
  684. issue, err = issues_model.GetIssueByID(ctx, issue.ID)
  685. if err != nil {
  686. ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
  687. return
  688. }
  689. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue))
  690. }
  691. // EditIssue modify an issue of a repository
  692. func EditIssue(ctx *context.APIContext) {
  693. // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue
  694. // ---
  695. // summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.
  696. // consumes:
  697. // - application/json
  698. // produces:
  699. // - application/json
  700. // parameters:
  701. // - name: owner
  702. // in: path
  703. // description: owner of the repo
  704. // type: string
  705. // required: true
  706. // - name: repo
  707. // in: path
  708. // description: name of the repo
  709. // type: string
  710. // required: true
  711. // - name: index
  712. // in: path
  713. // description: index of the issue to edit
  714. // type: integer
  715. // format: int64
  716. // required: true
  717. // - name: body
  718. // in: body
  719. // schema:
  720. // "$ref": "#/definitions/EditIssueOption"
  721. // responses:
  722. // "201":
  723. // "$ref": "#/responses/Issue"
  724. // "403":
  725. // "$ref": "#/responses/forbidden"
  726. // "404":
  727. // "$ref": "#/responses/notFound"
  728. // "412":
  729. // "$ref": "#/responses/error"
  730. form := web.GetForm(ctx).(*api.EditIssueOption)
  731. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  732. if err != nil {
  733. if issues_model.IsErrIssueNotExist(err) {
  734. ctx.NotFound()
  735. } else {
  736. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  737. }
  738. return
  739. }
  740. issue.Repo = ctx.Repo.Repository
  741. canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
  742. err = issue.LoadAttributes(ctx)
  743. if err != nil {
  744. ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
  745. return
  746. }
  747. if !issue.IsPoster(ctx.Doer.ID) && !canWrite {
  748. ctx.Status(http.StatusForbidden)
  749. return
  750. }
  751. if len(form.Title) > 0 {
  752. err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
  753. if err != nil {
  754. ctx.Error(http.StatusInternalServerError, "ChangeTitle", err)
  755. return
  756. }
  757. }
  758. if form.Body != nil {
  759. err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
  760. if err != nil {
  761. ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
  762. return
  763. }
  764. }
  765. if form.Ref != nil {
  766. err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref)
  767. if err != nil {
  768. ctx.Error(http.StatusInternalServerError, "UpdateRef", err)
  769. return
  770. }
  771. }
  772. // Update or remove the deadline, only if set and allowed
  773. if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
  774. var deadlineUnix timeutil.TimeStamp
  775. if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
  776. deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  777. 23, 59, 59, 0, form.Deadline.Location())
  778. deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  779. }
  780. if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
  781. ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  782. return
  783. }
  784. issue.DeadlineUnix = deadlineUnix
  785. }
  786. // Add/delete assignees
  787. // Deleting is done the GitHub way (quote from their api documentation):
  788. // https://developer.github.com/v3/issues/#edit-an-issue
  789. // "assignees" (array): Logins for Users to assign to this issue.
  790. // Pass one or more user logins to replace the set of assignees on this Issue.
  791. // Send an empty array ([]) to clear all assignees from the Issue.
  792. if canWrite && (form.Assignees != nil || form.Assignee != nil) {
  793. oneAssignee := ""
  794. if form.Assignee != nil {
  795. oneAssignee = *form.Assignee
  796. }
  797. err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
  798. if err != nil {
  799. if errors.Is(err, user_model.ErrBlockedUser) {
  800. ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
  801. } else {
  802. ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
  803. }
  804. return
  805. }
  806. }
  807. if canWrite && form.Milestone != nil &&
  808. issue.MilestoneID != *form.Milestone {
  809. oldMilestoneID := issue.MilestoneID
  810. issue.MilestoneID = *form.Milestone
  811. if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
  812. ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
  813. return
  814. }
  815. }
  816. if form.State != nil {
  817. if issue.IsPull {
  818. if err := issue.LoadPullRequest(ctx); err != nil {
  819. ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
  820. return
  821. }
  822. if issue.PullRequest.HasMerged {
  823. ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
  824. return
  825. }
  826. }
  827. if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", api.StateClosed == api.StateType(*form.State)); err != nil {
  828. if issues_model.IsErrDependenciesLeft(err) {
  829. ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
  830. return
  831. }
  832. ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
  833. return
  834. }
  835. }
  836. // Refetch from database to assign some automatic values
  837. issue, err = issues_model.GetIssueByID(ctx, issue.ID)
  838. if err != nil {
  839. ctx.InternalServerError(err)
  840. return
  841. }
  842. if err = issue.LoadMilestone(ctx); err != nil {
  843. ctx.InternalServerError(err)
  844. return
  845. }
  846. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue))
  847. }
  848. func DeleteIssue(ctx *context.APIContext) {
  849. // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index} issue issueDelete
  850. // ---
  851. // summary: Delete an issue
  852. // parameters:
  853. // - name: owner
  854. // in: path
  855. // description: owner of the repo
  856. // type: string
  857. // required: true
  858. // - name: repo
  859. // in: path
  860. // description: name of the repo
  861. // type: string
  862. // required: true
  863. // - name: index
  864. // in: path
  865. // description: index of issue to delete
  866. // type: integer
  867. // format: int64
  868. // required: true
  869. // responses:
  870. // "204":
  871. // "$ref": "#/responses/empty"
  872. // "403":
  873. // "$ref": "#/responses/forbidden"
  874. // "404":
  875. // "$ref": "#/responses/notFound"
  876. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  877. if err != nil {
  878. if issues_model.IsErrIssueNotExist(err) {
  879. ctx.NotFound(err)
  880. } else {
  881. ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
  882. }
  883. return
  884. }
  885. if err = issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil {
  886. ctx.Error(http.StatusInternalServerError, "DeleteIssueByID", err)
  887. return
  888. }
  889. ctx.Status(http.StatusNoContent)
  890. }
  891. // UpdateIssueDeadline updates an issue deadline
  892. func UpdateIssueDeadline(ctx *context.APIContext) {
  893. // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline
  894. // ---
  895. // summary: Set an issue deadline. If set to null, the deadline is deleted. If using deadline only the date will be taken into account, and time of day ignored.
  896. // consumes:
  897. // - application/json
  898. // produces:
  899. // - application/json
  900. // parameters:
  901. // - name: owner
  902. // in: path
  903. // description: owner of the repo
  904. // type: string
  905. // required: true
  906. // - name: repo
  907. // in: path
  908. // description: name of the repo
  909. // type: string
  910. // required: true
  911. // - name: index
  912. // in: path
  913. // description: index of the issue to create or update a deadline on
  914. // type: integer
  915. // format: int64
  916. // required: true
  917. // - name: body
  918. // in: body
  919. // schema:
  920. // "$ref": "#/definitions/EditDeadlineOption"
  921. // responses:
  922. // "201":
  923. // "$ref": "#/responses/IssueDeadline"
  924. // "403":
  925. // "$ref": "#/responses/forbidden"
  926. // "404":
  927. // "$ref": "#/responses/notFound"
  928. form := web.GetForm(ctx).(*api.EditDeadlineOption)
  929. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  930. if err != nil {
  931. if issues_model.IsErrIssueNotExist(err) {
  932. ctx.NotFound()
  933. } else {
  934. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  935. }
  936. return
  937. }
  938. if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  939. ctx.Error(http.StatusForbidden, "", "Not repo writer")
  940. return
  941. }
  942. var deadlineUnix timeutil.TimeStamp
  943. var deadline time.Time
  944. if form.Deadline != nil && !form.Deadline.IsZero() {
  945. deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  946. 23, 59, 59, 0, time.Local)
  947. deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  948. }
  949. if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
  950. ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  951. return
  952. }
  953. ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
  954. }