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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  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. "strings"
  9. "code.gitea.io/gitea/models"
  10. "code.gitea.io/gitea/modules/context"
  11. "code.gitea.io/gitea/modules/indexer"
  12. "code.gitea.io/gitea/modules/setting"
  13. "code.gitea.io/gitea/modules/util"
  14. api "code.gitea.io/sdk/gitea"
  15. )
  16. // ListIssues list the issues of a repository
  17. func ListIssues(ctx *context.APIContext) {
  18. // swagger:operation GET /repos/{owner}/{repo}/issues issue issueListIssues
  19. // ---
  20. // summary: List a repository's issues
  21. // produces:
  22. // - application/json
  23. // parameters:
  24. // - name: owner
  25. // in: path
  26. // description: owner of the repo
  27. // type: string
  28. // required: true
  29. // - name: repo
  30. // in: path
  31. // description: name of the repo
  32. // type: string
  33. // required: true
  34. // - name: state
  35. // in: query
  36. // description: whether issue is open or closed
  37. // type: string
  38. // - name: page
  39. // in: query
  40. // description: page number of requested issues
  41. // type: integer
  42. // - name: q
  43. // in: query
  44. // description: search string
  45. // type: string
  46. // responses:
  47. // "200":
  48. // "$ref": "#/responses/IssueList"
  49. var isClosed util.OptionalBool
  50. switch ctx.Query("state") {
  51. case "closed":
  52. isClosed = util.OptionalBoolTrue
  53. case "all":
  54. isClosed = util.OptionalBoolNone
  55. default:
  56. isClosed = util.OptionalBoolFalse
  57. }
  58. var issues []*models.Issue
  59. keyword := strings.Trim(ctx.Query("q"), " ")
  60. if strings.IndexByte(keyword, 0) >= 0 {
  61. keyword = ""
  62. }
  63. var issueIDs []int64
  64. var err error
  65. if len(keyword) > 0 {
  66. issueIDs, err = indexer.SearchIssuesByKeyword(ctx.Repo.Repository.ID, keyword)
  67. }
  68. // Only fetch the issues if we either don't have a keyword or the search returned issues
  69. // This would otherwise return all issues if no issues were found by the search.
  70. if len(keyword) == 0 || len(issueIDs) > 0 {
  71. issues, err = models.Issues(&models.IssuesOptions{
  72. RepoIDs: []int64{ctx.Repo.Repository.ID},
  73. Page: ctx.QueryInt("page"),
  74. PageSize: setting.UI.IssuePagingNum,
  75. IsClosed: isClosed,
  76. IssueIDs: issueIDs,
  77. })
  78. }
  79. if err != nil {
  80. ctx.Error(500, "Issues", err)
  81. return
  82. }
  83. apiIssues := make([]*api.Issue, len(issues))
  84. for i := range issues {
  85. apiIssues[i] = issues[i].APIFormat()
  86. }
  87. ctx.SetLinkHeader(ctx.Repo.Repository.NumIssues, setting.UI.IssuePagingNum)
  88. ctx.JSON(200, &apiIssues)
  89. }
  90. // GetIssue get an issue of a repository
  91. func GetIssue(ctx *context.APIContext) {
  92. // swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
  93. // ---
  94. // summary: Get an issue
  95. // produces:
  96. // - application/json
  97. // parameters:
  98. // - name: owner
  99. // in: path
  100. // description: owner of the repo
  101. // type: string
  102. // required: true
  103. // - name: repo
  104. // in: path
  105. // description: name of the repo
  106. // type: string
  107. // required: true
  108. // - name: index
  109. // in: path
  110. // description: index of the issue to get
  111. // type: integer
  112. // required: true
  113. // responses:
  114. // "200":
  115. // "$ref": "#/responses/Issue"
  116. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  117. if err != nil {
  118. if models.IsErrIssueNotExist(err) {
  119. ctx.Status(404)
  120. } else {
  121. ctx.Error(500, "GetIssueByIndex", err)
  122. }
  123. return
  124. }
  125. ctx.JSON(200, issue.APIFormat())
  126. }
  127. // CreateIssue create an issue of a repository
  128. func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) {
  129. // swagger:operation POST /repos/{owner}/{repo}/issues issue issueCreateIssue
  130. // ---
  131. // summary: Create an issue
  132. // consumes:
  133. // - application/json
  134. // produces:
  135. // - application/json
  136. // parameters:
  137. // - name: owner
  138. // in: path
  139. // description: owner of the repo
  140. // type: string
  141. // required: true
  142. // - name: repo
  143. // in: path
  144. // description: name of the repo
  145. // type: string
  146. // required: true
  147. // - name: body
  148. // in: body
  149. // schema:
  150. // "$ref": "#/definitions/CreateIssueOption"
  151. // responses:
  152. // "201":
  153. // "$ref": "#/responses/Issue"
  154. var deadlineUnix util.TimeStamp
  155. if form.Deadline != nil && ctx.Repo.IsWriter() {
  156. deadlineUnix = util.TimeStamp(form.Deadline.Unix())
  157. }
  158. issue := &models.Issue{
  159. RepoID: ctx.Repo.Repository.ID,
  160. Title: form.Title,
  161. PosterID: ctx.User.ID,
  162. Poster: ctx.User,
  163. Content: form.Body,
  164. DeadlineUnix: deadlineUnix,
  165. }
  166. var assigneeIDs = make([]int64, 0)
  167. var err error
  168. if ctx.Repo.IsWriter() {
  169. issue.MilestoneID = form.Milestone
  170. assigneeIDs, err = models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
  171. if err != nil {
  172. if models.IsErrUserNotExist(err) {
  173. ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
  174. } else {
  175. ctx.Error(500, "AddAssigneeByName", err)
  176. }
  177. return
  178. }
  179. } else {
  180. // setting labels is not allowed if user is not a writer
  181. form.Labels = make([]int64, 0)
  182. }
  183. if err := models.NewIssue(ctx.Repo.Repository, issue, form.Labels, assigneeIDs, nil); err != nil {
  184. if models.IsErrUserDoesNotHaveAccessToRepo(err) {
  185. ctx.Error(400, "UserDoesNotHaveAccessToRepo", err)
  186. return
  187. }
  188. ctx.Error(500, "NewIssue", err)
  189. return
  190. }
  191. if form.Closed {
  192. if err := issue.ChangeStatus(ctx.User, ctx.Repo.Repository, true); err != nil {
  193. ctx.Error(500, "ChangeStatus", err)
  194. return
  195. }
  196. }
  197. // Refetch from database to assign some automatic values
  198. issue, err = models.GetIssueByID(issue.ID)
  199. if err != nil {
  200. ctx.Error(500, "GetIssueByID", err)
  201. return
  202. }
  203. ctx.JSON(201, issue.APIFormat())
  204. }
  205. // EditIssue modify an issue of a repository
  206. func EditIssue(ctx *context.APIContext, form api.EditIssueOption) {
  207. // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue
  208. // ---
  209. // summary: Edit an issue
  210. // consumes:
  211. // - application/json
  212. // produces:
  213. // - application/json
  214. // parameters:
  215. // - name: owner
  216. // in: path
  217. // description: owner of the repo
  218. // type: string
  219. // required: true
  220. // - name: repo
  221. // in: path
  222. // description: name of the repo
  223. // type: string
  224. // required: true
  225. // - name: index
  226. // in: path
  227. // description: index of the issue to edit
  228. // type: integer
  229. // required: true
  230. // - name: body
  231. // in: body
  232. // schema:
  233. // "$ref": "#/definitions/EditIssueOption"
  234. // responses:
  235. // "201":
  236. // "$ref": "#/responses/Issue"
  237. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  238. if err != nil {
  239. if models.IsErrIssueNotExist(err) {
  240. ctx.Status(404)
  241. } else {
  242. ctx.Error(500, "GetIssueByIndex", err)
  243. }
  244. return
  245. }
  246. if !issue.IsPoster(ctx.User.ID) && !ctx.Repo.IsWriter() {
  247. ctx.Status(403)
  248. return
  249. }
  250. if len(form.Title) > 0 {
  251. issue.Title = form.Title
  252. }
  253. if form.Body != nil {
  254. issue.Content = *form.Body
  255. }
  256. // Update the deadline
  257. var deadlineUnix util.TimeStamp
  258. if form.Deadline != nil && !form.Deadline.IsZero() && ctx.Repo.IsWriter() {
  259. deadlineUnix = util.TimeStamp(form.Deadline.Unix())
  260. }
  261. if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
  262. ctx.Error(500, "UpdateIssueDeadline", err)
  263. return
  264. }
  265. // Add/delete assignees
  266. // Deleting is done the Github way (quote from their api documentation):
  267. // https://developer.github.com/v3/issues/#edit-an-issue
  268. // "assignees" (array): Logins for Users to assign to this issue.
  269. // Pass one or more user logins to replace the set of assignees on this Issue.
  270. // Send an empty array ([]) to clear all assignees from the Issue.
  271. if ctx.Repo.IsWriter() && (form.Assignees != nil || form.Assignee != nil) {
  272. oneAssignee := ""
  273. if form.Assignee != nil {
  274. oneAssignee = *form.Assignee
  275. }
  276. err = models.UpdateAPIAssignee(issue, oneAssignee, form.Assignees, ctx.User)
  277. if err != nil {
  278. ctx.Error(500, "UpdateAPIAssignee", err)
  279. return
  280. }
  281. }
  282. if ctx.Repo.IsWriter() && form.Milestone != nil &&
  283. issue.MilestoneID != *form.Milestone {
  284. oldMilestoneID := issue.MilestoneID
  285. issue.MilestoneID = *form.Milestone
  286. if err = models.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
  287. ctx.Error(500, "ChangeMilestoneAssign", err)
  288. return
  289. }
  290. }
  291. if err = models.UpdateIssue(issue); err != nil {
  292. ctx.Error(500, "UpdateIssue", err)
  293. return
  294. }
  295. if form.State != nil {
  296. if err = issue.ChangeStatus(ctx.User, ctx.Repo.Repository, api.StateClosed == api.StateType(*form.State)); err != nil {
  297. ctx.Error(500, "ChangeStatus", err)
  298. return
  299. }
  300. }
  301. // Refetch from database to assign some automatic values
  302. issue, err = models.GetIssueByID(issue.ID)
  303. if err != nil {
  304. ctx.Error(500, "GetIssueByID", err)
  305. return
  306. }
  307. ctx.JSON(201, issue.APIFormat())
  308. }
  309. // UpdateIssueDeadline updates an issue deadline
  310. func UpdateIssueDeadline(ctx *context.APIContext, form api.EditDeadlineOption) {
  311. // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline
  312. // ---
  313. // summary: Set an issue deadline. If set to null, the deadline is deleted.
  314. // consumes:
  315. // - application/json
  316. // produces:
  317. // - application/json
  318. // parameters:
  319. // - name: owner
  320. // in: path
  321. // description: owner of the repo
  322. // type: string
  323. // required: true
  324. // - name: repo
  325. // in: path
  326. // description: name of the repo
  327. // type: string
  328. // required: true
  329. // - name: index
  330. // in: path
  331. // description: index of the issue to create or update a deadline on
  332. // type: integer
  333. // required: true
  334. // - name: body
  335. // in: body
  336. // schema:
  337. // "$ref": "#/definitions/EditDeadlineOption"
  338. // responses:
  339. // "201":
  340. // "$ref": "#/responses/IssueDeadline"
  341. // "403":
  342. // description: Not repo writer
  343. // schema:
  344. // "$ref": "#/responses/forbidden"
  345. // "404":
  346. // description: Issue not found
  347. // schema:
  348. // "$ref": "#/responses/empty"
  349. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  350. if err != nil {
  351. if models.IsErrIssueNotExist(err) {
  352. ctx.Status(404)
  353. } else {
  354. ctx.Error(500, "GetIssueByIndex", err)
  355. }
  356. return
  357. }
  358. if !ctx.Repo.IsWriter() {
  359. ctx.Status(403)
  360. return
  361. }
  362. var deadlineUnix util.TimeStamp
  363. if form.Deadline != nil && !form.Deadline.IsZero() {
  364. deadlineUnix = util.TimeStamp(form.Deadline.Unix())
  365. }
  366. if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
  367. ctx.Error(500, "UpdateIssueDeadline", err)
  368. return
  369. }
  370. ctx.JSON(201, api.IssueDeadline{Deadline: form.Deadline})
  371. }