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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "fmt"
  8. "net/http"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "code.gitea.io/gitea/models"
  13. "code.gitea.io/gitea/modules/context"
  14. "code.gitea.io/gitea/modules/convert"
  15. issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
  16. "code.gitea.io/gitea/modules/log"
  17. "code.gitea.io/gitea/modules/notification"
  18. "code.gitea.io/gitea/modules/setting"
  19. api "code.gitea.io/gitea/modules/structs"
  20. "code.gitea.io/gitea/modules/timeutil"
  21. "code.gitea.io/gitea/modules/util"
  22. "code.gitea.io/gitea/routers/api/v1/utils"
  23. issue_service "code.gitea.io/gitea/services/issue"
  24. )
  25. // SearchIssues searches for issues across the repositories that the user has access to
  26. func SearchIssues(ctx *context.APIContext) {
  27. // swagger:operation GET /repos/issues/search issue issueSearchIssues
  28. // ---
  29. // summary: Search for issues across the repositories that the user has access to
  30. // produces:
  31. // - application/json
  32. // parameters:
  33. // - name: state
  34. // in: query
  35. // description: whether issue is open or closed
  36. // type: string
  37. // - name: labels
  38. // in: query
  39. // description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
  40. // type: string
  41. // - name: q
  42. // in: query
  43. // description: search string
  44. // type: string
  45. // - name: priority_repo_id
  46. // in: query
  47. // description: repository to prioritize in the results
  48. // type: integer
  49. // format: int64
  50. // - name: type
  51. // in: query
  52. // description: filter by type (issues / pulls) if set
  53. // type: string
  54. // - name: page
  55. // in: query
  56. // description: page number of requested issues
  57. // type: integer
  58. // responses:
  59. // "200":
  60. // "$ref": "#/responses/IssueList"
  61. var isClosed util.OptionalBool
  62. switch ctx.Query("state") {
  63. case "closed":
  64. isClosed = util.OptionalBoolTrue
  65. case "all":
  66. isClosed = util.OptionalBoolNone
  67. default:
  68. isClosed = util.OptionalBoolFalse
  69. }
  70. // find repos user can access (for issue search)
  71. repoIDs := make([]int64, 0)
  72. opts := &models.SearchRepoOptions{
  73. ListOptions: models.ListOptions{
  74. PageSize: 15,
  75. },
  76. Private: false,
  77. AllPublic: true,
  78. TopicOnly: false,
  79. Collaborate: util.OptionalBoolNone,
  80. // This needs to be a column that is not nil in fixtures or
  81. // MySQL will return different results when sorting by null in some cases
  82. OrderBy: models.SearchOrderByAlphabetically,
  83. Actor: ctx.User,
  84. }
  85. if ctx.IsSigned {
  86. opts.Private = true
  87. opts.AllLimited = true
  88. }
  89. issueCount := 0
  90. for page := 1; ; page++ {
  91. opts.Page = page
  92. repos, count, err := models.SearchRepositoryByName(opts)
  93. if err != nil {
  94. ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err)
  95. return
  96. }
  97. if len(repos) == 0 {
  98. break
  99. }
  100. log.Trace("Processing next %d repos of %d", len(repos), count)
  101. for _, repo := range repos {
  102. switch isClosed {
  103. case util.OptionalBoolTrue:
  104. issueCount += repo.NumClosedIssues
  105. case util.OptionalBoolFalse:
  106. issueCount += repo.NumOpenIssues
  107. case util.OptionalBoolNone:
  108. issueCount += repo.NumIssues
  109. }
  110. repoIDs = append(repoIDs, repo.ID)
  111. }
  112. }
  113. var issues []*models.Issue
  114. keyword := strings.Trim(ctx.Query("q"), " ")
  115. if strings.IndexByte(keyword, 0) >= 0 {
  116. keyword = ""
  117. }
  118. var issueIDs []int64
  119. var labelIDs []int64
  120. var err error
  121. if len(keyword) > 0 && len(repoIDs) > 0 {
  122. issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword)
  123. }
  124. var isPull util.OptionalBool
  125. switch ctx.Query("type") {
  126. case "pulls":
  127. isPull = util.OptionalBoolTrue
  128. case "issues":
  129. isPull = util.OptionalBoolFalse
  130. default:
  131. isPull = util.OptionalBoolNone
  132. }
  133. labels := strings.TrimSpace(ctx.Query("labels"))
  134. var includedLabelNames []string
  135. if len(labels) > 0 {
  136. includedLabelNames = strings.Split(labels, ",")
  137. }
  138. // Only fetch the issues if we either don't have a keyword or the search returned issues
  139. // This would otherwise return all issues if no issues were found by the search.
  140. if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
  141. issues, err = models.Issues(&models.IssuesOptions{
  142. ListOptions: models.ListOptions{
  143. Page: ctx.QueryInt("page"),
  144. PageSize: setting.UI.IssuePagingNum,
  145. },
  146. RepoIDs: repoIDs,
  147. IsClosed: isClosed,
  148. IssueIDs: issueIDs,
  149. IncludedLabelNames: includedLabelNames,
  150. SortType: "priorityrepo",
  151. PriorityRepoID: ctx.QueryInt64("priority_repo_id"),
  152. IsPull: isPull,
  153. })
  154. }
  155. if err != nil {
  156. ctx.Error(http.StatusInternalServerError, "Issues", err)
  157. return
  158. }
  159. ctx.SetLinkHeader(issueCount, setting.UI.IssuePagingNum)
  160. ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
  161. }
  162. // ListIssues list the issues of a repository
  163. func ListIssues(ctx *context.APIContext) {
  164. // swagger:operation GET /repos/{owner}/{repo}/issues issue issueListIssues
  165. // ---
  166. // summary: List a repository's issues
  167. // produces:
  168. // - application/json
  169. // parameters:
  170. // - name: owner
  171. // in: path
  172. // description: owner of the repo
  173. // type: string
  174. // required: true
  175. // - name: repo
  176. // in: path
  177. // description: name of the repo
  178. // type: string
  179. // required: true
  180. // - name: state
  181. // in: query
  182. // description: whether issue is open or closed
  183. // type: string
  184. // enum: [closed, open, all]
  185. // - name: labels
  186. // in: query
  187. // description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
  188. // type: string
  189. // - name: q
  190. // in: query
  191. // description: search string
  192. // type: string
  193. // - name: type
  194. // in: query
  195. // description: filter by type (issues / pulls) if set
  196. // type: string
  197. // enum: [issues, pulls]
  198. // - name: milestones
  199. // in: query
  200. // 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
  201. // type: string
  202. // - name: page
  203. // in: query
  204. // description: page number of results to return (1-based)
  205. // type: integer
  206. // - name: limit
  207. // in: query
  208. // description: page size of results
  209. // type: integer
  210. // responses:
  211. // "200":
  212. // "$ref": "#/responses/IssueList"
  213. var isClosed util.OptionalBool
  214. switch ctx.Query("state") {
  215. case "closed":
  216. isClosed = util.OptionalBoolTrue
  217. case "all":
  218. isClosed = util.OptionalBoolNone
  219. default:
  220. isClosed = util.OptionalBoolFalse
  221. }
  222. var issues []*models.Issue
  223. keyword := strings.Trim(ctx.Query("q"), " ")
  224. if strings.IndexByte(keyword, 0) >= 0 {
  225. keyword = ""
  226. }
  227. var issueIDs []int64
  228. var labelIDs []int64
  229. var err error
  230. if len(keyword) > 0 {
  231. issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword)
  232. }
  233. if splitted := strings.Split(ctx.Query("labels"), ","); len(splitted) > 0 {
  234. labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
  235. if err != nil {
  236. ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
  237. return
  238. }
  239. }
  240. var mileIDs []int64
  241. if part := strings.Split(ctx.Query("milestones"), ","); len(part) > 0 {
  242. for i := range part {
  243. // uses names and fall back to ids
  244. // non existent milestones are discarded
  245. mile, err := models.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i])
  246. if err == nil {
  247. mileIDs = append(mileIDs, mile.ID)
  248. continue
  249. }
  250. if !models.IsErrMilestoneNotExist(err) {
  251. ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoIDANDName", err)
  252. return
  253. }
  254. id, err := strconv.ParseInt(part[i], 10, 64)
  255. if err != nil {
  256. continue
  257. }
  258. mile, err = models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, id)
  259. if err == nil {
  260. mileIDs = append(mileIDs, mile.ID)
  261. continue
  262. }
  263. if models.IsErrMilestoneNotExist(err) {
  264. continue
  265. }
  266. ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
  267. }
  268. }
  269. listOptions := utils.GetListOptions(ctx)
  270. if ctx.QueryInt("limit") == 0 {
  271. listOptions.PageSize = setting.UI.IssuePagingNum
  272. }
  273. var isPull util.OptionalBool
  274. switch ctx.Query("type") {
  275. case "pulls":
  276. isPull = util.OptionalBoolTrue
  277. case "issues":
  278. isPull = util.OptionalBoolFalse
  279. default:
  280. isPull = util.OptionalBoolNone
  281. }
  282. // Only fetch the issues if we either don't have a keyword or the search returned issues
  283. // This would otherwise return all issues if no issues were found by the search.
  284. if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
  285. issues, err = models.Issues(&models.IssuesOptions{
  286. ListOptions: listOptions,
  287. RepoIDs: []int64{ctx.Repo.Repository.ID},
  288. IsClosed: isClosed,
  289. IssueIDs: issueIDs,
  290. LabelIDs: labelIDs,
  291. MilestoneIDs: mileIDs,
  292. IsPull: isPull,
  293. })
  294. }
  295. if err != nil {
  296. ctx.Error(http.StatusInternalServerError, "Issues", err)
  297. return
  298. }
  299. ctx.SetLinkHeader(ctx.Repo.Repository.NumIssues, listOptions.PageSize)
  300. ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", ctx.Repo.Repository.NumIssues))
  301. ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
  302. }
  303. // GetIssue get an issue of a repository
  304. func GetIssue(ctx *context.APIContext) {
  305. // swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
  306. // ---
  307. // summary: Get an issue
  308. // produces:
  309. // - application/json
  310. // parameters:
  311. // - name: owner
  312. // in: path
  313. // description: owner of the repo
  314. // type: string
  315. // required: true
  316. // - name: repo
  317. // in: path
  318. // description: name of the repo
  319. // type: string
  320. // required: true
  321. // - name: index
  322. // in: path
  323. // description: index of the issue to get
  324. // type: integer
  325. // format: int64
  326. // required: true
  327. // responses:
  328. // "200":
  329. // "$ref": "#/responses/Issue"
  330. // "404":
  331. // "$ref": "#/responses/notFound"
  332. issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  333. if err != nil {
  334. if models.IsErrIssueNotExist(err) {
  335. ctx.NotFound()
  336. } else {
  337. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  338. }
  339. return
  340. }
  341. ctx.JSON(http.StatusOK, convert.ToAPIIssue(issue))
  342. }
  343. // CreateIssue create an issue of a repository
  344. func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) {
  345. // swagger:operation POST /repos/{owner}/{repo}/issues issue issueCreateIssue
  346. // ---
  347. // summary: Create an issue. If using deadline only the date will be taken into account, and time of day ignored.
  348. // consumes:
  349. // - application/json
  350. // produces:
  351. // - application/json
  352. // parameters:
  353. // - name: owner
  354. // in: path
  355. // description: owner of the repo
  356. // type: string
  357. // required: true
  358. // - name: repo
  359. // in: path
  360. // description: name of the repo
  361. // type: string
  362. // required: true
  363. // - name: body
  364. // in: body
  365. // schema:
  366. // "$ref": "#/definitions/CreateIssueOption"
  367. // responses:
  368. // "201":
  369. // "$ref": "#/responses/Issue"
  370. // "403":
  371. // "$ref": "#/responses/forbidden"
  372. // "412":
  373. // "$ref": "#/responses/error"
  374. // "422":
  375. // "$ref": "#/responses/validationError"
  376. var deadlineUnix timeutil.TimeStamp
  377. if form.Deadline != nil && ctx.Repo.CanWrite(models.UnitTypeIssues) {
  378. deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
  379. }
  380. issue := &models.Issue{
  381. RepoID: ctx.Repo.Repository.ID,
  382. Repo: ctx.Repo.Repository,
  383. Title: form.Title,
  384. PosterID: ctx.User.ID,
  385. Poster: ctx.User,
  386. Content: form.Body,
  387. DeadlineUnix: deadlineUnix,
  388. }
  389. var assigneeIDs = make([]int64, 0)
  390. var err error
  391. if ctx.Repo.CanWrite(models.UnitTypeIssues) {
  392. issue.MilestoneID = form.Milestone
  393. assigneeIDs, err = models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
  394. if err != nil {
  395. if models.IsErrUserNotExist(err) {
  396. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
  397. } else {
  398. ctx.Error(http.StatusInternalServerError, "AddAssigneeByName", err)
  399. }
  400. return
  401. }
  402. // Check if the passed assignees is assignable
  403. for _, aID := range assigneeIDs {
  404. assignee, err := models.GetUserByID(aID)
  405. if err != nil {
  406. ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
  407. return
  408. }
  409. valid, err := models.CanBeAssigned(assignee, ctx.Repo.Repository, false)
  410. if err != nil {
  411. ctx.Error(http.StatusInternalServerError, "canBeAssigned", err)
  412. return
  413. }
  414. if !valid {
  415. ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
  416. return
  417. }
  418. }
  419. } else {
  420. // setting labels is not allowed if user is not a writer
  421. form.Labels = make([]int64, 0)
  422. }
  423. if err := issue_service.NewIssue(ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
  424. if models.IsErrUserDoesNotHaveAccessToRepo(err) {
  425. ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
  426. return
  427. }
  428. ctx.Error(http.StatusInternalServerError, "NewIssue", err)
  429. return
  430. }
  431. if form.Closed {
  432. if err := issue_service.ChangeStatus(issue, ctx.User, true); err != nil {
  433. if models.IsErrDependenciesLeft(err) {
  434. ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
  435. return
  436. }
  437. ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
  438. return
  439. }
  440. }
  441. // Refetch from database to assign some automatic values
  442. issue, err = models.GetIssueByID(issue.ID)
  443. if err != nil {
  444. ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
  445. return
  446. }
  447. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(issue))
  448. }
  449. // EditIssue modify an issue of a repository
  450. func EditIssue(ctx *context.APIContext, form api.EditIssueOption) {
  451. // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue
  452. // ---
  453. // summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.
  454. // consumes:
  455. // - application/json
  456. // produces:
  457. // - application/json
  458. // parameters:
  459. // - name: owner
  460. // in: path
  461. // description: owner of the repo
  462. // type: string
  463. // required: true
  464. // - name: repo
  465. // in: path
  466. // description: name of the repo
  467. // type: string
  468. // required: true
  469. // - name: index
  470. // in: path
  471. // description: index of the issue to edit
  472. // type: integer
  473. // format: int64
  474. // required: true
  475. // - name: body
  476. // in: body
  477. // schema:
  478. // "$ref": "#/definitions/EditIssueOption"
  479. // responses:
  480. // "201":
  481. // "$ref": "#/responses/Issue"
  482. // "403":
  483. // "$ref": "#/responses/forbidden"
  484. // "404":
  485. // "$ref": "#/responses/notFound"
  486. // "412":
  487. // "$ref": "#/responses/error"
  488. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  489. if err != nil {
  490. if models.IsErrIssueNotExist(err) {
  491. ctx.NotFound()
  492. } else {
  493. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  494. }
  495. return
  496. }
  497. issue.Repo = ctx.Repo.Repository
  498. canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
  499. err = issue.LoadAttributes()
  500. if err != nil {
  501. ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
  502. return
  503. }
  504. if !issue.IsPoster(ctx.User.ID) && !canWrite {
  505. ctx.Status(http.StatusForbidden)
  506. return
  507. }
  508. oldTitle := issue.Title
  509. if len(form.Title) > 0 {
  510. issue.Title = form.Title
  511. }
  512. if form.Body != nil {
  513. issue.Content = *form.Body
  514. }
  515. // Update or remove the deadline, only if set and allowed
  516. if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
  517. var deadlineUnix timeutil.TimeStamp
  518. if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
  519. deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  520. 23, 59, 59, 0, form.Deadline.Location())
  521. deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  522. }
  523. if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
  524. ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  525. return
  526. }
  527. issue.DeadlineUnix = deadlineUnix
  528. }
  529. // Add/delete assignees
  530. // Deleting is done the GitHub way (quote from their api documentation):
  531. // https://developer.github.com/v3/issues/#edit-an-issue
  532. // "assignees" (array): Logins for Users to assign to this issue.
  533. // Pass one or more user logins to replace the set of assignees on this Issue.
  534. // Send an empty array ([]) to clear all assignees from the Issue.
  535. if canWrite && (form.Assignees != nil || form.Assignee != nil) {
  536. oneAssignee := ""
  537. if form.Assignee != nil {
  538. oneAssignee = *form.Assignee
  539. }
  540. err = issue_service.UpdateAssignees(issue, oneAssignee, form.Assignees, ctx.User)
  541. if err != nil {
  542. ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
  543. return
  544. }
  545. }
  546. if canWrite && form.Milestone != nil &&
  547. issue.MilestoneID != *form.Milestone {
  548. oldMilestoneID := issue.MilestoneID
  549. issue.MilestoneID = *form.Milestone
  550. if err = issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
  551. ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
  552. return
  553. }
  554. }
  555. if form.State != nil {
  556. issue.IsClosed = (api.StateClosed == api.StateType(*form.State))
  557. }
  558. statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.User)
  559. if err != nil {
  560. if models.IsErrDependenciesLeft(err) {
  561. ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
  562. return
  563. }
  564. ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
  565. return
  566. }
  567. if titleChanged {
  568. notification.NotifyIssueChangeTitle(ctx.User, issue, oldTitle)
  569. }
  570. if statusChangeComment != nil {
  571. notification.NotifyIssueChangeStatus(ctx.User, issue, statusChangeComment, issue.IsClosed)
  572. }
  573. // Refetch from database to assign some automatic values
  574. issue, err = models.GetIssueByID(issue.ID)
  575. if err != nil {
  576. ctx.InternalServerError(err)
  577. return
  578. }
  579. if err = issue.LoadMilestone(); err != nil {
  580. ctx.InternalServerError(err)
  581. return
  582. }
  583. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(issue))
  584. }
  585. // UpdateIssueDeadline updates an issue deadline
  586. func UpdateIssueDeadline(ctx *context.APIContext, form api.EditDeadlineOption) {
  587. // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline
  588. // ---
  589. // 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.
  590. // consumes:
  591. // - application/json
  592. // produces:
  593. // - application/json
  594. // parameters:
  595. // - name: owner
  596. // in: path
  597. // description: owner of the repo
  598. // type: string
  599. // required: true
  600. // - name: repo
  601. // in: path
  602. // description: name of the repo
  603. // type: string
  604. // required: true
  605. // - name: index
  606. // in: path
  607. // description: index of the issue to create or update a deadline on
  608. // type: integer
  609. // format: int64
  610. // required: true
  611. // - name: body
  612. // in: body
  613. // schema:
  614. // "$ref": "#/definitions/EditDeadlineOption"
  615. // responses:
  616. // "201":
  617. // "$ref": "#/responses/IssueDeadline"
  618. // "403":
  619. // "$ref": "#/responses/forbidden"
  620. // "404":
  621. // "$ref": "#/responses/notFound"
  622. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  623. if err != nil {
  624. if models.IsErrIssueNotExist(err) {
  625. ctx.NotFound()
  626. } else {
  627. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  628. }
  629. return
  630. }
  631. if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  632. ctx.Error(http.StatusForbidden, "", "Not repo writer")
  633. return
  634. }
  635. var deadlineUnix timeutil.TimeStamp
  636. var deadline time.Time
  637. if form.Deadline != nil && !form.Deadline.IsZero() {
  638. deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  639. 23, 59, 59, 0, time.Local)
  640. deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  641. }
  642. if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
  643. ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  644. return
  645. }
  646. ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
  647. }