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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  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/notification"
  17. "code.gitea.io/gitea/modules/setting"
  18. api "code.gitea.io/gitea/modules/structs"
  19. "code.gitea.io/gitea/modules/timeutil"
  20. "code.gitea.io/gitea/modules/util"
  21. "code.gitea.io/gitea/modules/web"
  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: milestones
  42. // in: query
  43. // description: comma separated list of milestone names. Fetch only issues that have any of this milestones. Non existent are discarded
  44. // type: string
  45. // - name: q
  46. // in: query
  47. // description: search string
  48. // type: string
  49. // - name: priority_repo_id
  50. // in: query
  51. // description: repository to prioritize in the results
  52. // type: integer
  53. // format: int64
  54. // - name: type
  55. // in: query
  56. // description: filter by type (issues / pulls) if set
  57. // type: string
  58. // - name: since
  59. // in: query
  60. // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
  61. // type: string
  62. // format: date-time
  63. // required: false
  64. // - name: before
  65. // in: query
  66. // description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
  67. // type: string
  68. // format: date-time
  69. // required: false
  70. // - name: assigned
  71. // in: query
  72. // description: filter (issues / pulls) assigned to you, default is false
  73. // type: boolean
  74. // - name: created
  75. // in: query
  76. // description: filter (issues / pulls) created by you, default is false
  77. // type: boolean
  78. // - name: mentioned
  79. // in: query
  80. // description: filter (issues / pulls) mentioning you, default is false
  81. // type: boolean
  82. // - name: review_requested
  83. // in: query
  84. // description: filter pulls requesting your review, default is false
  85. // type: boolean
  86. // - name: page
  87. // in: query
  88. // description: page number of results to return (1-based)
  89. // type: integer
  90. // - name: limit
  91. // in: query
  92. // description: page size of results
  93. // type: integer
  94. // responses:
  95. // "200":
  96. // "$ref": "#/responses/IssueList"
  97. before, since, err := utils.GetQueryBeforeSince(ctx)
  98. if err != nil {
  99. ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
  100. return
  101. }
  102. var isClosed util.OptionalBool
  103. switch ctx.FormString("state") {
  104. case "closed":
  105. isClosed = util.OptionalBoolTrue
  106. case "all":
  107. isClosed = util.OptionalBoolNone
  108. default:
  109. isClosed = util.OptionalBoolFalse
  110. }
  111. // find repos user can access (for issue search)
  112. opts := &models.SearchRepoOptions{
  113. Private: false,
  114. AllPublic: true,
  115. TopicOnly: false,
  116. Collaborate: util.OptionalBoolNone,
  117. // This needs to be a column that is not nil in fixtures or
  118. // MySQL will return different results when sorting by null in some cases
  119. OrderBy: models.SearchOrderByAlphabetically,
  120. Actor: ctx.User,
  121. }
  122. if ctx.IsSigned {
  123. opts.Private = true
  124. opts.AllLimited = true
  125. }
  126. repoIDs, _, err := models.SearchRepositoryIDs(opts)
  127. if err != nil {
  128. ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err)
  129. return
  130. }
  131. var issues []*models.Issue
  132. var filteredCount int64
  133. keyword := ctx.FormTrim("q")
  134. if strings.IndexByte(keyword, 0) >= 0 {
  135. keyword = ""
  136. }
  137. var issueIDs []int64
  138. if len(keyword) > 0 && len(repoIDs) > 0 {
  139. if issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword); err != nil {
  140. ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err)
  141. return
  142. }
  143. }
  144. var isPull util.OptionalBool
  145. switch ctx.FormString("type") {
  146. case "pulls":
  147. isPull = util.OptionalBoolTrue
  148. case "issues":
  149. isPull = util.OptionalBoolFalse
  150. default:
  151. isPull = util.OptionalBoolNone
  152. }
  153. labels := ctx.FormTrim("labels")
  154. var includedLabelNames []string
  155. if len(labels) > 0 {
  156. includedLabelNames = strings.Split(labels, ",")
  157. }
  158. milestones := ctx.FormTrim("milestones")
  159. var includedMilestones []string
  160. if len(milestones) > 0 {
  161. includedMilestones = strings.Split(milestones, ",")
  162. }
  163. // this api is also used in UI,
  164. // so the default limit is set to fit UI needs
  165. limit := ctx.FormInt("limit")
  166. if limit == 0 {
  167. limit = setting.UI.IssuePagingNum
  168. } else if limit > setting.API.MaxResponseItems {
  169. limit = setting.API.MaxResponseItems
  170. }
  171. // Only fetch the issues if we either don't have a keyword or the search returned issues
  172. // This would otherwise return all issues if no issues were found by the search.
  173. if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 {
  174. issuesOpt := &models.IssuesOptions{
  175. ListOptions: models.ListOptions{
  176. Page: ctx.FormInt("page"),
  177. PageSize: limit,
  178. },
  179. RepoIDs: repoIDs,
  180. IsClosed: isClosed,
  181. IssueIDs: issueIDs,
  182. IncludedLabelNames: includedLabelNames,
  183. IncludeMilestones: includedMilestones,
  184. SortType: "priorityrepo",
  185. PriorityRepoID: ctx.FormInt64("priority_repo_id"),
  186. IsPull: isPull,
  187. UpdatedBeforeUnix: before,
  188. UpdatedAfterUnix: since,
  189. }
  190. // Filter for: Created by User, Assigned to User, Mentioning User, Review of User Requested
  191. if ctx.FormBool("created") {
  192. issuesOpt.PosterID = ctx.User.ID
  193. }
  194. if ctx.FormBool("assigned") {
  195. issuesOpt.AssigneeID = ctx.User.ID
  196. }
  197. if ctx.FormBool("mentioned") {
  198. issuesOpt.MentionedID = ctx.User.ID
  199. }
  200. if ctx.FormBool("review_requested") {
  201. issuesOpt.ReviewRequestedID = ctx.User.ID
  202. }
  203. if issues, err = models.Issues(issuesOpt); err != nil {
  204. ctx.Error(http.StatusInternalServerError, "Issues", err)
  205. return
  206. }
  207. issuesOpt.ListOptions = models.ListOptions{
  208. Page: -1,
  209. }
  210. if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
  211. ctx.Error(http.StatusInternalServerError, "CountIssues", err)
  212. return
  213. }
  214. }
  215. ctx.SetLinkHeader(int(filteredCount), setting.UI.IssuePagingNum)
  216. ctx.SetTotalCountHeader(filteredCount)
  217. ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
  218. }
  219. // ListIssues list the issues of a repository
  220. func ListIssues(ctx *context.APIContext) {
  221. // swagger:operation GET /repos/{owner}/{repo}/issues issue issueListIssues
  222. // ---
  223. // summary: List a repository's issues
  224. // produces:
  225. // - application/json
  226. // parameters:
  227. // - name: owner
  228. // in: path
  229. // description: owner of the repo
  230. // type: string
  231. // required: true
  232. // - name: repo
  233. // in: path
  234. // description: name of the repo
  235. // type: string
  236. // required: true
  237. // - name: state
  238. // in: query
  239. // description: whether issue is open or closed
  240. // type: string
  241. // enum: [closed, open, all]
  242. // - name: labels
  243. // in: query
  244. // description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
  245. // type: string
  246. // - name: q
  247. // in: query
  248. // description: search string
  249. // type: string
  250. // - name: type
  251. // in: query
  252. // description: filter by type (issues / pulls) if set
  253. // type: string
  254. // enum: [issues, pulls]
  255. // - name: milestones
  256. // in: query
  257. // 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
  258. // type: string
  259. // - name: since
  260. // in: query
  261. // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
  262. // type: string
  263. // format: date-time
  264. // required: false
  265. // - name: before
  266. // in: query
  267. // description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
  268. // type: string
  269. // format: date-time
  270. // required: false
  271. // - name: created_by
  272. // in: query
  273. // description: filter (issues / pulls) created to
  274. // type: string
  275. // - name: assigned_by
  276. // in: query
  277. // description: filter (issues / pulls) assigned to
  278. // type: string
  279. // - name: mentioned_by
  280. // in: query
  281. // description: filter (issues / pulls) mentioning to
  282. // type: string
  283. // - name: page
  284. // in: query
  285. // description: page number of results to return (1-based)
  286. // type: integer
  287. // - name: limit
  288. // in: query
  289. // description: page size of results
  290. // type: integer
  291. // responses:
  292. // "200":
  293. // "$ref": "#/responses/IssueList"
  294. before, since, err := utils.GetQueryBeforeSince(ctx)
  295. if err != nil {
  296. ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
  297. return
  298. }
  299. var isClosed util.OptionalBool
  300. switch ctx.FormString("state") {
  301. case "closed":
  302. isClosed = util.OptionalBoolTrue
  303. case "all":
  304. isClosed = util.OptionalBoolNone
  305. default:
  306. isClosed = util.OptionalBoolFalse
  307. }
  308. var issues []*models.Issue
  309. var filteredCount int64
  310. keyword := ctx.FormTrim("q")
  311. if strings.IndexByte(keyword, 0) >= 0 {
  312. keyword = ""
  313. }
  314. var issueIDs []int64
  315. var labelIDs []int64
  316. if len(keyword) > 0 {
  317. issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword)
  318. if err != nil {
  319. ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err)
  320. return
  321. }
  322. }
  323. if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
  324. labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
  325. if err != nil {
  326. ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
  327. return
  328. }
  329. }
  330. var mileIDs []int64
  331. if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
  332. for i := range part {
  333. // uses names and fall back to ids
  334. // non existent milestones are discarded
  335. mile, err := models.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i])
  336. if err == nil {
  337. mileIDs = append(mileIDs, mile.ID)
  338. continue
  339. }
  340. if !models.IsErrMilestoneNotExist(err) {
  341. ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoIDANDName", err)
  342. return
  343. }
  344. id, err := strconv.ParseInt(part[i], 10, 64)
  345. if err != nil {
  346. continue
  347. }
  348. mile, err = models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, id)
  349. if err == nil {
  350. mileIDs = append(mileIDs, mile.ID)
  351. continue
  352. }
  353. if models.IsErrMilestoneNotExist(err) {
  354. continue
  355. }
  356. ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
  357. }
  358. }
  359. listOptions := utils.GetListOptions(ctx)
  360. var isPull util.OptionalBool
  361. switch ctx.FormString("type") {
  362. case "pulls":
  363. isPull = util.OptionalBoolTrue
  364. case "issues":
  365. isPull = util.OptionalBoolFalse
  366. default:
  367. isPull = util.OptionalBoolNone
  368. }
  369. // FIXME: we should be more efficient here
  370. createdByID := getUserIDForFilter(ctx, "created_by")
  371. if ctx.Written() {
  372. return
  373. }
  374. assignedByID := getUserIDForFilter(ctx, "assigned_by")
  375. if ctx.Written() {
  376. return
  377. }
  378. mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
  379. if ctx.Written() {
  380. return
  381. }
  382. // Only fetch the issues if we either don't have a keyword or the search returned issues
  383. // This would otherwise return all issues if no issues were found by the search.
  384. if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
  385. issuesOpt := &models.IssuesOptions{
  386. ListOptions: listOptions,
  387. RepoIDs: []int64{ctx.Repo.Repository.ID},
  388. IsClosed: isClosed,
  389. IssueIDs: issueIDs,
  390. LabelIDs: labelIDs,
  391. MilestoneIDs: mileIDs,
  392. IsPull: isPull,
  393. UpdatedBeforeUnix: before,
  394. UpdatedAfterUnix: since,
  395. PosterID: createdByID,
  396. AssigneeID: assignedByID,
  397. MentionedID: mentionedByID,
  398. }
  399. if issues, err = models.Issues(issuesOpt); err != nil {
  400. ctx.Error(http.StatusInternalServerError, "Issues", err)
  401. return
  402. }
  403. issuesOpt.ListOptions = models.ListOptions{
  404. Page: -1,
  405. }
  406. if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
  407. ctx.Error(http.StatusInternalServerError, "CountIssues", err)
  408. return
  409. }
  410. }
  411. ctx.SetLinkHeader(int(filteredCount), listOptions.PageSize)
  412. ctx.SetTotalCountHeader(filteredCount)
  413. ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
  414. }
  415. func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
  416. userName := ctx.FormString(queryName)
  417. if len(userName) == 0 {
  418. return 0
  419. }
  420. user, err := models.GetUserByName(userName)
  421. if models.IsErrUserNotExist(err) {
  422. ctx.NotFound(err)
  423. return 0
  424. }
  425. if err != nil {
  426. ctx.InternalServerError(err)
  427. return 0
  428. }
  429. return user.ID
  430. }
  431. // GetIssue get an issue of a repository
  432. func GetIssue(ctx *context.APIContext) {
  433. // swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
  434. // ---
  435. // summary: Get an issue
  436. // produces:
  437. // - application/json
  438. // parameters:
  439. // - name: owner
  440. // in: path
  441. // description: owner of the repo
  442. // type: string
  443. // required: true
  444. // - name: repo
  445. // in: path
  446. // description: name of the repo
  447. // type: string
  448. // required: true
  449. // - name: index
  450. // in: path
  451. // description: index of the issue to get
  452. // type: integer
  453. // format: int64
  454. // required: true
  455. // responses:
  456. // "200":
  457. // "$ref": "#/responses/Issue"
  458. // "404":
  459. // "$ref": "#/responses/notFound"
  460. issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  461. if err != nil {
  462. if models.IsErrIssueNotExist(err) {
  463. ctx.NotFound()
  464. } else {
  465. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  466. }
  467. return
  468. }
  469. ctx.JSON(http.StatusOK, convert.ToAPIIssue(issue))
  470. }
  471. // CreateIssue create an issue of a repository
  472. func CreateIssue(ctx *context.APIContext) {
  473. // swagger:operation POST /repos/{owner}/{repo}/issues issue issueCreateIssue
  474. // ---
  475. // summary: Create an issue. If using deadline only the date will be taken into account, and time of day ignored.
  476. // consumes:
  477. // - application/json
  478. // produces:
  479. // - application/json
  480. // parameters:
  481. // - name: owner
  482. // in: path
  483. // description: owner of the repo
  484. // type: string
  485. // required: true
  486. // - name: repo
  487. // in: path
  488. // description: name of the repo
  489. // type: string
  490. // required: true
  491. // - name: body
  492. // in: body
  493. // schema:
  494. // "$ref": "#/definitions/CreateIssueOption"
  495. // responses:
  496. // "201":
  497. // "$ref": "#/responses/Issue"
  498. // "403":
  499. // "$ref": "#/responses/forbidden"
  500. // "412":
  501. // "$ref": "#/responses/error"
  502. // "422":
  503. // "$ref": "#/responses/validationError"
  504. form := web.GetForm(ctx).(*api.CreateIssueOption)
  505. var deadlineUnix timeutil.TimeStamp
  506. if form.Deadline != nil && ctx.Repo.CanWrite(models.UnitTypeIssues) {
  507. deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
  508. }
  509. issue := &models.Issue{
  510. RepoID: ctx.Repo.Repository.ID,
  511. Repo: ctx.Repo.Repository,
  512. Title: form.Title,
  513. PosterID: ctx.User.ID,
  514. Poster: ctx.User,
  515. Content: form.Body,
  516. Ref: form.Ref,
  517. DeadlineUnix: deadlineUnix,
  518. }
  519. var assigneeIDs = make([]int64, 0)
  520. var err error
  521. if ctx.Repo.CanWrite(models.UnitTypeIssues) {
  522. issue.MilestoneID = form.Milestone
  523. assigneeIDs, err = models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
  524. if err != nil {
  525. if models.IsErrUserNotExist(err) {
  526. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
  527. } else {
  528. ctx.Error(http.StatusInternalServerError, "AddAssigneeByName", err)
  529. }
  530. return
  531. }
  532. // Check if the passed assignees is assignable
  533. for _, aID := range assigneeIDs {
  534. assignee, err := models.GetUserByID(aID)
  535. if err != nil {
  536. ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
  537. return
  538. }
  539. valid, err := models.CanBeAssigned(assignee, ctx.Repo.Repository, false)
  540. if err != nil {
  541. ctx.Error(http.StatusInternalServerError, "canBeAssigned", err)
  542. return
  543. }
  544. if !valid {
  545. ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
  546. return
  547. }
  548. }
  549. } else {
  550. // setting labels is not allowed if user is not a writer
  551. form.Labels = make([]int64, 0)
  552. }
  553. if err := issue_service.NewIssue(ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
  554. if models.IsErrUserDoesNotHaveAccessToRepo(err) {
  555. ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
  556. return
  557. }
  558. ctx.Error(http.StatusInternalServerError, "NewIssue", err)
  559. return
  560. }
  561. if form.Closed {
  562. if err := issue_service.ChangeStatus(issue, ctx.User, true); err != nil {
  563. if models.IsErrDependenciesLeft(err) {
  564. ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
  565. return
  566. }
  567. ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
  568. return
  569. }
  570. }
  571. // Refetch from database to assign some automatic values
  572. issue, err = models.GetIssueByID(issue.ID)
  573. if err != nil {
  574. ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
  575. return
  576. }
  577. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(issue))
  578. }
  579. // EditIssue modify an issue of a repository
  580. func EditIssue(ctx *context.APIContext) {
  581. // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue
  582. // ---
  583. // summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.
  584. // consumes:
  585. // - application/json
  586. // produces:
  587. // - application/json
  588. // parameters:
  589. // - name: owner
  590. // in: path
  591. // description: owner of the repo
  592. // type: string
  593. // required: true
  594. // - name: repo
  595. // in: path
  596. // description: name of the repo
  597. // type: string
  598. // required: true
  599. // - name: index
  600. // in: path
  601. // description: index of the issue to edit
  602. // type: integer
  603. // format: int64
  604. // required: true
  605. // - name: body
  606. // in: body
  607. // schema:
  608. // "$ref": "#/definitions/EditIssueOption"
  609. // responses:
  610. // "201":
  611. // "$ref": "#/responses/Issue"
  612. // "403":
  613. // "$ref": "#/responses/forbidden"
  614. // "404":
  615. // "$ref": "#/responses/notFound"
  616. // "412":
  617. // "$ref": "#/responses/error"
  618. form := web.GetForm(ctx).(*api.EditIssueOption)
  619. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  620. if err != nil {
  621. if models.IsErrIssueNotExist(err) {
  622. ctx.NotFound()
  623. } else {
  624. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  625. }
  626. return
  627. }
  628. issue.Repo = ctx.Repo.Repository
  629. canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
  630. err = issue.LoadAttributes()
  631. if err != nil {
  632. ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
  633. return
  634. }
  635. if !issue.IsPoster(ctx.User.ID) && !canWrite {
  636. ctx.Status(http.StatusForbidden)
  637. return
  638. }
  639. oldTitle := issue.Title
  640. if len(form.Title) > 0 {
  641. issue.Title = form.Title
  642. }
  643. if form.Body != nil {
  644. issue.Content = *form.Body
  645. }
  646. if form.Ref != nil {
  647. err = issue_service.ChangeIssueRef(issue, ctx.User, *form.Ref)
  648. if err != nil {
  649. ctx.Error(http.StatusInternalServerError, "UpdateRef", err)
  650. return
  651. }
  652. }
  653. // Update or remove the deadline, only if set and allowed
  654. if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
  655. var deadlineUnix timeutil.TimeStamp
  656. if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
  657. deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  658. 23, 59, 59, 0, form.Deadline.Location())
  659. deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  660. }
  661. if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
  662. ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  663. return
  664. }
  665. issue.DeadlineUnix = deadlineUnix
  666. }
  667. // Add/delete assignees
  668. // Deleting is done the GitHub way (quote from their api documentation):
  669. // https://developer.github.com/v3/issues/#edit-an-issue
  670. // "assignees" (array): Logins for Users to assign to this issue.
  671. // Pass one or more user logins to replace the set of assignees on this Issue.
  672. // Send an empty array ([]) to clear all assignees from the Issue.
  673. if canWrite && (form.Assignees != nil || form.Assignee != nil) {
  674. oneAssignee := ""
  675. if form.Assignee != nil {
  676. oneAssignee = *form.Assignee
  677. }
  678. err = issue_service.UpdateAssignees(issue, oneAssignee, form.Assignees, ctx.User)
  679. if err != nil {
  680. ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
  681. return
  682. }
  683. }
  684. if canWrite && form.Milestone != nil &&
  685. issue.MilestoneID != *form.Milestone {
  686. oldMilestoneID := issue.MilestoneID
  687. issue.MilestoneID = *form.Milestone
  688. if err = issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
  689. ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
  690. return
  691. }
  692. }
  693. if form.State != nil {
  694. issue.IsClosed = api.StateClosed == api.StateType(*form.State)
  695. }
  696. statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.User)
  697. if err != nil {
  698. if models.IsErrDependenciesLeft(err) {
  699. ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
  700. return
  701. }
  702. ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
  703. return
  704. }
  705. if titleChanged {
  706. notification.NotifyIssueChangeTitle(ctx.User, issue, oldTitle)
  707. }
  708. if statusChangeComment != nil {
  709. notification.NotifyIssueChangeStatus(ctx.User, issue, statusChangeComment, issue.IsClosed)
  710. }
  711. // Refetch from database to assign some automatic values
  712. issue, err = models.GetIssueByID(issue.ID)
  713. if err != nil {
  714. ctx.InternalServerError(err)
  715. return
  716. }
  717. if err = issue.LoadMilestone(); err != nil {
  718. ctx.InternalServerError(err)
  719. return
  720. }
  721. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(issue))
  722. }
  723. // UpdateIssueDeadline updates an issue deadline
  724. func UpdateIssueDeadline(ctx *context.APIContext) {
  725. // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline
  726. // ---
  727. // 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.
  728. // consumes:
  729. // - application/json
  730. // produces:
  731. // - application/json
  732. // parameters:
  733. // - name: owner
  734. // in: path
  735. // description: owner of the repo
  736. // type: string
  737. // required: true
  738. // - name: repo
  739. // in: path
  740. // description: name of the repo
  741. // type: string
  742. // required: true
  743. // - name: index
  744. // in: path
  745. // description: index of the issue to create or update a deadline on
  746. // type: integer
  747. // format: int64
  748. // required: true
  749. // - name: body
  750. // in: body
  751. // schema:
  752. // "$ref": "#/definitions/EditDeadlineOption"
  753. // responses:
  754. // "201":
  755. // "$ref": "#/responses/IssueDeadline"
  756. // "403":
  757. // "$ref": "#/responses/forbidden"
  758. // "404":
  759. // "$ref": "#/responses/notFound"
  760. form := web.GetForm(ctx).(*api.EditDeadlineOption)
  761. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  762. if err != nil {
  763. if models.IsErrIssueNotExist(err) {
  764. ctx.NotFound()
  765. } else {
  766. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  767. }
  768. return
  769. }
  770. if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  771. ctx.Error(http.StatusForbidden, "", "Not repo writer")
  772. return
  773. }
  774. var deadlineUnix timeutil.TimeStamp
  775. var deadline time.Time
  776. if form.Deadline != nil && !form.Deadline.IsZero() {
  777. deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  778. 23, 59, 59, 0, time.Local)
  779. deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  780. }
  781. if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
  782. ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  783. return
  784. }
  785. ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
  786. }