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.

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