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

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