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