gitea源码

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