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

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