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_dependency.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // Copyright 2023 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package repo
  5. import (
  6. "net/http"
  7. "code.gitea.io/gitea/models/db"
  8. issues_model "code.gitea.io/gitea/models/issues"
  9. access_model "code.gitea.io/gitea/models/perm/access"
  10. repo_model "code.gitea.io/gitea/models/repo"
  11. "code.gitea.io/gitea/modules/context"
  12. "code.gitea.io/gitea/modules/setting"
  13. api "code.gitea.io/gitea/modules/structs"
  14. "code.gitea.io/gitea/modules/web"
  15. "code.gitea.io/gitea/services/convert"
  16. )
  17. // GetIssueDependencies list an issue's dependencies
  18. func GetIssueDependencies(ctx *context.APIContext) {
  19. // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/dependencies issue issueListIssueDependencies
  20. // ---
  21. // summary: List an issue's dependencies, i.e all issues that block this issue.
  22. // produces:
  23. // - application/json
  24. // parameters:
  25. // - name: owner
  26. // in: path
  27. // description: owner of the repo
  28. // type: string
  29. // required: true
  30. // - name: repo
  31. // in: path
  32. // description: name of the repo
  33. // type: string
  34. // required: true
  35. // - name: index
  36. // in: path
  37. // description: index of the issue
  38. // type: string
  39. // required: true
  40. // - name: page
  41. // in: query
  42. // description: page number of results to return (1-based)
  43. // type: integer
  44. // - name: limit
  45. // in: query
  46. // description: page size of results
  47. // type: integer
  48. // responses:
  49. // "200":
  50. // "$ref": "#/responses/IssueList"
  51. // "404":
  52. // "$ref": "#/responses/notFound"
  53. // If this issue's repository does not enable dependencies then there can be no dependencies by default
  54. if !ctx.Repo.Repository.IsDependenciesEnabled(ctx) {
  55. ctx.NotFound()
  56. return
  57. }
  58. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  59. if err != nil {
  60. if issues_model.IsErrIssueNotExist(err) {
  61. ctx.NotFound("IsErrIssueNotExist", err)
  62. } else {
  63. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  64. }
  65. return
  66. }
  67. // 1. We must be able to read this issue
  68. if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
  69. ctx.NotFound()
  70. return
  71. }
  72. page := ctx.FormInt("page")
  73. if page <= 1 {
  74. page = 1
  75. }
  76. limit := ctx.FormInt("limit")
  77. if limit == 0 {
  78. limit = setting.API.DefaultPagingNum
  79. } else if limit > setting.API.MaxResponseItems {
  80. limit = setting.API.MaxResponseItems
  81. }
  82. canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
  83. blockerIssues := make([]*issues_model.Issue, 0, limit)
  84. // 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
  85. blockersInfo, err := issue.BlockedByDependencies(ctx, db.ListOptions{
  86. Page: page,
  87. PageSize: limit,
  88. })
  89. if err != nil {
  90. ctx.Error(http.StatusInternalServerError, "BlockedByDependencies", err)
  91. return
  92. }
  93. var lastRepoID int64
  94. var lastPerm access_model.Permission
  95. for _, blocker := range blockersInfo {
  96. // Get the permissions for this repository
  97. perm := lastPerm
  98. if lastRepoID != blocker.Repository.ID {
  99. if blocker.Repository.ID == ctx.Repo.Repository.ID {
  100. perm = ctx.Repo.Permission
  101. } else {
  102. var err error
  103. perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
  104. if err != nil {
  105. ctx.ServerError("GetUserRepoPermission", err)
  106. return
  107. }
  108. }
  109. lastRepoID = blocker.Repository.ID
  110. }
  111. // check permission
  112. if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
  113. if !canWrite {
  114. hiddenBlocker := &issues_model.DependencyInfo{
  115. Issue: issues_model.Issue{
  116. Title: "HIDDEN",
  117. },
  118. }
  119. blocker = hiddenBlocker
  120. } else {
  121. confidentialBlocker := &issues_model.DependencyInfo{
  122. Issue: issues_model.Issue{
  123. RepoID: blocker.Issue.RepoID,
  124. Index: blocker.Index,
  125. Title: blocker.Title,
  126. IsClosed: blocker.IsClosed,
  127. IsPull: blocker.IsPull,
  128. },
  129. Repository: repo_model.Repository{
  130. ID: blocker.Issue.Repo.ID,
  131. Name: blocker.Issue.Repo.Name,
  132. OwnerName: blocker.Issue.Repo.OwnerName,
  133. },
  134. }
  135. confidentialBlocker.Issue.Repo = &confidentialBlocker.Repository
  136. blocker = confidentialBlocker
  137. }
  138. }
  139. blockerIssues = append(blockerIssues, &blocker.Issue)
  140. }
  141. ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, blockerIssues))
  142. }
  143. // CreateIssueDependency create a new issue dependencies
  144. func CreateIssueDependency(ctx *context.APIContext) {
  145. // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/dependencies issue issueCreateIssueDependencies
  146. // ---
  147. // summary: Make the issue in the url depend on the issue in the form.
  148. // produces:
  149. // - application/json
  150. // parameters:
  151. // - name: owner
  152. // in: path
  153. // description: owner of the repo
  154. // type: string
  155. // required: true
  156. // - name: repo
  157. // in: path
  158. // description: name of the repo
  159. // type: string
  160. // required: true
  161. // - name: index
  162. // in: path
  163. // description: index of the issue
  164. // type: string
  165. // required: true
  166. // - name: body
  167. // in: body
  168. // schema:
  169. // "$ref": "#/definitions/IssueMeta"
  170. // responses:
  171. // "201":
  172. // "$ref": "#/responses/Issue"
  173. // "404":
  174. // description: the issue does not exist
  175. // We want to make <:index> depend on <Form>, i.e. <:index> is the target
  176. target := getParamsIssue(ctx)
  177. if ctx.Written() {
  178. return
  179. }
  180. // and <Form> represents the dependency
  181. form := web.GetForm(ctx).(*api.IssueMeta)
  182. dependency := getFormIssue(ctx, form)
  183. if ctx.Written() {
  184. return
  185. }
  186. dependencyPerm := getPermissionForRepo(ctx, target.Repo)
  187. if ctx.Written() {
  188. return
  189. }
  190. createIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
  191. if ctx.Written() {
  192. return
  193. }
  194. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
  195. }
  196. // RemoveIssueDependency remove an issue dependency
  197. func RemoveIssueDependency(ctx *context.APIContext) {
  198. // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/dependencies issue issueRemoveIssueDependencies
  199. // ---
  200. // summary: Remove an issue dependency
  201. // produces:
  202. // - application/json
  203. // parameters:
  204. // - name: owner
  205. // in: path
  206. // description: owner of the repo
  207. // type: string
  208. // required: true
  209. // - name: repo
  210. // in: path
  211. // description: name of the repo
  212. // type: string
  213. // required: true
  214. // - name: index
  215. // in: path
  216. // description: index of the issue
  217. // type: string
  218. // required: true
  219. // - name: body
  220. // in: body
  221. // schema:
  222. // "$ref": "#/definitions/IssueMeta"
  223. // responses:
  224. // "200":
  225. // "$ref": "#/responses/Issue"
  226. // "404":
  227. // "$ref": "#/responses/notFound"
  228. // We want to make <:index> depend on <Form>, i.e. <:index> is the target
  229. target := getParamsIssue(ctx)
  230. if ctx.Written() {
  231. return
  232. }
  233. // and <Form> represents the dependency
  234. form := web.GetForm(ctx).(*api.IssueMeta)
  235. dependency := getFormIssue(ctx, form)
  236. if ctx.Written() {
  237. return
  238. }
  239. dependencyPerm := getPermissionForRepo(ctx, target.Repo)
  240. if ctx.Written() {
  241. return
  242. }
  243. removeIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
  244. if ctx.Written() {
  245. return
  246. }
  247. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
  248. }
  249. // GetIssueBlocks list issues that are blocked by this issue
  250. func GetIssueBlocks(ctx *context.APIContext) {
  251. // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/blocks issue issueListBlocks
  252. // ---
  253. // summary: List issues that are blocked by this issue
  254. // produces:
  255. // - application/json
  256. // parameters:
  257. // - name: owner
  258. // in: path
  259. // description: owner of the repo
  260. // type: string
  261. // required: true
  262. // - name: repo
  263. // in: path
  264. // description: name of the repo
  265. // type: string
  266. // required: true
  267. // - name: index
  268. // in: path
  269. // description: index of the issue
  270. // type: string
  271. // required: true
  272. // - name: page
  273. // in: query
  274. // description: page number of results to return (1-based)
  275. // type: integer
  276. // - name: limit
  277. // in: query
  278. // description: page size of results
  279. // type: integer
  280. // responses:
  281. // "200":
  282. // "$ref": "#/responses/IssueList"
  283. // "404":
  284. // "$ref": "#/responses/notFound"
  285. // We need to list the issues that DEPEND on this issue not the other way round
  286. // Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
  287. issue := getParamsIssue(ctx)
  288. if ctx.Written() {
  289. return
  290. }
  291. if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
  292. ctx.NotFound()
  293. return
  294. }
  295. page := ctx.FormInt("page")
  296. if page <= 1 {
  297. page = 1
  298. }
  299. limit := ctx.FormInt("limit")
  300. if limit <= 1 {
  301. limit = setting.API.DefaultPagingNum
  302. }
  303. skip := (page - 1) * limit
  304. max := page * limit
  305. deps, err := issue.BlockingDependencies(ctx)
  306. if err != nil {
  307. ctx.Error(http.StatusInternalServerError, "BlockingDependencies", err)
  308. return
  309. }
  310. var lastRepoID int64
  311. var lastPerm access_model.Permission
  312. var issues []*issues_model.Issue
  313. for i, depMeta := range deps {
  314. if i < skip || i >= max {
  315. continue
  316. }
  317. // Get the permissions for this repository
  318. perm := lastPerm
  319. if lastRepoID != depMeta.Repository.ID {
  320. if depMeta.Repository.ID == ctx.Repo.Repository.ID {
  321. perm = ctx.Repo.Permission
  322. } else {
  323. var err error
  324. perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
  325. if err != nil {
  326. ctx.ServerError("GetUserRepoPermission", err)
  327. return
  328. }
  329. }
  330. lastRepoID = depMeta.Repository.ID
  331. }
  332. if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) {
  333. continue
  334. }
  335. depMeta.Issue.Repo = &depMeta.Repository
  336. issues = append(issues, &depMeta.Issue)
  337. }
  338. ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
  339. }
  340. // CreateIssueBlocking block the issue given in the body by the issue in path
  341. func CreateIssueBlocking(ctx *context.APIContext) {
  342. // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/blocks issue issueCreateIssueBlocking
  343. // ---
  344. // summary: Block the issue given in the body by the issue in path
  345. // produces:
  346. // - application/json
  347. // parameters:
  348. // - name: owner
  349. // in: path
  350. // description: owner of the repo
  351. // type: string
  352. // required: true
  353. // - name: repo
  354. // in: path
  355. // description: name of the repo
  356. // type: string
  357. // required: true
  358. // - name: index
  359. // in: path
  360. // description: index of the issue
  361. // type: string
  362. // required: true
  363. // - name: body
  364. // in: body
  365. // schema:
  366. // "$ref": "#/definitions/IssueMeta"
  367. // responses:
  368. // "201":
  369. // "$ref": "#/responses/Issue"
  370. // "404":
  371. // description: the issue does not exist
  372. dependency := getParamsIssue(ctx)
  373. if ctx.Written() {
  374. return
  375. }
  376. form := web.GetForm(ctx).(*api.IssueMeta)
  377. target := getFormIssue(ctx, form)
  378. if ctx.Written() {
  379. return
  380. }
  381. targetPerm := getPermissionForRepo(ctx, target.Repo)
  382. if ctx.Written() {
  383. return
  384. }
  385. createIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
  386. if ctx.Written() {
  387. return
  388. }
  389. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
  390. }
  391. // RemoveIssueBlocking unblock the issue given in the body by the issue in path
  392. func RemoveIssueBlocking(ctx *context.APIContext) {
  393. // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/blocks issue issueRemoveIssueBlocking
  394. // ---
  395. // summary: Unblock the issue given in the body by the issue in path
  396. // produces:
  397. // - application/json
  398. // parameters:
  399. // - name: owner
  400. // in: path
  401. // description: owner of the repo
  402. // type: string
  403. // required: true
  404. // - name: repo
  405. // in: path
  406. // description: name of the repo
  407. // type: string
  408. // required: true
  409. // - name: index
  410. // in: path
  411. // description: index of the issue
  412. // type: string
  413. // required: true
  414. // - name: body
  415. // in: body
  416. // schema:
  417. // "$ref": "#/definitions/IssueMeta"
  418. // responses:
  419. // "200":
  420. // "$ref": "#/responses/Issue"
  421. // "404":
  422. // "$ref": "#/responses/notFound"
  423. dependency := getParamsIssue(ctx)
  424. if ctx.Written() {
  425. return
  426. }
  427. form := web.GetForm(ctx).(*api.IssueMeta)
  428. target := getFormIssue(ctx, form)
  429. if ctx.Written() {
  430. return
  431. }
  432. targetPerm := getPermissionForRepo(ctx, target.Repo)
  433. if ctx.Written() {
  434. return
  435. }
  436. removeIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
  437. if ctx.Written() {
  438. return
  439. }
  440. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
  441. }
  442. func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
  443. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  444. if err != nil {
  445. if issues_model.IsErrIssueNotExist(err) {
  446. ctx.NotFound("IsErrIssueNotExist", err)
  447. } else {
  448. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  449. }
  450. return nil
  451. }
  452. issue.Repo = ctx.Repo.Repository
  453. return issue
  454. }
  455. func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue {
  456. var repo *repo_model.Repository
  457. if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name {
  458. if !setting.Service.AllowCrossRepositoryDependencies {
  459. ctx.JSON(http.StatusBadRequest, "CrossRepositoryDependencies not enabled")
  460. return nil
  461. }
  462. var err error
  463. repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name)
  464. if err != nil {
  465. if repo_model.IsErrRepoNotExist(err) {
  466. ctx.NotFound("IsErrRepoNotExist", err)
  467. } else {
  468. ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerAndName", err)
  469. }
  470. return nil
  471. }
  472. } else {
  473. repo = ctx.Repo.Repository
  474. }
  475. issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index)
  476. if err != nil {
  477. if issues_model.IsErrIssueNotExist(err) {
  478. ctx.NotFound("IsErrIssueNotExist", err)
  479. } else {
  480. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  481. }
  482. return nil
  483. }
  484. issue.Repo = repo
  485. return issue
  486. }
  487. func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) *access_model.Permission {
  488. if repo.ID == ctx.Repo.Repository.ID {
  489. return &ctx.Repo.Permission
  490. }
  491. perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
  492. if err != nil {
  493. ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
  494. return nil
  495. }
  496. return &perm
  497. }
  498. func createIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
  499. if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
  500. // The target's repository doesn't have dependencies enabled
  501. ctx.NotFound()
  502. return
  503. }
  504. if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
  505. // We can't write to the target
  506. ctx.NotFound()
  507. return
  508. }
  509. if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
  510. // We can't read the dependency
  511. ctx.NotFound()
  512. return
  513. }
  514. err := issues_model.CreateIssueDependency(ctx.Doer, target, dependency)
  515. if err != nil {
  516. ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
  517. return
  518. }
  519. }
  520. func removeIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
  521. if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
  522. // The target's repository doesn't have dependencies enabled
  523. ctx.NotFound()
  524. return
  525. }
  526. if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
  527. // We can't write to the target
  528. ctx.NotFound()
  529. return
  530. }
  531. if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
  532. // We can't read the dependency
  533. ctx.NotFound()
  534. return
  535. }
  536. err := issues_model.RemoveIssueDependency(ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy)
  537. if err != nil {
  538. ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
  539. return
  540. }
  541. }