code.gitea.io/gitea@v1.22.3/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/setting" 15 api "code.gitea.io/gitea/modules/structs" 16 "code.gitea.io/gitea/modules/web" 17 "code.gitea.io/gitea/services/context" 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, ctx.Doer, 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 // "423": 192 // "$ref": "#/responses/repoArchivedError" 193 194 // We want to make <:index> depend on <Form>, i.e. <:index> is the target 195 target := getParamsIssue(ctx) 196 if ctx.Written() { 197 return 198 } 199 200 // and <Form> represents the dependency 201 form := web.GetForm(ctx).(*api.IssueMeta) 202 dependency := getFormIssue(ctx, form) 203 if ctx.Written() { 204 return 205 } 206 207 dependencyPerm := getPermissionForRepo(ctx, target.Repo) 208 if ctx.Written() { 209 return 210 } 211 212 createIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm) 213 if ctx.Written() { 214 return 215 } 216 217 ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target)) 218 } 219 220 // RemoveIssueDependency remove an issue dependency 221 func RemoveIssueDependency(ctx *context.APIContext) { 222 // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/dependencies issue issueRemoveIssueDependencies 223 // --- 224 // summary: Remove an issue dependency 225 // produces: 226 // - application/json 227 // parameters: 228 // - name: owner 229 // in: path 230 // description: owner of the repo 231 // type: string 232 // required: true 233 // - name: repo 234 // in: path 235 // description: name of the repo 236 // type: string 237 // required: true 238 // - name: index 239 // in: path 240 // description: index of the issue 241 // type: string 242 // required: true 243 // - name: body 244 // in: body 245 // schema: 246 // "$ref": "#/definitions/IssueMeta" 247 // responses: 248 // "200": 249 // "$ref": "#/responses/Issue" 250 // "404": 251 // "$ref": "#/responses/notFound" 252 // "423": 253 // "$ref": "#/responses/repoArchivedError" 254 255 // We want to make <:index> depend on <Form>, i.e. <:index> is the target 256 target := getParamsIssue(ctx) 257 if ctx.Written() { 258 return 259 } 260 261 // and <Form> represents the dependency 262 form := web.GetForm(ctx).(*api.IssueMeta) 263 dependency := getFormIssue(ctx, form) 264 if ctx.Written() { 265 return 266 } 267 268 dependencyPerm := getPermissionForRepo(ctx, target.Repo) 269 if ctx.Written() { 270 return 271 } 272 273 removeIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm) 274 if ctx.Written() { 275 return 276 } 277 278 ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target)) 279 } 280 281 // GetIssueBlocks list issues that are blocked by this issue 282 func GetIssueBlocks(ctx *context.APIContext) { 283 // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/blocks issue issueListBlocks 284 // --- 285 // summary: List issues that are blocked by this issue 286 // produces: 287 // - application/json 288 // parameters: 289 // - name: owner 290 // in: path 291 // description: owner of the repo 292 // type: string 293 // required: true 294 // - name: repo 295 // in: path 296 // description: name of the repo 297 // type: string 298 // required: true 299 // - name: index 300 // in: path 301 // description: index of the issue 302 // type: string 303 // required: true 304 // - name: page 305 // in: query 306 // description: page number of results to return (1-based) 307 // type: integer 308 // - name: limit 309 // in: query 310 // description: page size of results 311 // type: integer 312 // responses: 313 // "200": 314 // "$ref": "#/responses/IssueList" 315 // "404": 316 // "$ref": "#/responses/notFound" 317 318 // We need to list the issues that DEPEND on this issue not the other way round 319 // Therefore whether dependencies are enabled or not in this repository is potentially irrelevant. 320 321 issue := getParamsIssue(ctx) 322 if ctx.Written() { 323 return 324 } 325 326 if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { 327 ctx.NotFound() 328 return 329 } 330 331 page := ctx.FormInt("page") 332 if page <= 1 { 333 page = 1 334 } 335 limit := ctx.FormInt("limit") 336 if limit <= 1 { 337 limit = setting.API.DefaultPagingNum 338 } 339 340 skip := (page - 1) * limit 341 max := page * limit 342 343 deps, err := issue.BlockingDependencies(ctx) 344 if err != nil { 345 ctx.Error(http.StatusInternalServerError, "BlockingDependencies", err) 346 return 347 } 348 349 var issues []*issues_model.Issue 350 351 repoPerms := make(map[int64]access_model.Permission) 352 repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission 353 354 for i, depMeta := range deps { 355 if i < skip || i >= max { 356 continue 357 } 358 359 // Get the permissions for this repository 360 // If the repo ID exists in the map, return the exist permissions 361 // else get the permission and add it to the map 362 var perm access_model.Permission 363 existPerm, ok := repoPerms[depMeta.RepoID] 364 if ok { 365 perm = existPerm 366 } else { 367 var err error 368 perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer) 369 if err != nil { 370 ctx.ServerError("GetUserRepoPermission", err) 371 return 372 } 373 repoPerms[depMeta.RepoID] = perm 374 } 375 376 if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) { 377 continue 378 } 379 380 depMeta.Issue.Repo = &depMeta.Repository 381 issues = append(issues, &depMeta.Issue) 382 } 383 384 ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues)) 385 } 386 387 // CreateIssueBlocking block the issue given in the body by the issue in path 388 func CreateIssueBlocking(ctx *context.APIContext) { 389 // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/blocks issue issueCreateIssueBlocking 390 // --- 391 // summary: Block the issue given in the body by the issue in path 392 // produces: 393 // - application/json 394 // parameters: 395 // - name: owner 396 // in: path 397 // description: owner of the repo 398 // type: string 399 // required: true 400 // - name: repo 401 // in: path 402 // description: name of the repo 403 // type: string 404 // required: true 405 // - name: index 406 // in: path 407 // description: index of the issue 408 // type: string 409 // required: true 410 // - name: body 411 // in: body 412 // schema: 413 // "$ref": "#/definitions/IssueMeta" 414 // responses: 415 // "201": 416 // "$ref": "#/responses/Issue" 417 // "404": 418 // description: the issue does not exist 419 420 dependency := getParamsIssue(ctx) 421 if ctx.Written() { 422 return 423 } 424 425 form := web.GetForm(ctx).(*api.IssueMeta) 426 target := getFormIssue(ctx, form) 427 if ctx.Written() { 428 return 429 } 430 431 targetPerm := getPermissionForRepo(ctx, target.Repo) 432 if ctx.Written() { 433 return 434 } 435 436 createIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission) 437 if ctx.Written() { 438 return 439 } 440 441 ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency)) 442 } 443 444 // RemoveIssueBlocking unblock the issue given in the body by the issue in path 445 func RemoveIssueBlocking(ctx *context.APIContext) { 446 // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/blocks issue issueRemoveIssueBlocking 447 // --- 448 // summary: Unblock the issue given in the body by the issue in path 449 // produces: 450 // - application/json 451 // parameters: 452 // - name: owner 453 // in: path 454 // description: owner of the repo 455 // type: string 456 // required: true 457 // - name: repo 458 // in: path 459 // description: name of the repo 460 // type: string 461 // required: true 462 // - name: index 463 // in: path 464 // description: index of the issue 465 // type: string 466 // required: true 467 // - name: body 468 // in: body 469 // schema: 470 // "$ref": "#/definitions/IssueMeta" 471 // responses: 472 // "200": 473 // "$ref": "#/responses/Issue" 474 // "404": 475 // "$ref": "#/responses/notFound" 476 477 dependency := getParamsIssue(ctx) 478 if ctx.Written() { 479 return 480 } 481 482 form := web.GetForm(ctx).(*api.IssueMeta) 483 target := getFormIssue(ctx, form) 484 if ctx.Written() { 485 return 486 } 487 488 targetPerm := getPermissionForRepo(ctx, target.Repo) 489 if ctx.Written() { 490 return 491 } 492 493 removeIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission) 494 if ctx.Written() { 495 return 496 } 497 498 ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency)) 499 } 500 501 func getParamsIssue(ctx *context.APIContext) *issues_model.Issue { 502 issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 503 if err != nil { 504 if issues_model.IsErrIssueNotExist(err) { 505 ctx.NotFound("IsErrIssueNotExist", err) 506 } else { 507 ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) 508 } 509 return nil 510 } 511 issue.Repo = ctx.Repo.Repository 512 return issue 513 } 514 515 func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue { 516 var repo *repo_model.Repository 517 if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name { 518 if !setting.Service.AllowCrossRepositoryDependencies { 519 ctx.JSON(http.StatusBadRequest, "CrossRepositoryDependencies not enabled") 520 return nil 521 } 522 var err error 523 repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name) 524 if err != nil { 525 if repo_model.IsErrRepoNotExist(err) { 526 ctx.NotFound("IsErrRepoNotExist", err) 527 } else { 528 ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerAndName", err) 529 } 530 return nil 531 } 532 } else { 533 repo = ctx.Repo.Repository 534 } 535 536 issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index) 537 if err != nil { 538 if issues_model.IsErrIssueNotExist(err) { 539 ctx.NotFound("IsErrIssueNotExist", err) 540 } else { 541 ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) 542 } 543 return nil 544 } 545 issue.Repo = repo 546 return issue 547 } 548 549 func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) *access_model.Permission { 550 if repo.ID == ctx.Repo.Repository.ID { 551 return &ctx.Repo.Permission 552 } 553 554 perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) 555 if err != nil { 556 ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) 557 return nil 558 } 559 560 return &perm 561 } 562 563 func createIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) { 564 if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) { 565 // The target's repository doesn't have dependencies enabled 566 ctx.NotFound() 567 return 568 } 569 570 if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) { 571 // We can't write to the target 572 ctx.NotFound() 573 return 574 } 575 576 if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) { 577 // We can't read the dependency 578 ctx.NotFound() 579 return 580 } 581 582 err := issues_model.CreateIssueDependency(ctx, ctx.Doer, target, dependency) 583 if err != nil { 584 ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err) 585 return 586 } 587 } 588 589 func removeIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) { 590 if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) { 591 // The target's repository doesn't have dependencies enabled 592 ctx.NotFound() 593 return 594 } 595 596 if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) { 597 // We can't write to the target 598 ctx.NotFound() 599 return 600 } 601 602 if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) { 603 // We can't read the dependency 604 ctx.NotFound() 605 return 606 } 607 608 err := issues_model.RemoveIssueDependency(ctx, ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy) 609 if err != nil { 610 ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err) 611 return 612 } 613 }